1 # -*- coding: utf-8 -*-
3 # Licensed under the Apache License, Version 2.0 (the "License"); you may
4 # not use this file except in compliance with the License. You may obtain
5 # a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 # License for the specific language governing permissions and limitations
23 from azure
.common
.credentials
import ServicePrincipalCredentials
24 from azure
.mgmt
.resource
import ResourceManagementClient
25 from azure
.mgmt
.network
import NetworkManagementClient
26 from azure
.mgmt
.compute
import ComputeManagementClient
27 from azure
.mgmt
.compute
.models
import DiskCreateOption
28 from msrestazure
.azure_exceptions
import CloudError
29 from msrest
.exceptions
import AuthenticationError
30 from requests
.exceptions
import ConnectionError
32 __author__
= 'Isabel Lloret, Sergio Gonzalez, Alfonso Tierno'
33 __date__
= '$18-apr-2019 23:59:59$'
36 if getenv('OSMRO_PDB_DEBUG'):
43 class vimconnector(vimconn
.vimconnector
):
45 # Translate azure provisioning state to OSM provision state
46 # The first three ones are the transitional status once a user initiated action has been requested
47 # Once the operation is complete, it will transition into the states Succeeded or Failed
48 # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/states-lifecycle
49 provision_state2osm
= {
52 "Deleting": "INACTIVE",
53 "Succeeded": "ACTIVE",
57 # Translate azure power state to OSM provision state
59 "starting": "INACTIVE",
61 "stopping": "INACTIVE",
62 "stopped": "INACTIVE",
64 "deallocated": "BUILD",
65 "deallocating": "BUILD"
68 def __init__(self
, uuid
, name
, tenant_id
, tenant_name
, url
, url_admin
=None, user
=None, passwd
=None, log_level
=None,
69 config
={}, persistent_info
={}):
71 Constructor of VIM. Raise an exception is some needed parameter is missing, but it must not do any connectivity
72 checking against the VIM
73 Using common constructor parameters.
74 In this case: config must include the following parameters:
75 subscription_id: assigned azure subscription identifier
76 region_name: current region for azure network
77 resource_group: used for all azure created resources
78 vnet_name: base vnet for azure, created networks will be subnets from this base network
79 config may also include the following parameter:
80 flavors_pattern: pattern that will be used to select a range of vm sizes, for example
81 "^((?!Standard_B).)*$" will filter out Standard_B range that is cheap but is very overused
82 "^Standard_B" will select a serie B maybe for test environment
85 vimconn
.vimconnector
.__init
__(self
, uuid
, name
, tenant_id
, tenant_name
, url
, url_admin
, user
, passwd
, log_level
,
86 config
, persistent_info
)
88 # Variable that indicates if client must be reloaded or initialized
89 self
.reload_client
= True
91 self
.vnet_address_space
= None
93 self
.logger
= logging
.getLogger('openmano.vim.azure')
96 self
.logger
.setLevel(getattr(logging
, log_level
))
98 self
.tenant
= (tenant_id
or tenant_name
)
100 # Store config to create azure subscription later
104 "tenant": tenant_id
or tenant_name
108 if 'subscription_id' in config
:
109 self
._config
["subscription_id"] = config
.get('subscription_id')
110 # self.logger.debug('Setting subscription to: %s', self.config["subscription_id"])
112 raise vimconn
.vimconnException('Subscription not specified')
115 if 'region_name' in config
:
116 self
.region
= config
.get('region_name')
118 raise vimconn
.vimconnException('Azure region_name is not specified at config')
121 if 'resource_group' in config
:
122 self
.resource_group
= config
.get('resource_group')
124 raise vimconn
.vimconnException('Azure resource_group is not specified at config')
127 if 'vnet_name' in config
:
128 self
.vnet_name
= config
["vnet_name"]
131 self
.pub_key
= config
.get('pub_key')
133 # flavor pattern regex
134 if 'flavors_pattern' in config
:
135 self
._config
['flavors_pattern'] = config
['flavors_pattern']
137 def _reload_connection(self
):
139 Called before any operation, checks python azure clients
141 if self
.reload_client
:
142 self
.logger
.debug('reloading azure client')
144 self
.credentials
= ServicePrincipalCredentials(
145 client_id
=self
._config
["user"],
146 secret
=self
._config
["passwd"],
147 tenant
=self
._config
["tenant"]
149 self
.conn
= ResourceManagementClient(self
.credentials
, self
._config
["subscription_id"])
150 self
.conn_compute
= ComputeManagementClient(self
.credentials
, self
._config
["subscription_id"])
151 self
.conn_vnet
= NetworkManagementClient(self
.credentials
, self
._config
["subscription_id"])
152 self
._check
_or
_create
_resource
_group
()
153 self
._check
_or
_create
_vnet
()
155 # Set to client created
156 self
.reload_client
= False
157 except Exception as e
:
158 self
._format
_vimconn
_exception
(e
)
160 def _get_resource_name_from_resource_id(self
, resource_id
):
162 Obtains resource_name from the azure complete identifier: resource_name will always be last item
165 resource
= str(resource_id
.split('/')[-1])
167 except Exception as e
:
168 raise vimconn
.vimconnException("Unable to get resource name from resource_id '{}' Error: '{}'".
169 format(resource_id
, e
))
171 def _get_location_from_resource_group(self
, resource_group_name
):
173 location
= self
.conn
.resource_groups
.get(resource_group_name
).location
175 except Exception as e
:
176 raise vimconn
.vimconnNotFoundException("Location '{}' not found".format(resource_group_name
))
178 def _get_resource_group_name_from_resource_id(self
, resource_id
):
181 rg
= str(resource_id
.split('/')[4])
183 except Exception as e
:
184 raise vimconn
.vimconnException("Unable to get resource group from invalid resource_id format '{}'".
187 def _get_net_name_from_resource_id(self
, resource_id
):
190 net_name
= str(resource_id
.split('/')[8])
192 except Exception as e
:
193 raise vimconn
.vimconnException("Unable to get azure net_name from invalid resource_id format '{}'".
196 def _check_subnets_for_vm(self
, net_list
):
197 # All subnets must belong to the same resource group and vnet
198 rg_vnet
= set(self
._get
_resource
_group
_name
_from
_resource
_id
(net
['net_id']) +
199 self
._get
_net
_name
_from
_resource
_id
(net
['net_id']) for net
in net_list
)
201 if len(rg_vnet
) != 1:
202 raise self
._format
_vimconn
_exception
('Azure VMs can only attach to subnets in same VNET')
204 def _format_vimconn_exception(self
, e
):
206 Transforms a generic or azure exception to a vimcommException
208 if isinstance(e
, vimconn
.vimconnException
):
210 elif isinstance(e
, AuthenticationError
):
211 raise vimconn
.vimconnAuthException(type(e
).__name
__ + ': ' + str(e
))
212 elif isinstance(e
, ConnectionError
):
213 raise vimconn
.vimconnConnectionException(type(e
).__name
__ + ': ' + str(e
))
215 # In case of generic error recreate client
216 self
.reload_client
= True
217 raise vimconn
.vimconnException(type(e
).__name
__ + ': ' + str(e
))
219 def _check_or_create_resource_group(self
):
221 Creates the base resource group if it does not exist
224 rg_exists
= self
.conn
.resource_groups
.check_existence(self
.resource_group
)
226 self
.logger
.debug("create base rgroup: %s", self
.resource_group
)
227 self
.conn
.resource_groups
.create_or_update(self
.resource_group
, {'location': self
.region
})
228 except Exception as e
:
229 self
._format
_vimconn
_exception
(e
)
231 def _check_or_create_vnet(self
):
233 Try to get existent base vnet, in case it does not exist it creates it
236 vnet
= self
.conn_vnet
.virtual_networks
.get(self
.resource_group
, self
.vnet_name
)
237 self
.vnet_address_space
= vnet
.address_space
.address_prefixes
[0]
238 self
.vnet_id
= vnet
.id
240 except CloudError
as e
:
241 if e
.error
.error
and "notfound" in e
.error
.error
.lower():
243 # continue and create it
245 self
._format
_vimconn
_exception
(e
)
247 # if it does not exist, create it
250 'location': self
.region
,
252 'address_prefixes': ["10.0.0.0/8"]
255 self
.vnet_address_space
= "10.0.0.0/8"
257 self
.logger
.debug("create base vnet: %s", self
.vnet_name
)
258 self
.conn_vnet
.virtual_networks
.create_or_update(self
.resource_group
, self
.vnet_name
, vnet_params
)
259 vnet
= self
.conn_vnet
.virtual_networks
.get(self
.resource_group
, self
.vnet_name
)
260 self
.vnet_id
= vnet
.id
261 except Exception as e
:
262 self
._format
_vimconn
_exception
(e
)
264 def new_network(self
, net_name
, net_type
, ip_profile
=None, shared
=False, vlan
=None):
266 Adds a tenant network to VIM
267 :param net_name: name of the network
268 :param net_type: not used for azure networks
269 :param ip_profile: is a dict containing the IP parameters of the network (Currently only IPv4 is implemented)
270 'ip-version': can be one of ['IPv4','IPv6']
271 'subnet-address': ip_prefix_schema, that is X.X.X.X/Y
272 'gateway-address': (Optional) ip_schema, that is X.X.X.X, not implemented for azure connector
273 'dns-address': (Optional) ip_schema, not implemented for azure connector
274 'dhcp': (Optional) dict containing, not implemented for azure connector
275 'enabled': {'type': 'boolean'},
276 'start-address': ip_schema, first IP to grant
277 'count': number of IPs to grant.
278 :param shared: Not allowed for Azure Connector
279 :param vlan: VLAN tagging is not allowed for Azure
280 :return: a tuple with the network identifier and created_items, or raises an exception on error
281 created_items can be None or a dictionary where this method can include key-values that will be passed to
282 the method delete_network. Can be used to store created segments, created l2gw connections, etc.
283 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
286 return self
._new
_subnet
(net_name
, ip_profile
)
288 def _new_subnet(self
, net_name
, ip_profile
):
290 Adds a tenant network to VIM. It creates a new subnet at existing base vnet
291 :param net_name: subnet name
293 subnet-address: if it is not provided a subnet/24 in the default vnet is created,
294 otherwise it creates a subnet in the indicated address
295 :return: a tuple with the network identifier and created_items, or raises an exception on error
297 self
.logger
.debug('create subnet name %s, ip_profile %s', net_name
, ip_profile
)
298 self
._reload
_connection
()
300 if ip_profile
is None:
301 # get a non used vnet ip range /24 and allocate automatically inside the range self.vnet_address_space
302 used_subnets
= self
.get_network_list()
303 for ip_range
in netaddr
.IPNetwork(self
.vnet_address_space
).subnet(24):
304 for used_subnet
in used_subnets
:
305 subnet_range
= netaddr
.IPNetwork(used_subnet
["cidr_block"])
306 if subnet_range
in ip_range
or ip_range
in subnet_range
:
307 # this range overlaps with an existing subnet ip range. Breaks and look for another
310 ip_profile
= {"subnet_address": str(ip_range
)}
311 self
.logger
.debug('dinamically obtained ip_profile: %s', ip_range
)
314 raise vimconn
.vimconnException("Cannot find a non-used subnet range in {}".
315 format(self
.vnet_address_space
))
317 ip_profile
= {"subnet_address": ip_profile
['subnet_address']}
320 # subnet_name = "{}-{}".format(net_name[:24], uuid4())
322 'address_prefix': ip_profile
['subnet_address']
324 # Assign a not duplicated net name
325 subnet_name
= self
._get
_unused
_subnet
_name
(net_name
)
327 self
.logger
.debug('creating subnet_name: {}'.format(subnet_name
))
328 async_creation
= self
.conn_vnet
.subnets
.create_or_update(self
.resource_group
, self
.vnet_name
,
329 subnet_name
, subnet_params
)
330 async_creation
.wait()
331 self
.logger
.debug('created subnet_name: {}'.format(subnet_name
))
333 return "{}/subnets/{}".format(self
.vnet_id
, subnet_name
), None
334 except Exception as e
:
335 self
._format
_vimconn
_exception
(e
)
337 def _get_unused_subnet_name(self
, subnet_name
):
339 Adds a prefix to the subnet_name with a number in case the indicated name is repeated
340 Checks subnets with the indicated name (without suffix) and adds a suffix with a number
342 all_subnets
= self
.conn_vnet
.subnets
.list(self
.resource_group
, self
.vnet_name
)
343 # Filter to subnets starting with the indicated name
344 subnets
= list(filter(lambda subnet
: (subnet
.name
.startswith(subnet_name
)), all_subnets
))
345 net_names
= [str(subnet
.name
) for subnet
in subnets
]
347 # get the name with the first not used suffix
349 # name = subnet_name + "-" + str(name_suffix)
350 name
= subnet_name
# first subnet created will have no prefix
351 while name
in net_names
:
353 name
= subnet_name
+ "-" + str(name_suffix
)
356 def _create_nic(self
, net
, nic_name
, static_ip
=None):
358 self
.logger
.debug('create nic name %s, net_name %s', nic_name
, net
)
359 self
._reload
_connection
()
361 subnet_id
= net
['net_id']
362 location
= self
._get
_location
_from
_resource
_group
(self
.resource_group
)
364 net_ifz
= {'location': location
}
365 net_ip_config
= {'name': nic_name
+ '-ipconfiguration', 'subnet': {'id': subnet_id
}}
367 net_ip_config
['privateIPAddress'] = static_ip
368 net_ip_config
['privateIPAllocationMethod'] = 'Static'
369 net_ifz
['ip_configurations'] = [net_ip_config
]
370 mac_address
= net
.get('mac_address')
372 net_ifz
['mac_address'] = mac_address
374 async_nic_creation
= self
.conn_vnet
.network_interfaces
.create_or_update(self
.resource_group
, nic_name
,
376 async_nic_creation
.wait()
377 self
.logger
.debug('created nic name %s', nic_name
)
379 public_ip
= net
.get('floating_ip')
381 public_ip_address_params
= {
382 'location': location
,
383 'public_ip_allocation_method': 'Dynamic'
385 public_ip_name
= nic_name
+ '-public-ip'
386 public_ip
= self
.conn_vnet
.public_ip_addresses
.create_or_update(
389 public_ip_address_params
391 self
.logger
.debug('created public IP: {}'.format(public_ip
.result()))
393 # Associate NIC to Public IP
394 nic_data
= self
.conn_vnet
.network_interfaces
.get(
398 nic_data
.ip_configurations
[0].public_ip_address
= public_ip
.result()
400 self
.conn_vnet
.network_interfaces
.create_or_update(
405 except Exception as e
:
406 self
._format
_vimconn
_exception
(e
)
408 return async_nic_creation
.result()
410 def new_flavor(self
, flavor_data
):
412 It is not allowed to create new flavors in Azure, must always use an existing one
414 raise vimconn
.vimconnAuthException("It is not possible to create new flavors in AZURE")
416 def new_tenant(self
, tenant_name
, tenant_description
):
418 It is not allowed to create new tenants in azure
420 raise vimconn
.vimconnAuthException("It is not possible to create a TENANT in AZURE")
422 def new_image(self
, image_dict
):
424 It is not allowed to create new images in Azure, must always use an existing one
426 raise vimconn
.vimconnAuthException("It is not possible to create new images in AZURE")
428 def get_image_id_from_path(self
, path
):
429 """Get the image id from image path in the VIM database.
430 Returns the image_id or raises a vimconnNotFoundException
432 raise vimconn
.vimconnAuthException("It is not possible to obtain image from path in AZURE")
434 def get_image_list(self
, filter_dict
={}):
435 """Obtain tenant images from VIM
437 name: image name with the format: publisher:offer:sku:version
438 If some part of the name is provide ex: publisher:offer it will search all availables skus and version
439 for the provided publisher and offer
440 id: image uuid, currently not supported for azure
441 Returns the image list of dictionaries:
442 [{<the fields at Filter_dict plus some VIM specific>}, ...]
446 self
.logger
.debug("get_image_list filter {}".format(filter_dict
))
448 self
._reload
_connection
()
451 if filter_dict
.get("name"):
452 # name will have the format 'publisher:offer:sku:version'
453 # publisher is required, offer sku and version will be searched if not provided
454 params
= filter_dict
["name"].split(":")
455 publisher
= params
[0]
458 offer_list
= self
._get
_offer
_list
(params
, publisher
)
459 for offer
in offer_list
:
461 sku_list
= self
._get
_sku
_list
(params
, publisher
, offer
)
463 # if version is defined get directly version, else list images
464 if len(params
) == 4 and params
[3]:
466 image_list
= self
._get
_version
_image
_list
(publisher
, offer
, sku
, version
)
468 image_list
= self
._get
_sku
_image
_list
(publisher
, offer
, sku
)
470 raise vimconn
.vimconnAuthException(
471 "List images in Azure must include name param with at least publisher")
473 raise vimconn
.vimconnAuthException("List images in Azure must include name param with at"
477 except Exception as e
:
478 self
._format
_vimconn
_exception
(e
)
480 def _get_offer_list(self
, params
, publisher
):
482 Helper method to obtain offer list for defined publisher
484 if len(params
) >= 2 and params
[1]:
488 # get list of offers from azure
489 result_offers
= self
.conn_compute
.virtual_machine_images
.list_offers(self
.region
, publisher
)
490 return [offer
.name
for offer
in result_offers
]
491 except CloudError
as e
:
492 # azure raises CloudError when not found
493 self
.logger
.info("error listing offers for publisher {}, Error: {}".format(publisher
, e
))
496 def _get_sku_list(self
, params
, publisher
, offer
):
498 Helper method to obtain sku list for defined publisher and offer
500 if len(params
) >= 3 and params
[2]:
504 # get list of skus from azure
505 result_skus
= self
.conn_compute
.virtual_machine_images
.list_skus(self
.region
, publisher
, offer
)
506 return [sku
.name
for sku
in result_skus
]
507 except CloudError
as e
:
508 # azure raises CloudError when not found
509 self
.logger
.info("error listing skus for publisher {}, offer {}, Error: {}".format(publisher
, offer
, e
))
512 def _get_sku_image_list(self
, publisher
, offer
, sku
):
514 Helper method to obtain image list for publisher, offer and sku
518 result_images
= self
.conn_compute
.virtual_machine_images
.list(self
.region
, publisher
, offer
, sku
)
519 for result_image
in result_images
:
521 'id': str(result_image
.id),
522 'name': ":".join([publisher
, offer
, sku
, result_image
.name
])
524 except CloudError
as e
:
526 "error listing skus for publisher {}, offer {}, Error: {}".format(publisher
, offer
, e
))
530 def _get_version_image_list(self
, publisher
, offer
, sku
, version
):
533 result_image
= self
.conn_compute
.virtual_machine_images
.get(self
.region
, publisher
, offer
, sku
, version
)
536 'id': str(result_image
.id),
537 'name': ":".join([publisher
, offer
, sku
, version
])
539 except CloudError
as e
:
540 # azure gives CloudError when not found
541 self
.logger
.info("error listing images for publisher {}, offer {}, sku {}, version {} Error: {}".
542 format(publisher
, offer
, sku
, version
, e
))
546 def get_network_list(self
, filter_dict
={}):
547 """Obtain tenant networks of VIM
551 shared: boolean, not implemented in Azure
552 tenant_id: tenant, not used in Azure, all networks same tenants
553 admin_state_up: boolean, not implemented in Azure
554 status: 'ACTIVE', not implemented in Azure #
555 Returns the network list of dictionaries
557 # self.logger.debug('getting network list for vim, filter %s', filter_dict)
559 self
._reload
_connection
()
561 vnet
= self
.conn_vnet
.virtual_networks
.get(self
.resource_group
, self
.vnet_name
)
564 for subnet
in vnet
.subnets
:
566 if filter_dict
.get("id") and str(subnet
.id) != filter_dict
["id"]:
568 if filter_dict
.get("name") and \
569 str(subnet
.name
) != filter_dict
["name"]:
572 name
= self
._get
_resource
_name
_from
_resource
_id
(subnet
.id)
575 'id': str(subnet
.id),
577 'status': self
.provision_state2osm
[subnet
.provisioning_state
],
578 'cidr_block': str(subnet
.address_prefix
),
584 except Exception as e
:
585 self
._format
_vimconn
_exception
(e
)
587 def new_vminstance(self
, name
, description
, start
, image_id
, flavor_id
, net_list
, cloud_config
=None,
588 disk_list
=None, availability_zone_index
=None, availability_zone_list
=None):
590 self
.logger
.debug("new vm instance name: %s, image_id: %s, flavor_id: %s, net_list: %s, cloud_config: %s, "
591 "disk_list: %s, availability_zone_index: %s, availability_zone_list: %s",
592 name
, image_id
, flavor_id
, net_list
, cloud_config
, disk_list
,
593 availability_zone_index
, availability_zone_list
)
595 self
._reload
_connection
()
597 # Validate input data is valid
598 # The virtual machine name must have less or 64 characters and it can not have the following
599 # characters: (~ ! @ # $ % ^ & * ( ) = + _ [ ] { } \ | ; : ' " , < > / ?.)
600 vm_name
= self
._check
_vm
_name
(name
)
601 # Obtain vm unused name
602 vm_name
= self
._get
_unused
_vm
_name
(vm_name
)
604 # At least one network must be provided
606 raise vimconn
.vimconnException("At least one net must be provided to create a new VM")
608 # image_id are several fields of the image_id
609 image_reference
= self
._get
_image
_reference
(image_id
)
611 self
._check
_subnets
_for
_vm
(net_list
)
613 for idx
, net
in enumerate(net_list
):
614 # Fault with subnet_id
615 # subnet_id=net['subnet_id']
616 # subnet_id=net['net_id']
617 nic_name
= vm_name
+ '-nic-'+str(idx
)
618 vm_nic
= self
._create
_nic
(net
, nic_name
, net
.get('ip_address'))
619 vm_nics
.append({'id': str(vm_nic
.id)})
620 net
['vim_id'] = vm_nic
.id
624 # cloud-init configuration
627 config_drive
, userdata
= self
._create
_user
_data
(cloud_config
)
628 custom_data
= base64
.b64encode(userdata
.encode('utf-8')).decode('latin-1')
630 key_pairs
= cloud_config
.get("key-pairs")
632 key_data
= key_pairs
[0]
634 if cloud_config
.get("users"):
635 user_name
= cloud_config
.get("users")[0].get("name", "osm")
637 user_name
= "osm" # DEFAULT USER IS OSM
640 'computer_name': vm_name
,
641 'admin_username': user_name
,
642 'linux_configuration': {
643 "disable_password_authentication": True,
646 "path": "/home/{}/.ssh/authorized_keys".format(user_name
),
651 'custom_data': custom_data
655 'computer_name': vm_name
,
656 'admin_username': 'osm',
657 'admin_password': 'Osm4u!',
661 'location': self
.region
,
662 'os_profile': os_profile
,
663 'hardware_profile': {
667 'image_reference': image_reference
671 # Add data disks if they are provided
674 for lun_name
, disk
in enumerate(disk_list
):
675 self
.logger
.debug("add disk size: %s, image: %s", disk
.get("size"), disk
.get("image_id"))
676 if not disk
.get("image_id"):
678 'lun': lun_name
, # You choose the value, depending of what is available for you
679 'name': vm_name
+ "_data_disk-" + str(lun_name
),
680 'create_option': DiskCreateOption
.empty
,
681 'disk_size_gb': disk
.get("size")
684 # self.logger.debug("currently not able to create data disks from image for azure, ignoring")
686 'lun': lun_name
, # You choose the value, depending of what is available for you
687 'name': vm_name
+ "_data_disk-" + str(lun_name
),
688 'create_option': 'Attach',
689 'disk_size_gb': disk
.get("size"),
691 'id': disk
.get("image_id")
696 vm_parameters
["storage_profile"]["data_disks"] = data_disks
698 # If the machine has several networks one must be marked as primary
699 # As it is not indicated in the interface the first interface will be marked as primary
701 for idx
, vm_nic
in enumerate(vm_nics
):
703 vm_nics
[0]['Primary'] = True
705 vm_nics
[idx
]['Primary'] = False
707 vm_parameters
['network_profile'] = {'network_interfaces': vm_nics
}
709 self
.logger
.debug("create vm name: %s", vm_name
)
710 creation_result
= self
.conn_compute
.virtual_machines
.create_or_update(
715 # creation_result.wait()
716 result
= creation_result
.result()
717 self
.logger
.debug("created vm name: %s", vm_name
)
720 self
.conn_compute
.virtual_machines
.start(
723 # start_result.wait()
725 return result
.id, None
727 # run_command_parameters = {
728 # 'command_id': 'RunShellScript', # For linux, don't change it
730 # 'date > /tmp/test.txt'
733 except Exception as e
:
734 self
.logger
.debug('Exception creating new vminstance: %s', e
, exc_info
=True)
735 self
._format
_vimconn
_exception
(e
)
737 def _get_unused_vm_name(self
, vm_name
):
739 Checks the vm name and in case it is used adds a suffix to the name to allow creation
742 all_vms
= self
.conn_compute
.virtual_machines
.list(self
.resource_group
)
743 # Filter to vms starting with the indicated name
744 vms
= list(filter(lambda vm
: (vm
.name
.startswith(vm_name
)), all_vms
))
745 vm_names
= [str(vm
.name
) for vm
in vms
]
747 # get the name with the first not used suffix
749 # name = subnet_name + "-" + str(name_suffix)
750 name
= vm_name
# first subnet created will have no prefix
751 while name
in vm_names
:
753 name
= vm_name
+ "-" + str(name_suffix
)
756 # It is necesary extract from image_id data to create the VM with this format
757 # 'image_reference': {
758 # 'publisher': vm_reference['publisher'],
759 # 'offer': vm_reference['offer'],
760 # 'sku': vm_reference['sku'],
761 # 'version': vm_reference['version']
763 def _get_image_reference(self
, image_id
):
766 # The data input format example:
767 # /Subscriptions/ca3d18ab-d373-4afb-a5d6-7c44f098d16a/Providers/Microsoft.Compute/Locations/westeurope/
768 # Publishers/Canonical/ArtifactTypes/VMImage/
769 # Offers/UbuntuServer/
771 # Versions/18.04.201809110
772 publisher
= str(image_id
.split('/')[8])
773 offer
= str(image_id
.split('/')[12])
774 sku
= str(image_id
.split('/')[14])
775 version
= str(image_id
.split('/')[16])
778 'publisher': publisher
,
783 except Exception as e
:
784 raise vimconn
.vimconnException(
785 "Unable to get image_reference from invalid image_id format: '{}'".format(image_id
))
787 # Azure VM names can not have some special characters
788 def _check_vm_name(self
, vm_name
):
790 Checks vm name, in case the vm has not allowed characters they are removed, not error raised
793 chars_not_allowed_list
= "~!@#$%^&*()=+_[]{}|;:<>/?."
795 # First: the VM name max length is 64 characters
796 vm_name_aux
= vm_name
[:64]
798 # Second: replace not allowed characters
799 for elem
in chars_not_allowed_list
:
800 # Check if string is in the main string
801 if elem
in vm_name_aux
:
802 # self.logger.debug('Dentro del IF')
804 vm_name_aux
= vm_name_aux
.replace(elem
, '-')
808 def get_flavor_id_from_data(self
, flavor_dict
):
810 self
.logger
.debug("getting flavor id from data, flavor_dict: %s", flavor_dict
)
811 filter_dict
= flavor_dict
or {}
813 self
._reload
_connection
()
814 vm_sizes_list
= [vm_size
.serialize() for vm_size
in
815 self
.conn_compute
.virtual_machine_sizes
.list(self
.region
)]
817 cpus
= filter_dict
.get('vcpus') or 0
818 memMB
= filter_dict
.get('ram') or 0
821 if self
._config
.get("flavors_pattern"):
822 filtered_sizes
= [size
for size
in vm_sizes_list
if size
['numberOfCores'] >= cpus
and
823 size
['memoryInMB'] >= memMB
and
824 re
.search(self
._config
.get("flavors_pattern"), size
["name"])]
826 filtered_sizes
= [size
for size
in vm_sizes_list
if size
['numberOfCores'] >= cpus
and
827 size
['memoryInMB'] >= memMB
]
830 listedFilteredSizes
= sorted(filtered_sizes
, key
=lambda k
: (k
['numberOfCores'], k
['memoryInMB'],
831 k
['resourceDiskSizeInMB']))
833 if listedFilteredSizes
:
834 return listedFilteredSizes
[0]['name']
835 raise vimconn
.vimconnNotFoundException("Cannot find any flavor matching '{}'".format(str(flavor_dict
)))
837 except Exception as e
:
838 self
._format
_vimconn
_exception
(e
)
840 def _get_flavor_id_from_flavor_name(self
, flavor_name
):
842 # self.logger.debug("getting flavor id from flavor name {}".format(flavor_name))
844 self
._reload
_connection
()
845 vm_sizes_list
= [vm_size
.serialize() for vm_size
in
846 self
.conn_compute
.virtual_machine_sizes
.list(self
.region
)]
849 for size
in vm_sizes_list
:
850 if size
['name'] == flavor_name
:
853 # None is returned if not found anything
856 except Exception as e
:
857 self
._format
_vimconn
_exception
(e
)
859 def check_vim_connectivity(self
):
861 self
._reload
_connection
()
863 except Exception as e
:
864 raise vimconn
.vimconnException("Connectivity issue with Azure API: {}".format(e
))
866 def get_network(self
, net_id
):
868 # self.logger.debug('get network id: {}'.format(net_id))
869 # res_name = self._get_resource_name_from_resource_id(net_id)
870 self
._reload
_connection
()
872 filter_dict
= {'name': net_id
}
873 network_list
= self
.get_network_list(filter_dict
)
876 raise vimconn
.vimconnNotFoundException("network '{}' not found".format(net_id
))
878 return network_list
[0]
880 def delete_network(self
, net_id
, created_items
=None):
882 self
.logger
.debug('deleting network {} - {}'.format(self
.resource_group
, net_id
))
884 self
._reload
_connection
()
885 res_name
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
886 filter_dict
= {'name': res_name
}
887 network_list
= self
.get_network_list(filter_dict
)
889 raise vimconn
.vimconnNotFoundException("network '{}' not found".format(net_id
))
892 # Subnet API fails (CloudError: Azure Error: ResourceNotFound)
893 # Put the initial virtual_network API
894 async_delete
= self
.conn_vnet
.subnets
.delete(self
.resource_group
, self
.vnet_name
, res_name
)
898 except CloudError
as e
:
899 if e
.error
.error
and "notfound" in e
.error
.error
.lower():
900 raise vimconn
.vimconnNotFoundException("network '{}' not found".format(net_id
))
902 self
._format
_vimconn
_exception
(e
)
903 except Exception as e
:
904 self
._format
_vimconn
_exception
(e
)
906 def delete_vminstance(self
, vm_id
, created_items
=None):
907 """ Deletes a vm instance from the vim.
909 self
.logger
.debug('deleting VM instance {} - {}'.format(self
.resource_group
, vm_id
))
910 self
._reload
_connection
()
914 res_name
= self
._get
_resource
_name
_from
_resource
_id
(vm_id
)
915 vm
= self
.conn_compute
.virtual_machines
.get(self
.resource_group
, res_name
)
917 # Shuts down the virtual machine and releases the compute resources
918 # vm_stop = self.conn_compute.virtual_machines.power_off(self.resource_group, resName)
921 vm_delete
= self
.conn_compute
.virtual_machines
.delete(self
.resource_group
, res_name
)
923 self
.logger
.debug('deleted VM name: %s', res_name
)
926 os_disk_name
= vm
.storage_profile
.os_disk
.name
927 self
.logger
.debug('delete OS DISK: %s', os_disk_name
)
928 self
.conn_compute
.disks
.delete(self
.resource_group
, os_disk_name
)
929 self
.logger
.debug('deleted OS DISK name: %s', os_disk_name
)
931 for data_disk
in vm
.storage_profile
.data_disks
:
932 self
.logger
.debug('delete data_disk: %s', data_disk
.name
)
933 self
.conn_compute
.disks
.delete(self
.resource_group
, data_disk
.name
)
934 self
.logger
.debug('deleted OS DISK name: %s', data_disk
.name
)
936 # After deleting VM, it is necessary to delete NIC, because if is not deleted delete_network
937 # does not work because Azure says that is in use the subnet
938 network_interfaces
= vm
.network_profile
.network_interfaces
940 for network_interface
in network_interfaces
:
942 nic_name
= self
._get
_resource
_name
_from
_resource
_id
(network_interface
.id)
943 nic_data
= self
.conn_vnet
.network_interfaces
.get(
947 public_ip_name
= None
948 exist_public_ip
= nic_data
.ip_configurations
[0].public_ip_address
950 public_ip_id
= nic_data
.ip_configurations
[0].public_ip_address
.id
953 public_ip_name
= self
._get
_resource
_name
_from
_resource
_id
(public_ip_id
)
955 # Public ip must be deleted afterwards of nic that is attached
957 self
.logger
.debug('delete NIC name: %s', nic_name
)
958 nic_delete
= self
.conn_vnet
.network_interfaces
.delete(self
.resource_group
, nic_name
)
960 self
.logger
.debug('deleted NIC name: %s', nic_name
)
962 # Delete list of public ips
964 self
.logger
.debug('delete PUBLIC IP - ' + public_ip_name
)
965 self
.conn_vnet
.public_ip_addresses
.delete(self
.resource_group
, public_ip_name
)
967 except CloudError
as e
:
968 if e
.error
.error
and "notfound" in e
.error
.error
.lower():
969 raise vimconn
.vimconnNotFoundException("No vm instance found '{}'".format(vm_id
))
971 self
._format
_vimconn
_exception
(e
)
972 except Exception as e
:
973 self
._format
_vimconn
_exception
(e
)
975 def action_vminstance(self
, vm_id
, action_dict
, created_items
={}):
976 """Send and action over a VM instance from VIM
977 Returns the vm_id if the action was successfully sent to the VIM
980 self
.logger
.debug("Action over VM '%s': %s", vm_id
, str(action_dict
))
982 self
._reload
_connection
()
983 resName
= self
._get
_resource
_name
_from
_resource
_id
(vm_id
)
984 if "start" in action_dict
:
985 self
.conn_compute
.virtual_machines
.start(self
.resource_group
, resName
)
986 elif "stop" in action_dict
or "shutdown" in action_dict
or "shutoff" in action_dict
:
987 self
.conn_compute
.virtual_machines
.power_off(self
.resource_group
, resName
)
988 elif "terminate" in action_dict
:
989 self
.conn_compute
.virtual_machines
.delete(self
.resource_group
, resName
)
990 elif "reboot" in action_dict
:
991 self
.conn_compute
.virtual_machines
.restart(self
.resource_group
, resName
)
993 except CloudError
as e
:
994 if e
.error
.error
and "notfound" in e
.error
.error
.lower():
995 raise vimconn
.vimconnNotFoundException("No vm found '{}'".format(vm_id
))
997 self
._format
_vimconn
_exception
(e
)
998 except Exception as e
:
999 self
._format
_vimconn
_exception
(e
)
1001 def delete_flavor(self
, flavor_id
):
1002 raise vimconn
.vimconnAuthException("It is not possible to delete a FLAVOR in AZURE")
1004 def delete_tenant(self
, tenant_id
,):
1005 raise vimconn
.vimconnAuthException("It is not possible to delete a TENANT in AZURE")
1007 def delete_image(self
, image_id
):
1008 raise vimconn
.vimconnAuthException("It is not possible to delete a IMAGE in AZURE")
1010 def get_vminstance(self
, vm_id
):
1012 Obtaing the vm instance data from v_id
1014 self
.logger
.debug("get vm instance: %s", vm_id
)
1015 self
._reload
_connection
()
1017 resName
= self
._get
_resource
_name
_from
_resource
_id
(vm_id
)
1018 vm
= self
.conn_compute
.virtual_machines
.get(self
.resource_group
, resName
)
1019 except CloudError
as e
:
1020 if e
.error
.error
and "notfound" in e
.error
.error
.lower():
1021 raise vimconn
.vimconnNotFoundException("No vminstance found '{}'".format(vm_id
))
1023 self
._format
_vimconn
_exception
(e
)
1024 except Exception as e
:
1025 self
._format
_vimconn
_exception
(e
)
1029 def get_flavor(self
, flavor_id
):
1031 Obtains the flavor_data from the flavor_id
1033 self
._reload
_connection
()
1034 self
.logger
.debug("get flavor from id: %s", flavor_id
)
1035 flavor_data
= self
._get
_flavor
_id
_from
_flavor
_name
(flavor_id
)
1040 'ram': flavor_data
['memoryInMB'],
1041 'vcpus': flavor_data
['numberOfCores'],
1042 'disk': flavor_data
['resourceDiskSizeInMB']/1024
1046 raise vimconn
.vimconnNotFoundException("flavor '{}' not found".format(flavor_id
))
1048 def get_tenant_list(self
, filter_dict
={}):
1049 """ Obtains the list of tenants
1050 For the azure connector only the azure tenant will be returned if it is compatible
1053 tenants_azure
= [{'name': self
.tenant
, 'id': self
.tenant
}]
1056 self
.logger
.debug("get tenant list: %s", filter_dict
)
1057 for tenant_azure
in tenants_azure
:
1059 if filter_dict
.get("id") and str(tenant_azure
.get("id")) != filter_dict
["id"]:
1061 if filter_dict
.get("name") and str(tenant_azure
.get("name")) != filter_dict
["name"]:
1064 tenant_list
.append(tenant_azure
)
1068 def refresh_nets_status(self
, net_list
):
1069 """Get the status of the networks
1070 Params: the list of network identifiers
1071 Returns a dictionary with:
1072 net_id: #VIM id of this network
1073 status: #Mandatory. Text with one of:
1074 # DELETED (not found at vim)
1075 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1076 # OTHER (Vim reported other status not understood)
1077 # ERROR (VIM indicates an ERROR status)
1078 # ACTIVE, INACTIVE, DOWN (admin down),
1079 # BUILD (on building process)
1081 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1082 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1087 self
._reload
_connection
()
1089 self
.logger
.debug("reload nets status net_list: %s", net_list
)
1090 for net_id
in net_list
:
1092 netName
= self
._get
_net
_name
_from
_resource
_id
(net_id
)
1093 resName
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
1095 net
= self
.conn_vnet
.subnets
.get(self
.resource_group
, netName
, resName
)
1097 out_nets
[net_id
] = {
1098 "status": self
.provision_state2osm
[net
.provisioning_state
],
1099 "vim_info": str(net
)
1101 except CloudError
as e
:
1102 if e
.error
.error
and "notfound" in e
.error
.error
.lower():
1103 self
.logger
.info("Not found subnet net_name: %s, subnet_name: %s", netName
, resName
)
1104 out_nets
[net_id
] = {
1105 "status": "DELETED",
1109 self
.logger
.error("CloudError Exception %s when searching subnet", e
)
1110 out_nets
[net_id
] = {
1111 "status": "VIM_ERROR",
1114 except vimconn
.vimconnNotFoundException
as e
:
1115 self
.logger
.error("VimConnNotFoundException %s when searching subnet", e
)
1116 out_nets
[net_id
] = {
1117 "status": "DELETED",
1120 except Exception as e
:
1121 self
.logger
.error("Exception %s when searching subnet", e
, exc_info
=True)
1122 out_nets
[net_id
] = {
1123 "status": "VIM_ERROR",
1128 def refresh_vms_status(self
, vm_list
):
1129 """ Get the status of the virtual machines and their interfaces/ports
1130 Params: the list of VM identifiers
1131 Returns a dictionary with:
1132 vm_id: # VIM id of this Virtual Machine
1133 status: # Mandatory. Text with one of:
1134 # DELETED (not found at vim)
1135 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1136 # OTHER (Vim reported other status not understood)
1137 # ERROR (VIM indicates an ERROR status)
1138 # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
1139 # BUILD (on building process), ERROR
1140 # ACTIVE:NoMgmtIP (Active but none of its interfaces has an IP address
1141 # (ACTIVE:NoMgmtIP is not returned for Azure)
1143 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1144 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1145 interfaces: list with interface info. Each item a dictionary with:
1146 vim_interface_id - The ID of the interface
1147 mac_address - The MAC address of the interface.
1148 ip_address - The IP address of the interface within the subnet.
1152 self
._reload
_connection
()
1154 self
.logger
.debug("refresh vm status vm_list: %s", vm_list
)
1155 search_vm_list
= vm_list
or {}
1157 for vm_id
in search_vm_list
:
1160 res_name
= self
._get
_resource
_name
_from
_resource
_id
(vm_id
)
1162 vm
= self
.conn_compute
.virtual_machines
.get(self
.resource_group
, res_name
)
1163 out_vm
['vim_info'] = str(vm
)
1164 out_vm
['status'] = self
.provision_state2osm
.get(vm
.provisioning_state
, 'OTHER')
1165 if vm
.provisioning_state
== 'Succeeded':
1166 # check if machine is running or stopped
1167 instance_view
= self
.conn_compute
.virtual_machines
.instance_view(self
.resource_group
,
1169 for status
in instance_view
.statuses
:
1170 splitted_status
= status
.code
.split("/")
1171 if len(splitted_status
) == 2 and splitted_status
[0] == 'PowerState':
1172 out_vm
['status'] = self
.power_state2osm
.get(splitted_status
[1], 'OTHER')
1174 network_interfaces
= vm
.network_profile
.network_interfaces
1175 out_vm
['interfaces'] = self
._get
_vm
_interfaces
_status
(vm_id
, network_interfaces
)
1177 except CloudError
as e
:
1178 if e
.error
.error
and "notfound" in e
.error
.error
.lower():
1179 self
.logger
.debug("Not found vm id: %s", vm_id
)
1180 out_vm
['status'] = "DELETED"
1181 out_vm
['error_msg'] = str(e
)
1182 out_vm
['vim_info'] = None
1184 # maybe connection error or another type of error, return vim error
1185 self
.logger
.error("Exception %s refreshing vm_status", e
)
1186 out_vm
['status'] = "VIM_ERROR"
1187 out_vm
['error_msg'] = str(e
)
1188 out_vm
['vim_info'] = None
1189 except Exception as e
:
1190 self
.logger
.error("Exception %s refreshing vm_status", e
, exc_info
=True)
1191 out_vm
['status'] = "VIM_ERROR"
1192 out_vm
['error_msg'] = str(e
)
1193 out_vm
['vim_info'] = None
1195 out_vms
[vm_id
] = out_vm
1199 def _get_vm_interfaces_status(self
, vm_id
, interfaces
):
1201 Gets the interfaces detail for a vm
1202 :param interfaces: List of interfaces.
1203 :return: Dictionary with list of interfaces including, vim_interface_id, mac_address and ip_address
1207 for network_interface
in interfaces
:
1209 nic_name
= self
._get
_resource
_name
_from
_resource
_id
(network_interface
.id)
1210 interface_dict
['vim_interface_id'] = network_interface
.id
1212 nic_data
= self
.conn_vnet
.network_interfaces
.get(
1213 self
.resource_group
,
1217 if nic_data
.ip_configurations
[0].public_ip_address
:
1218 self
.logger
.debug("Obtain public ip address")
1219 public_ip_name
= self
._get
_resource
_name
_from
_resource
_id
(
1220 nic_data
.ip_configurations
[0].public_ip_address
.id)
1221 public_ip
= self
.conn_vnet
.public_ip_addresses
.get(self
.resource_group
, public_ip_name
)
1222 self
.logger
.debug("Public ip address is: %s", public_ip
.ip_address
)
1223 ips
.append(public_ip
.ip_address
)
1225 private_ip
= nic_data
.ip_configurations
[0].private_ip_address
1226 ips
.append(private_ip
)
1228 interface_dict
['mac_address'] = nic_data
.mac_address
1229 interface_dict
['ip_address'] = ";".join(ips
)
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
])