1 # -*- coding: utf-8 -*-
12 from azure
.common
.credentials
import ServicePrincipalCredentials
13 from azure
.mgmt
.resource
import ResourceManagementClient
14 from azure
.mgmt
.network
import NetworkManagementClient
15 from azure
.mgmt
.compute
import ComputeManagementClient
16 from azure
.mgmt
.compute
.v2019_03_01
.models
import DiskCreateOptionTypes
17 from msrestazure
.azure_exceptions
import CloudError
18 from msrest
.exceptions
import AuthenticationError
19 from requests
.exceptions
import ConnectionError
21 __author__
= 'Sergio Gonzalez'
22 __date__
= '$18-apr-2019 23:59:59$'
25 if getenv('OSMRO_PDB_DEBUG'):
32 class vimconnector(vimconn
.vimconnector
):
34 # Translate azure provisioning state to OSM provision state
35 # The first three ones are the transitional status once a user initiated action has been requested
36 # Once the operation is complete, it will transition into the states Succeeded or Failed
37 # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/states-lifecycle
38 provision_state2osm
= {
41 "Deleting": "INACTIVE",
42 "Succeeded": "ACTIVE",
46 # Translate azure power state to OSM provision state
48 "starting": "INACTIVE",
50 "stopping": "INACTIVE",
51 "stopped": "INACTIVE",
53 "deallocated": "BUILD",
54 "deallocating": "BUILD"
57 def __init__(self
, uuid
, name
, tenant_id
, tenant_name
, url
, url_admin
=None, user
=None, passwd
=None, log_level
=None,
58 config
={}, persistent_info
={}):
60 Constructor of VIM. Raise an exception is some needed parameter is missing, but it must not do any connectivity
61 checking against the VIM
62 Using common constructor parameters.
63 In this case: config must include the following parameters:
64 subscription_id: assigned azure subscription identifier
65 region_name: current region for azure network
66 resource_group: used for all azure created resources
67 vnet_name: base vnet for azure, created networks will be subnets from this base network
68 config may also include the following parameter:
69 flavors_pattern: pattern that will be used to select a range of vm sizes, for example
70 "^((?!Standard_B).)*$" will filter out Standard_B range that is cheap but is very overused
71 "^Standard_B" will select a serie B maybe for test environment
74 vimconn
.vimconnector
.__init
__(self
, uuid
, name
, tenant_id
, tenant_name
, url
, url_admin
, user
, passwd
, log_level
,
75 config
, persistent_info
)
77 # Variable that indicates if client must be reloaded or initialized
78 self
.reload_client
= True
80 self
.vnet_address_space
= None
82 self
.logger
= logging
.getLogger('openmano.vim.azure')
85 self
.logger
.setLevel(getattr(logging
, log_level
))
87 self
.tenant
= (tenant_id
or tenant_name
)
89 # Store config to create azure subscription later
91 self
._config
["user"] = user
92 self
._config
["passwd"] = passwd
93 self
._config
["tenant"] = (tenant_id
or tenant_name
)
96 if 'subscription_id' in config
:
97 self
._config
["subscription_id"] = config
.get('subscription_id')
98 #self.logger.debug('Setting subscription to: %s', self.config["subscription_id"])
100 raise vimconn
.vimconnException('Subscription not specified')
103 if 'region_name' in config
:
104 self
.region
= config
.get('region_name')
106 raise vimconn
.vimconnException('Azure region_name is not specified at config')
109 if 'resource_group' in config
:
110 self
.resource_group
= config
.get('resource_group')
112 raise vimconn
.vimconnException('Azure resource_group is not specified at config')
115 if 'vnet_name' in config
:
116 self
.vnet_name
= config
["vnet_name"]
119 self
.pub_key
= config
.get('pub_key')
121 # flavor pattern regex
122 if 'flavors_pattern' in config
:
123 self
._config
['flavors_pattern'] = config
['flavors_pattern']
125 def _reload_connection(self
):
127 Called before any operation, checks python azure clients
129 if self
.reload_client
:
130 self
.logger
.debug('reloading azure client')
132 self
.credentials
= ServicePrincipalCredentials(
133 client_id
=self
._config
["user"],
134 secret
=self
._config
["passwd"],
135 tenant
=self
._config
["tenant"]
137 self
.conn
= ResourceManagementClient(self
.credentials
, self
._config
["subscription_id"])
138 self
.conn_compute
= ComputeManagementClient(self
.credentials
, self
._config
["subscription_id"])
139 self
.conn_vnet
= NetworkManagementClient(self
.credentials
, self
._config
["subscription_id"])
140 self
._check
_or
_create
_resource
_group
()
141 self
._check
_or
_create
_vnet
()
143 # Set to client created
144 self
.reload_client
= False
145 except Exception as e
:
146 self
._format
_vimconn
_exception
(e
)
148 def _get_resource_name_from_resource_id(self
, resource_id
):
150 Obtains resource_name from the azure complete identifier: resource_name will always be last item
153 resource
= str(resource_id
.split('/')[-1])
155 except Exception as e
:
156 raise vimconn
.vimconnException("Unable to get resource name from invalid resource_id format: '{}'".format(resource_id
))
158 def _get_location_from_resource_group(self
, resource_group_name
):
160 location
= self
.conn
.resource_groups
.get(resource_group_name
).location
162 except Exception as e
:
163 raise vimconn
.vimconnNotFoundException("Location '{}' not found".format(resource_group_name
))
165 def _get_resource_group_name_from_resource_id(self
, resource_id
):
168 rg
= str(resource_id
.split('/')[4])
170 except Exception as e
:
171 raise vimconn
.vimconnException("Unable to get resource group from invalid resource_id format '{}'".
174 def _get_net_name_from_resource_id(self
, resource_id
):
177 net_name
= str(resource_id
.split('/')[8])
179 except Exception as e
:
180 raise vimconn
.vimconnException("Unable to get azure net_name from invalid resource_id format '{}'".
183 def _check_subnets_for_vm(self
, net_list
):
184 # All subnets must belong to the same resource group and vnet
185 rg_vnet
= set(self
._get
_resource
_group
_name
_from
_resource
_id
(net
['net_id']) +
186 self
._get
_net
_name
_from
_resource
_id
(net
['net_id']) for net
in net_list
)
188 if len(rg_vnet
) != 1:
189 raise self
._format
_vimconn
_exception
('Azure VMs can only attach to subnets in same VNET')
191 def _format_vimconn_exception(self
, e
):
193 Transforms a generic or azure exception to a vimcommException
195 if isinstance(e
, vimconn
.vimconnException
):
197 elif isinstance(e
, AuthenticationError
):
198 raise vimconn
.vimconnAuthException(type(e
).__name
__ + ': ' + str(e
))
199 elif isinstance(e
, ConnectionError
):
200 raise vimconn
.vimconnConnectionException(type(e
).__name
__ + ': ' + str(e
))
202 # In case of generic error recreate client
203 self
.reload_client
= True
204 raise vimconn
.vimconnException(type(e
).__name
__ + ': ' + str(e
))
206 def _check_or_create_resource_group(self
):
208 Creates the base resource group if it does not exist
211 rg_exists
= self
.conn
.resource_groups
.check_existence(self
.resource_group
)
213 self
.logger
.debug("create base rgroup: %s", self
.resource_group
)
214 self
.conn
.resource_groups
.create_or_update(self
.resource_group
, {'location': self
.region
})
215 except Exception as e
:
216 self
._format
_vimconn
_exception
(e
)
218 def _check_or_create_vnet(self
):
220 Try to get existent base vnet, in case it does not exist it creates it
223 vnet
= self
.conn_vnet
.virtual_networks
.get(self
.resource_group
, self
.vnet_name
)
224 self
.vnet_address_space
= vnet
.address_space
.address_prefixes
[0]
225 self
.vnet_id
= vnet
.id
227 except CloudError
as e
:
228 if e
.error
.error
and "notfound" in e
.error
.error
.lower():
230 # continue and create it
232 self
._format
_vimconn
_exception
(e
)
234 # if it does not exist, create it
237 'location': self
.region
,
239 'address_prefixes': ["10.0.0.0/8"]
242 self
.vnet_address_space
= "10.0.0.0/8"
244 self
.logger
.debug("create base vnet: %s", self
.vnet_name
)
245 self
.conn_vnet
.virtual_networks
.create_or_update(self
.resource_group
, self
.vnet_name
, vnet_params
)
246 vnet
= self
.conn_vnet
.virtual_networks
.get(self
.resource_group
, self
.vnet_name
)
247 self
.vnet_id
= vnet
.id
248 except Exception as e
:
249 self
._format
_vimconn
_exception
(e
)
251 def new_network(self
, net_name
, net_type
, ip_profile
=None, shared
=False, vlan
=None):
253 Adds a tenant network to VIM
254 :param net_name: name of the network
255 :param net_type: not used for azure networks
256 :param ip_profile: is a dict containing the IP parameters of the network (Currently only IPv4 is implemented)
257 'ip-version': can be one of ['IPv4','IPv6']
258 'subnet-address': ip_prefix_schema, that is X.X.X.X/Y
259 'gateway-address': (Optional) ip_schema, that is X.X.X.X, not implemented for azure connector
260 'dns-address': (Optional) ip_schema, not implemented for azure connector
261 'dhcp': (Optional) dict containing, not implemented for azure connector
262 'enabled': {'type': 'boolean'},
263 'start-address': ip_schema, first IP to grant
264 'count': number of IPs to grant.
265 :param shared: Not allowed for Azure Connector
266 :param vlan: VLAN tagging is not allowed for Azure
267 :return: a tuple with the network identifier and created_items, or raises an exception on error
268 created_items can be None or a dictionary where this method can include key-values that will be passed to
269 the method delete_network. Can be used to store created segments, created l2gw connections, etc.
270 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
273 return self
._new
_subnet
(net_name
, ip_profile
)
275 def _new_subnet(self
, net_name
, ip_profile
):
277 Adds a tenant network to VIM. It creates a new subnet at existing base vnet
278 :param net_name: subnet name
280 subnet-address: if it is not provided a subnet/24 in the default vnet is created,
281 otherwise it creates a subnet in the indicated address
282 :return: a tuple with the network identifier and created_items, or raises an exception on error
284 self
.logger
.debug('create subnet name %s, ip_profile %s', net_name
, ip_profile
)
285 self
._reload
_connection
()
287 if ip_profile
is None:
288 # get a non used vnet ip range /24 and allocate automatically inside the range self.vnet_address_space
289 used_subnets
= self
.get_network_list()
290 for ip_range
in netaddr
.IPNetwork(self
.vnet_address_space
).subnet(24):
291 for used_subnet
in used_subnets
:
292 subnet_range
= netaddr
.IPNetwork(used_subnet
["cidr_block"])
293 if subnet_range
in ip_range
or ip_range
in subnet_range
:
294 # this range overlaps with an existing subnet ip range. Breaks and look for another
297 ip_profile
= {"subnet_address": str(ip_range
)}
298 self
.logger
.debug('dinamically obtained ip_profile: %s', ip_range
)
301 raise vimconn
.vimconnException("Cannot find a non-used subnet range in {}".
302 format(self
.vnet_address_space
))
304 ip_profile
= {"subnet_address": ip_profile
['subnet_address']}
307 #subnet_name = "{}-{}".format(net_name[:24], uuid4())
309 'address_prefix': ip_profile
['subnet_address']
311 # Assign a not duplicated net name
312 subnet_name
= self
._get
_unused
_subnet
_name
(net_name
)
314 self
.logger
.debug('creating subnet_name: {}'.format(subnet_name
))
315 async_creation
= self
.conn_vnet
.subnets
.create_or_update(self
.resource_group
, self
.vnet_name
,
316 subnet_name
, subnet_params
)
317 async_creation
.wait()
318 self
.logger
.debug('created subnet_name: {}'.format(subnet_name
))
320 return "{}/subnets/{}".format(self
.vnet_id
, subnet_name
), None
321 except Exception as e
:
322 self
._format
_vimconn
_exception
(e
)
324 def _get_unused_subnet_name(self
, subnet_name
):
326 Adds a prefix to the subnet_name with a number in case the indicated name is repeated
327 Checks subnets with the indicated name (without suffix) and adds a suffix with a number
329 all_subnets
= self
.conn_vnet
.subnets
.list(self
.resource_group
, self
.vnet_name
)
330 # Filter to subnets starting with the indicated name
331 subnets
= list(filter(lambda subnet
: (subnet
.name
.startswith(subnet_name
)), all_subnets
))
332 net_names
= [str(subnet
.name
) for subnet
in subnets
]
334 # get the name with the first not used suffix
336 #name = subnet_name + "-" + str(name_suffix)
337 name
= subnet_name
# first subnet created will have no prefix
338 while name
in net_names
:
340 name
= subnet_name
+ "-" + str(name_suffix
)
343 def _create_nic(self
, net
, nic_name
, static_ip
=None):
345 self
.logger
.debug('create nic name %s, net_name %s', nic_name
, net
)
346 self
._reload
_connection
()
348 subnet_id
= net
['net_id']
349 location
= self
._get
_location
_from
_resource
_group
(self
.resource_group
)
352 async_nic_creation
= self
.conn_vnet
.network_interfaces
.create_or_update(
356 'location': location
,
357 'ip_configurations': [{
358 'name': nic_name
+ '-ipconfiguration',
359 'privateIPAddress': static_ip
,
360 'privateIPAllocationMethod': 'Static',
367 async_nic_creation
.wait()
369 ip_configuration_name
= nic_name
+ '-ipconfiguration'
370 async_nic_creation
= self
.conn_vnet
.network_interfaces
.create_or_update(
374 'location': location
,
375 'ip_configurations': [{
376 'name': ip_configuration_name
,
383 async_nic_creation
.wait()
384 self
.logger
.debug('created nic name %s', nic_name
)
386 public_ip
= net
.get('floating_ip')
388 public_ip_address_params
= {
389 'location': location
,
390 'public_ip_allocation_method': 'Dynamic'
392 public_ip_name
= nic_name
+ '-public-ip'
393 public_ip
= self
.conn_vnet
.public_ip_addresses
.create_or_update(
396 public_ip_address_params
398 self
.logger
.debug('created public IP: {}'.format(public_ip
.result()))
400 # Asociate NIC to Public IP
401 nic_data
= self
.conn_vnet
.network_interfaces
.get(
405 nic_data
.ip_configurations
[0].public_ip_address
= public_ip
.result()
407 self
.conn_vnet
.network_interfaces
.create_or_update(
412 except Exception as e
:
413 self
._format
_vimconn
_exception
(e
)
415 return async_nic_creation
.result()
417 def new_flavor(self
, flavor_data
):
419 It is not allowed to create new flavors in Azure, must always use an existing one
421 raise vimconn
.vimconnAuthException("It is not possible to create new flavors in AZURE")
423 def new_tenant(self
, tenant_name
, tenant_description
):
425 It is not allowed to create new tenants in azure
427 raise vimconn
.vimconnAuthException("It is not possible to create a TENANT in AZURE")
429 def new_image(self
, image_dict
):
431 It is not allowed to create new images in Azure, must always use an existing one
433 raise vimconn
.vimconnAuthException("It is not possible to create new images in AZURE")
435 def get_image_id_from_path(self
, path
):
436 """Get the image id from image path in the VIM database.
437 Returns the image_id or raises a vimconnNotFoundException
439 raise vimconn
.vimconnAuthException("It is not possible to obtain image from path in AZURE")
441 def get_image_list(self
, filter_dict
={}):
442 """Obtain tenant images from VIM
444 name: image name with the format: publisher:offer:sku:version
445 If some part of the name is provide ex: publisher:offer it will search all availables skus and version
446 for the provided publisher and offer
447 id: image uuid, currently not supported for azure
448 Returns the image list of dictionaries:
449 [{<the fields at Filter_dict plus some VIM specific>}, ...]
453 self
.logger
.debug("get_image_list filter {}".format(filter_dict
))
455 self
._reload
_connection
()
458 if filter_dict
.get("name"):
459 # name will have the format 'publisher:offer:sku:version'
460 # publisher is required, offer sku and version will be searched if not provided
461 params
= filter_dict
["name"].split(":")
462 publisher
= params
[0]
465 offer_list
= self
._get
_offer
_list
(params
, publisher
)
466 for offer
in offer_list
:
468 sku_list
= self
._get
_sku
_list
(params
, publisher
, offer
)
470 # if version is defined get directly version, else list images
471 if len(params
) == 4 and params
[3]:
473 image_list
= self
._get
_version
_image
_list
(publisher
, offer
, sku
, version
)
475 image_list
= self
._get
_sku
_image
_list
(publisher
, offer
, sku
)
477 raise vimconn
.vimconnAuthException(
478 "List images in Azure must include name param with at least publisher")
480 raise vimconn
.vimconnAuthException("List images in Azure must include name param with at"
484 except Exception as e
:
485 self
._format
_vimconn
_exception
(e
)
487 def _get_offer_list(self
, params
, publisher
):
489 Helper method to obtain offer list for defined publisher
491 if len(params
) >= 2 and params
[1]:
495 # get list of offers from azure
496 result_offers
= self
.conn_compute
.virtual_machine_images
.list_offers(self
.region
, publisher
)
497 return [offer
.name
for offer
in result_offers
]
498 except CloudError
as e
:
499 # azure raises CloudError when not found
500 self
.logger
.info("error listing offers for publisher {}, message: {}".format(publisher
, e
.message
))
503 def _get_sku_list(self
, params
, publisher
, offer
):
505 Helper method to obtain sku list for defined publisher and offer
507 if len(params
) >= 3 and params
[2]:
511 # get list of skus from azure
512 result_skus
= self
.conn_compute
.virtual_machine_images
.list_skus(self
.region
, publisher
, offer
)
513 return [sku
.name
for sku
in result_skus
]
514 except CloudError
as e
:
515 # azure raises CloudError when not found
516 self
.logger
.info("error listing skus for publisher {}, offer {}, message: {}".format(publisher
, offer
, e
.message
))
519 def _get_sku_image_list(self
, publisher
, offer
, sku
):
521 Helper method to obtain image list for publisher, offer and sku
525 result_images
= self
.conn_compute
.virtual_machine_images
.list(self
.region
, publisher
, offer
, sku
)
526 for result_image
in result_images
:
528 'id': str(result_image
.id),
529 'name': ":".join([publisher
, offer
, sku
, result_image
.name
])
531 except CloudError
as e
:
533 "error listing skus for publisher {}, offer {}, message: {}".format(publisher
, offer
, e
.message
))
537 def _get_version_image_list(self
, publisher
, offer
, sku
, version
):
540 result_image
= self
.conn_compute
.virtual_machine_images
.get(self
.region
, publisher
, offer
, sku
, version
)
543 'id': str(result_image
.id),
544 'name': ":".join([publisher
, offer
, sku
, version
])
546 except CloudError
as e
:
547 # azure gives CloudError when not found
549 "error listing images for publisher {}, offer {}, sku {}, vesion {} message: {}".format(publisher
,
557 def get_network_list(self
, filter_dict
={}):
558 """Obtain tenant networks of VIM
562 shared: boolean, not implemented in Azure
563 tenant_id: tenant, not used in Azure, all networks same tenants
564 admin_state_up: boolean, not implemented in Azure
565 status: 'ACTIVE', not implemented in Azure #
566 Returns the network list of dictionaries
568 #self.logger.debug('getting network list for vim, filter %s', filter_dict)
570 self
._reload
_connection
()
572 vnet
= self
.conn_vnet
.virtual_networks
.get(self
.resource_group
, self
.vnet_name
)
575 for subnet
in vnet
.subnets
:
577 if filter_dict
.get("id") and str(subnet
.id) != filter_dict
["id"]:
579 if filter_dict
.get("name") and \
580 str(subnet
.name
) != filter_dict
["name"]:
583 name
= self
._get
_resource
_name
_from
_resource
_id
(subnet
.id)
586 'id': str(subnet
.id),
587 'name': self
._get
_resource
_name
_from
_resource
_id
(subnet
.id),
588 'status': self
.provision_state2osm
[subnet
.provisioning_state
],
589 'cidr_block': str(subnet
.address_prefix
),
596 except Exception as e
:
597 self
._format
_vimconn
_exception
(e
)
599 def new_vminstance(self
, vm_name
, description
, start
, image_id
, flavor_id
, net_list
, cloud_config
=None,
600 disk_list
=None, availability_zone_index
=None, availability_zone_list
=None):
602 return self
._new
_vminstance
(vm_name
, image_id
, flavor_id
, net_list
)
604 def new_vminstance(self
, name
, description
, start
, image_id
, flavor_id
, net_list
, cloud_config
=None,
605 disk_list
=None, availability_zone_index
=None, availability_zone_list
=None):
607 self
.logger
.debug("new vm instance name: %s, image_id: %s, flavor_id: %s, net_list: %s, cloud_config: %s"
608 + "disk_list: %s, availability_zone_index: %s, availability_zone_list: %s",
609 name
, image_id
, flavor_id
, net_list
, cloud_config
, disk_list
,
610 availability_zone_index
, availability_zone_list
)
612 self
._reload
_connection
()
614 # Validate input data is valid
615 # The virtual machine name must have less or 64 characters and it can not have the following
616 # characters: (~ ! @ # $ % ^ & * ( ) = + _ [ ] { } \ | ; : ' " , < > / ?.)
617 vm_name
= self
._check
_vm
_name
(name
)
618 # Obtain vm unused name
619 vm_name
= self
._get
_unused
_vm
_name
(vm_name
)
621 # At least one network must be provided
623 raise vimconn
.vimconnException("At least one net must be provided to create a new VM")
625 # image_id are several fields of the image_id
626 image_reference
= self
._get
_image
_reference
(image_id
)
628 self
._check
_subnets
_for
_vm
(net_list
)
630 for idx
, net
in enumerate(net_list
):
631 # Fault with subnet_id
632 # subnet_id=net['subnet_id']
633 # subnet_id=net['net_id']
634 nic_name
= vm_name
+ '-nic-'+str(idx
)
635 vm_nic
= self
._create
_nic
(net
, nic_name
)
636 vm_nics
.append({'id': str(vm_nic
.id)})
637 net
['vim_id'] = vm_nic
.id
641 # cloud-init configuration
644 config_drive
, userdata
= self
._create
_user
_data
(cloud_config
)
645 custom_data
= base64
.b64encode(userdata
.encode('utf-8')).decode('latin-1')
647 key_pairs
= cloud_config
.get("key-pairs")
649 key_data
= key_pairs
[0]
651 if cloud_config
.get("users"):
652 user_name
= cloud_config
.get("users")[0].get("name", "osm")
654 user_name
= "osm" # DEFAULT USER IS OSM
657 'computer_name': vm_name
,
658 'admin_username': user_name
,
659 'linux_configuration': {
660 "disable_password_authentication": True,
663 "path": "/home/{}/.ssh/authorized_keys".format(user_name
),
668 'custom_data': custom_data
672 'computer_name': vm_name
673 ,'admin_username': 'osm'
674 ,'admin_password': 'Osm4u!',
678 'location': self
.region
,
679 'os_profile': os_profile
,
680 'hardware_profile': {
684 'image_reference': image_reference
688 # Add data disks if they are provided
691 for lun_name
, disk
in enumerate(disk_list
):
692 self
.logger
.debug("add disk size: %s, image: %s", disk
.get("size"), disk
.get("image_id"))
693 if not disk
.get("image_id"):
695 'lun': lun_name
, # You choose the value, depending of what is available for you
696 'name': vm_name
+ "_data_disk-" + str(lun_name
),
697 'create_option': DiskCreateOptionTypes
.empty
,
698 'disk_size_gb': disk
.get("size")
701 self
.logger
.debug("currently not able to create data disks from image for azure, ignoring")
704 vm_parameters
["storage_profile"]["data_disks"] = data_disks
706 # If the machine has several networks one must be marked as primary
707 # As it is not indicated in the interface the first interface will be marked as primary
709 for idx
, vm_nic
in enumerate(vm_nics
):
711 vm_nics
[0]['Primary'] = True
713 vm_nics
[idx
]['Primary'] = False
715 vm_parameters
['network_profile'] = {'network_interfaces': vm_nics
}
717 self
.logger
.debug("create vm name: %s", vm_name
)
718 creation_result
= self
.conn_compute
.virtual_machines
.create_or_update(
723 self
.logger
.debug("created vm name: %s", vm_name
)
725 #creation_result.wait()
726 result
= creation_result
.result()
729 start_result
= self
.conn_compute
.virtual_machines
.start(
734 return result
.id, None
736 #run_command_parameters = {
737 # 'command_id': 'RunShellScript', # For linux, don't change it
739 # 'date > /tmp/test.txt'
742 except Exception as e
:
743 self
.logger
.debug('Exception creating new vminstance: %s', e
, exc_info
=True)
744 self
._format
_vimconn
_exception
(e
)
746 def _get_unused_vm_name(self
, vm_name
):
748 Checks the vm name and in case it is used adds a suffix to the name to allow creation
751 all_vms
= self
.conn_compute
.virtual_machines
.list(self
.resource_group
)
752 # Filter to vms starting with the indicated name
753 vms
= list(filter(lambda vm
: (vm
.name
.startswith(vm_name
)), all_vms
))
754 vm_names
= [str(vm
.name
) for vm
in vms
]
756 # get the name with the first not used suffix
758 # name = subnet_name + "-" + str(name_suffix)
759 name
= vm_name
# first subnet created will have no prefix
760 while name
in vm_names
:
762 name
= vm_name
+ "-" + str(name_suffix
)
766 # It is necesary extract from image_id data to create the VM with this format
767 # 'image_reference': {
768 # 'publisher': vm_reference['publisher'],
769 # 'offer': vm_reference['offer'],
770 # 'sku': vm_reference['sku'],
771 # 'version': vm_reference['version']
773 def _get_image_reference(self
, image_id
):
776 # The data input format example:
777 # /Subscriptions/ca3d18ab-d373-4afb-a5d6-7c44f098d16a/Providers/Microsoft.Compute/Locations/westeurope/
778 # Publishers/Canonical/ArtifactTypes/VMImage/
779 # Offers/UbuntuServer/
781 # Versions/18.04.201809110
782 publisher
= str(image_id
.split('/')[8])
783 offer
= str(image_id
.split('/')[12])
784 sku
= str(image_id
.split('/')[14])
785 version
= str(image_id
.split('/')[16])
788 'publisher': publisher
,
793 except Exception as e
:
794 raise vimconn
.vimconnException(
795 "Unable to get image_reference from invalid image_id format: '{}'".format(image_id
))
797 # Azure VM names can not have some special characters
798 def _check_vm_name(self
, vm_name
):
800 Checks vm name, in case the vm has not allowed characters they are removed, not error raised
803 #chars_not_allowed_list = ['~','!','@','#','$','%','^','&','*','(',')','=','+','_','[',']','{','}','|',';',':','<','>','/','?','.']
804 chars_not_allowed_list
= "~!@#$%^&*()=+_[]{}|;:<>/?."
806 # First: the VM name max length is 64 characters
807 vm_name_aux
= vm_name
[:64]
809 # Second: replace not allowed characters
810 for elem
in chars_not_allowed_list
:
811 # Check if string is in the main string
812 if elem
in vm_name_aux
:
813 #self.logger.debug('Dentro del IF')
815 vm_name_aux
= vm_name_aux
.replace(elem
, '-')
819 def get_flavor_id_from_data(self
, flavor_dict
):
821 self
.logger
.debug("getting flavor id from data, flavor_dict: %s", flavor_dict
)
822 filter_dict
= flavor_dict
or {}
824 self
._reload
_connection
()
825 vm_sizes_list
= [vm_size
.serialize() for vm_size
in self
.conn_compute
.virtual_machine_sizes
.list(self
.region
)]
827 cpus
= filter_dict
.get('vcpus') or 0
828 memMB
= filter_dict
.get('ram') or 0
831 if self
._config
.get("flavors_pattern"):
832 filtered_sizes
= [size
for size
in vm_sizes_list
if size
['numberOfCores'] >= cpus
833 and size
['memoryInMB'] >= memMB
834 and re
.search(self
._config
.get("flavors_pattern"), size
["name"])]
836 filtered_sizes
= [size
for size
in vm_sizes_list
if size
['numberOfCores'] >= cpus
837 and size
['memoryInMB'] >= memMB
]
842 listedFilteredSizes
= sorted(filtered_sizes
, key
=lambda k
: (k
['numberOfCores'], k
['memoryInMB'], k
['resourceDiskSizeInMB']))
844 if listedFilteredSizes
:
845 return listedFilteredSizes
[0]['name']
846 raise vimconn
.vimconnNotFoundException("Cannot find any flavor matching '{}'".format(str(flavor_dict
)))
848 except Exception as e
:
849 self
._format
_vimconn
_exception
(e
)
851 def _get_flavor_id_from_flavor_name(self
, flavor_name
):
853 #self.logger.debug("getting flavor id from flavor name {}".format(flavor_name))
855 self
._reload
_connection
()
856 vm_sizes_list
= [vm_size
.serialize() for vm_size
in self
.conn_compute
.virtual_machine_sizes
.list(self
.region
)]
859 for size
in vm_sizes_list
:
860 if size
['name'] == flavor_name
:
863 # Si no se encuentra ninguno, este metodo devuelve None
866 except Exception as e
:
867 self
._format
_vimconn
_exception
(e
)
869 def check_vim_connectivity(self
):
871 self
._reload
_connection
()
873 except Exception as e
:
874 raise vimconn
.vimconnException("Connectivity issue with Azure API: {}".format(e
))
876 def get_network(self
, net_id
):
878 #self.logger.debug('get network id: {}'.format(net_id))
879 res_name
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
880 self
._reload
_connection
()
882 filter_dict
= {'name': net_id
}
883 network_list
= self
.get_network_list(filter_dict
)
886 raise vimconn
.vimconnNotFoundException("network '{}' not found".format(net_id
))
888 return network_list
[0]
890 def delete_network(self
, net_id
, created_items
=None):
892 self
.logger
.debug('deleting network {} - {}'.format(self
.resource_group
, net_id
))
894 self
._reload
_connection
()
895 res_name
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
896 filter_dict
= {'name': res_name
}
897 network_list
= self
.get_network_list(filter_dict
)
899 raise vimconn
.vimconnNotFoundException("network '{}' not found".format(net_id
))
902 # Subnet API fails (CloudError: Azure Error: ResourceNotFound)
903 # Put the initial virtual_network API
904 async_delete
= self
.conn_vnet
.subnets
.delete(self
.resource_group
, self
.vnet_name
, res_name
)
908 except CloudError
as e
:
909 if e
.error
.error
and "notfound" in e
.error
.error
.lower():
910 raise vimconn
.vimconnNotFoundException("network '{}' not found".format(net_id
))
912 self
._format
_vimconn
_exception
(e
)
913 except Exception as e
:
914 self
._format
_vimconn
_exception
(e
)
916 def delete_vminstance(self
, vm_id
, created_items
=None):
917 """ Deletes a vm instance from the vim.
919 self
.logger
.debug('deleting VM instance {} - {}'.format(self
.resource_group
, vm_id
))
920 self
._reload
_connection
()
924 res_name
= self
._get
_resource
_name
_from
_resource
_id
(vm_id
)
925 vm
= self
.conn_compute
.virtual_machines
.get(self
.resource_group
, res_name
)
927 # Shuts down the virtual machine and releases the compute resources
928 #vm_stop = self.conn_compute.virtual_machines.power_off(self.resource_group, resName)
931 vm_delete
= self
.conn_compute
.virtual_machines
.delete(self
.resource_group
, res_name
)
933 self
.logger
.debug('deleted VM name: %s', res_name
)
936 os_disk_name
= vm
.storage_profile
.os_disk
.name
937 self
.logger
.debug('delete OS DISK: %s', os_disk_name
)
938 self
.conn_compute
.disks
.delete(self
.resource_group
, os_disk_name
)
939 self
.logger
.debug('deleted OS DISK name: %s', os_disk_name
)
941 for data_disk
in vm
.storage_profile
.data_disks
:
942 self
.logger
.debug('delete data_disk: %s', data_disk
.name
)
943 self
.conn_compute
.disks
.delete(self
.resource_group
, data_disk
.name
)
944 self
.logger
.debug('deleted OS DISK name: %s', data_disk
.name
)
946 # After deleting VM, it is necessary to delete NIC, because if is not deleted delete_network
947 # does not work because Azure says that is in use the subnet
948 network_interfaces
= vm
.network_profile
.network_interfaces
950 for network_interface
in network_interfaces
:
952 nic_name
= self
._get
_resource
_name
_from
_resource
_id
(network_interface
.id)
953 nic_data
= self
.conn_vnet
.network_interfaces
.get(
957 public_ip_name
= None
958 exist_public_ip
= nic_data
.ip_configurations
[0].public_ip_address
960 public_ip_id
= nic_data
.ip_configurations
[0].public_ip_address
.id
963 public_ip_name
= self
._get
_resource
_name
_from
_resource
_id
(public_ip_id
)
965 # Public ip must be deleted afterwards of nic that is attached
967 self
.logger
.debug('delete NIC name: %s', nic_name
)
968 nic_delete
= self
.conn_vnet
.network_interfaces
.delete(self
.resource_group
, nic_name
)
970 self
.logger
.debug('deleted NIC name: %s', nic_name
)
972 # Delete list of public ips
974 self
.logger
.debug('delete PUBLIC IP - ' + public_ip_name
)
975 public_ip
= self
.conn_vnet
.public_ip_addresses
.delete(self
.resource_group
, public_ip_name
)
977 except CloudError
as e
:
978 if e
.error
.error
and "notfound" in e
.error
.error
.lower():
979 raise vimconn
.vimconnNotFoundException("No vm instance found '{}'".format(vm_id
))
981 self
._format
_vimconn
_exception
(e
)
982 except Exception as e
:
983 self
._format
_vimconn
_exception
(e
)
985 def action_vminstance(self
, vm_id
, action_dict
, created_items
= {}):
986 """Send and action over a VM instance from VIM
987 Returns the vm_id if the action was successfully sent to the VIM
990 self
.logger
.debug("Action over VM '%s': %s", vm_id
, str(action_dict
))
992 self
._reload
_connection
()
993 resName
= self
._get
_resource
_name
_from
_resource
_id
(vm_id
)
994 if "start" in action_dict
:
995 self
.conn_compute
.virtual_machines
.start(self
.resource_group
, resName
)
996 elif "stop" in action_dict
or "shutdown" in action_dict
or "shutoff" in action_dict
:
997 self
.conn_compute
.virtual_machines
.power_off(self
.resource_group
, resName
)
998 elif "terminate" in action_dict
:
999 self
.conn_compute
.virtual_machines
.delete(self
.resource_group
, resName
)
1000 elif "reboot" in action_dict
:
1001 self
.conn_compute
.virtual_machines
.restart(self
.resource_group
, resName
)
1003 except CloudError
as e
:
1004 if e
.error
.error
and "notfound" in e
.error
.error
.lower():
1005 raise vimconn
.vimconnNotFoundException("No vm found '{}'".format(vm_id
))
1007 self
._format
_vimconn
_exception
(e
)
1008 except Exception as e
:
1009 self
._format
_vimconn
_exception
(e
)
1011 def delete_flavor(self
, flavor_id
):
1012 raise vimconn
.vimconnAuthException("It is not possible to delete a FLAVOR in AZURE")
1014 def delete_tenant(self
, tenant_id
,):
1015 raise vimconn
.vimconnAuthException("It is not possible to delete a TENANT in AZURE")
1017 def delete_image(self
, image_id
):
1018 raise vimconn
.vimconnAuthException("It is not possible to delete a IMAGE in AZURE")
1020 def get_vminstance(self
, vm_id
):
1022 Obtaing the vm instance data from v_id
1024 self
.logger
.debug("get vm instance: %s", vm_id
)
1025 self
._reload
_connection
()
1027 resName
= self
._get
_resource
_name
_from
_resource
_id
(vm_id
)
1028 vm
=self
.conn_compute
.virtual_machines
.get(self
.resource_group
, resName
)
1029 except CloudError
as e
:
1030 if e
.error
.error
and "notfound" in e
.error
.error
.lower():
1031 raise vimconn
.vimconnNotFoundException("No vminstance found '{}'".format(vm_id
))
1033 self
._format
_vimconn
_exception
(e
)
1034 except Exception as e
:
1035 self
._format
_vimconn
_exception
(e
)
1039 def get_flavor(self
, flavor_id
):
1041 Obtains the flavor_data from the flavor_id
1043 self
._reload
_connection
()
1044 self
.logger
.debug("get flavor from id: %s", flavor_id
)
1045 flavor_data
= self
._get
_flavor
_id
_from
_flavor
_name
(flavor_id
)
1050 'ram': flavor_data
['memoryInMB'],
1051 'vcpus': flavor_data
['numberOfCores'],
1052 'disk': flavor_data
['resourceDiskSizeInMB']/1024
1056 raise vimconn
.vimconnNotFoundException("flavor '{}' not found".format(flavor_id
))
1058 def get_tenant_list(self
, filter_dict
={}):
1059 """ Obtains the list of tenants
1060 For the azure connector only the azure tenant will be returned if it is compatible
1063 tenants_azure
=[{'name': self
.tenant
, 'id': self
.tenant
}]
1066 self
.logger
.debug("get tenant list: %s", filter_dict
)
1067 for tenant_azure
in tenants_azure
:
1069 if filter_dict
.get("id") and str(tenant_azure
.get("id")) != filter_dict
["id"]:
1071 if filter_dict
.get("name") and str(tenant_azure
.get("name")) != filter_dict
["name"]:
1074 tenant_list
.append(tenant_azure
)
1078 def refresh_nets_status(self
, net_list
):
1079 """Get the status of the networks
1080 Params: the list of network identifiers
1081 Returns a dictionary with:
1082 net_id: #VIM id of this network
1083 status: #Mandatory. Text with one of:
1084 # DELETED (not found at vim)
1085 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1086 # OTHER (Vim reported other status not understood)
1087 # ERROR (VIM indicates an ERROR status)
1088 # ACTIVE, INACTIVE, DOWN (admin down),
1089 # BUILD (on building process)
1091 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1092 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1097 self
._reload
_connection
()
1099 self
.logger
.debug("reload nets status net_list: %s", net_list
)
1100 for net_id
in net_list
:
1102 netName
= self
._get
_net
_name
_from
_resource
_id
(net_id
)
1103 resName
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
1105 net
= self
.conn_vnet
.subnets
.get(self
.resource_group
, netName
, resName
)
1107 out_nets
[net_id
] = {
1108 "status": self
.provision_state2osm
[net
.provisioning_state
],
1109 "vim_info": str(net
)
1111 except CloudError
as e
:
1112 if e
.error
.error
and "notfound" in e
.error
.error
.lower():
1113 self
.logger
.info("Not found subnet net_name: %s, subnet_name: %s", netName
, resName
)
1114 out_nets
[net_id
] = {
1115 "status": "DELETED",
1119 self
.logger
.error("CloudError Exception %s when searching subnet", e
)
1120 out_nets
[net_id
] = {
1121 "status": "VIM_ERROR",
1124 except vimconn
.vimconnNotFoundException
as e
:
1125 self
.logger
.error("VimConnNotFoundException %s when searching subnet", e
)
1126 out_nets
[net_id
] = {
1127 "status": "DELETED",
1130 except Exception as e
:
1131 self
.logger
.error("Exception %s when searching subnet", e
, exc_info
=True)
1132 out_nets
[net_id
] = {
1133 "status": "VIM_ERROR",
1138 def refresh_vms_status(self
, vm_list
):
1139 """ Get the status of the virtual machines and their interfaces/ports
1140 Params: the list of VM identifiers
1141 Returns a dictionary with:
1142 vm_id: # VIM id of this Virtual Machine
1143 status: # Mandatory. Text with one of:
1144 # DELETED (not found at vim)
1145 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1146 # OTHER (Vim reported other status not understood)
1147 # ERROR (VIM indicates an ERROR status)
1148 # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
1149 # BUILD (on building process), ERROR
1150 # ACTIVE:NoMgmtIP (Active but none of its interfaces has an IP address
1151 # (ACTIVE:NoMgmtIP is not returned for Azure)
1153 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1154 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1155 interfaces: list with interface info. Each item a dictionary with:
1156 vim_interface_id - The ID of the interface
1157 mac_address - The MAC address of the interface.
1158 ip_address - The IP address of the interface within the subnet.
1162 self
._reload
_connection
()
1164 self
.logger
.debug("refresh vm status vm_list: %s", vm_list
)
1165 search_vm_list
= vm_list
or {}
1167 for vm_id
in search_vm_list
:
1170 res_name
= self
._get
_resource
_name
_from
_resource
_id
(vm_id
)
1172 vm
= self
.conn_compute
.virtual_machines
.get(self
.resource_group
, res_name
)
1173 out_vm
['vim_info'] = str(vm
)
1174 out_vm
['status'] = self
.provision_state2osm
.get(vm
.provisioning_state
, 'OTHER')
1175 if vm
.provisioning_state
== 'Succeeded':
1176 # check if machine is running or stopped
1177 instance_view
= self
.conn_compute
.virtual_machines
.instance_view(self
.resource_group
,
1179 for status
in instance_view
.statuses
:
1180 splitted_status
= status
.code
.split("/")
1181 if len(splitted_status
) == 2 and splitted_status
[0] == 'PowerState':
1182 out_vm
['status'] = self
.power_state2osm
.get(splitted_status
[1], 'OTHER')
1184 network_interfaces
= vm
.network_profile
.network_interfaces
1185 out_vm
['interfaces'] = self
._get
_vm
_interfaces
_status
(vm_id
, network_interfaces
)
1187 except CloudError
as e
:
1188 if e
.error
.error
and "notfound" in e
.error
.error
.lower():
1189 self
.logger
.debug("Not found vm id: %s", vm_id
)
1190 out_vm
['status'] = "DELETED"
1191 out_vm
['error_msg'] = str(e
)
1192 out_vm
['vim_info'] = None
1194 # maybe connection error or another type of error, return vim error
1195 self
.logger
.error("Exception %s refreshing vm_status", e
)
1196 out_vm
['status'] = "VIM_ERROR"
1197 out_vm
['error_msg'] = str(e
)
1198 out_vm
['vim_info'] = None
1199 except Exception as e
:
1200 self
.logger
.error("Exception %s refreshing vm_status", e
, exc_info
=True)
1201 out_vm
['status'] = "VIM_ERROR"
1202 out_vm
['error_msg'] = str(e
)
1203 out_vm
['vim_info'] = None
1205 out_vms
[vm_id
] = out_vm
1209 def _get_vm_interfaces_status(self
, vm_id
, interfaces
):
1211 Gets the interfaces detail for a vm
1212 :param interfaces: List of interfaces.
1213 :return: Dictionary with list of interfaces including, vim_interface_id, mac_address and ip_address
1217 for network_interface
in interfaces
:
1219 nic_name
= self
._get
_resource
_name
_from
_resource
_id
(network_interface
.id)
1220 interface_dict
['vim_interface_id'] = network_interface
.id
1222 nic_data
= self
.conn_vnet
.network_interfaces
.get(
1223 self
.resource_group
,
1226 private_ip
= nic_data
.ip_configurations
[0].private_ip_address
1228 interface_dict
['mac_address'] = nic_data
.mac_address
1229 interface_dict
['ip_address'] = private_ip
1230 interface_list
.append(interface_dict
)
1232 return interface_list
1233 except Exception as e
:
1234 self
.logger
.error("Exception %s obtaining interface data for vm: %s, error: %s", vm_id
, e
, exc_info
=True)
1235 self
._format
_vimconn
_exception
(e
)
1238 if __name__
== "__main__":
1240 # Making some basic test
1243 needed_test_params
= {
1244 "client_id": "AZURE_CLIENT_ID",
1245 "secret": "AZURE_SECRET",
1246 "tenant": "AZURE_TENANT",
1247 "resource_group": "AZURE_RESOURCE_GROUP",
1248 "subscription_id": "AZURE_SUBSCRIPTION_ID",
1249 "vnet_name": "AZURE_VNET_NAME",
1253 for param
, env_var
in needed_test_params
.items():
1254 value
= getenv(env_var
)
1256 raise Exception("Provide a valid value for env '{}'".format(env_var
))
1257 test_params
[param
] = value
1260 'region_name': getenv("AZURE_REGION_NAME", 'westeurope'),
1261 'resource_group': getenv("AZURE_RESOURCE_GROUP"),
1262 'subscription_id': getenv("AZURE_SUBSCRIPTION_ID"),
1263 'pub_key': getenv("AZURE_PUB_KEY", None),
1264 'vnet_name': getenv("AZURE_VNET_NAME", 'myNetwork'),
1269 'description': 'new VM',
1270 'status': 'running',
1272 'publisher': 'Canonical',
1273 'offer': 'UbuntuServer',
1274 'sku': '16.04.0-LTS',
1277 'hardware_profile': {
1278 'vm_size': 'Standard_DS1_v2'
1286 'subnet_address': '10.1.2.0/24',
1287 #'subnet_name': 'subnet-oam'
1289 ###########################
1291 azure
= vimconnector(vim_id
, vim_name
, tenant_id
=test_params
["tenant"], tenant_name
=None, url
=None, url_admin
=None,
1292 user
=test_params
["client_id"], passwd
=test_params
["secret"], log_level
=None, config
=config
)
1294 # azure.get_flavor_id_from_data("here")
1295 # subnets=azure.get_network_list()
1296 # azure.new_vminstance(virtualMachine['name'], virtualMachine['description'], virtualMachine['status'],
1297 # virtualMachine['image'], virtualMachine['hardware_profile']['vm_size'], subnets)
1299 azure
.new_network("mynet", None)
1300 net_id
= "/subscriptions/82f80cc1-876b-4591-9911-1fb5788384fd/resourceGroups/osmRG/providers/Microsoft."\
1301 "Network/virtualNetworks/test"
1302 net_id_not_found
= "/subscriptions/82f80cc1-876b-4591-9911-1fb5788384fd/resourceGroups/osmRG/providers/"\
1303 "Microsoft.Network/virtualNetworks/testALF"
1304 azure
.refresh_nets_status([net_id
, net_id_not_found
])