Reformat files according to new black validation
[osm/RO.git] / RO-VIM-azure / osm_rovim_azure / vimconn_azure.py
1 # -*- coding: utf-8 -*-
2 ##
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
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
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
13 # under the License.
14 ##
15
16 import base64
17 import logging
18 from os import getenv
19 import re
20
21 from azure.core.exceptions import ResourceNotFoundError
22 from azure.identity import ClientSecretCredential
23 from azure.mgmt.compute import ComputeManagementClient
24 from azure.mgmt.network import NetworkManagementClient
25 from azure.mgmt.resource import ResourceManagementClient
26 from azure.profiles import ProfileDefinition
27 from cryptography.hazmat.backends import default_backend as crypto_default_backend
28 from cryptography.hazmat.primitives import serialization as crypto_serialization
29 from cryptography.hazmat.primitives.asymmetric import rsa
30 from msrest.exceptions import AuthenticationError
31 from msrestazure.azure_exceptions import CloudError
32 import msrestazure.tools as azure_tools
33 import netaddr
34 from osm_ro_plugin import vimconn
35 from requests.exceptions import ConnectionError
36
37 __author__ = "Isabel Lloret, Sergio Gonzalez, Alfonso Tierno, Gerardo Garcia"
38 __date__ = "$18-apr-2019 23:59:59$"
39
40
41 if getenv("OSMRO_PDB_DEBUG"):
42 import sys
43
44 print(sys.path)
45 import pdb
46
47 pdb.set_trace()
48
49
50 def find_in_list(the_list, condition_lambda):
51 for item in the_list:
52 if condition_lambda(item):
53 return item
54 else:
55 return None
56
57
58 class vimconnector(vimconn.VimConnector):
59 # Translate azure provisioning state to OSM provision state
60 # The first three ones are the transitional status once a user initiated action has been requested
61 # Once the operation is complete, it will transition into the states Succeeded or Failed
62 # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/states-lifecycle
63 provision_state2osm = {
64 "Creating": "BUILD",
65 "Updating": "BUILD",
66 "Deleting": "INACTIVE",
67 "Succeeded": "ACTIVE",
68 "Failed": "ERROR",
69 }
70
71 # Translate azure power state to OSM provision state
72 power_state2osm = {
73 "starting": "INACTIVE",
74 "running": "ACTIVE",
75 "stopping": "INACTIVE",
76 "stopped": "INACTIVE",
77 "unknown": "OTHER",
78 "deallocated": "BUILD",
79 "deallocating": "BUILD",
80 }
81
82 # TODO - review availability zones
83 AZURE_ZONES = ["1", "2", "3"]
84
85 AZURE_COMPUTE_MGMT_CLIENT_API_VERSION = "2021-03-01"
86 AZURE_COMPUTE_MGMT_PROFILE_TAG = "azure.mgmt.compute.ComputeManagementClient"
87 AZURE_COMPUTE_MGMT_PROFILE = ProfileDefinition(
88 {
89 AZURE_COMPUTE_MGMT_PROFILE_TAG: {
90 None: AZURE_COMPUTE_MGMT_CLIENT_API_VERSION,
91 "availability_sets": "2020-12-01",
92 "dedicated_host_groups": "2020-12-01",
93 "dedicated_hosts": "2020-12-01",
94 "disk_accesses": "2020-12-01",
95 "disk_encryption_sets": "2020-12-01",
96 "disk_restore_point": "2020-12-01",
97 "disks": "2020-12-01",
98 "galleries": "2020-09-30",
99 "gallery_application_versions": "2020-09-30",
100 "gallery_applications": "2020-09-30",
101 "gallery_image_versions": "2020-09-30",
102 "gallery_images": "2020-09-30",
103 "gallery_sharing_profile": "2020-09-30",
104 "images": "2020-12-01",
105 "log_analytics": "2020-12-01",
106 "operations": "2020-12-01",
107 "proximity_placement_groups": "2020-12-01",
108 "resource_skus": "2019-04-01",
109 "shared_galleries": "2020-09-30",
110 "shared_gallery_image_versions": "2020-09-30",
111 "shared_gallery_images": "2020-09-30",
112 "snapshots": "2020-12-01",
113 "ssh_public_keys": "2020-12-01",
114 "usage": "2020-12-01",
115 "virtual_machine_extension_images": "2020-12-01",
116 "virtual_machine_extensions": "2020-12-01",
117 "virtual_machine_images": "2020-12-01",
118 "virtual_machine_images_edge_zone": "2020-12-01",
119 "virtual_machine_run_commands": "2020-12-01",
120 "virtual_machine_scale_set_extensions": "2020-12-01",
121 "virtual_machine_scale_set_rolling_upgrades": "2020-12-01",
122 "virtual_machine_scale_set_vm_extensions": "2020-12-01",
123 "virtual_machine_scale_set_vm_run_commands": "2020-12-01",
124 "virtual_machine_scale_set_vms": "2020-12-01",
125 "virtual_machine_scale_sets": "2020-12-01",
126 "virtual_machine_sizes": "2020-12-01",
127 "virtual_machines": "2020-12-01",
128 }
129 },
130 AZURE_COMPUTE_MGMT_PROFILE_TAG + " osm",
131 )
132
133 AZURE_RESOURCE_MGMT_CLIENT_API_VERSION = "2020-10-01"
134 AZURE_RESOURCE_MGMT_PROFILE_TAG = (
135 "azure.mgmt.resource.resources.ResourceManagementClient"
136 )
137 AZURE_RESOURCE_MGMT_PROFILE = ProfileDefinition(
138 {
139 AZURE_RESOURCE_MGMT_PROFILE_TAG: {
140 None: AZURE_RESOURCE_MGMT_CLIENT_API_VERSION,
141 }
142 },
143 AZURE_RESOURCE_MGMT_PROFILE_TAG + " osm",
144 )
145
146 AZURE_NETWORK_MGMT_CLIENT_API_VERSION = "2020-11-01"
147 AZURE_NETWORK_MGMT_PROFILE_TAG = "azure.mgmt.network.NetworkManagementClient"
148 AZURE_NETWORK_MGMT_PROFILE = ProfileDefinition(
149 {
150 AZURE_NETWORK_MGMT_PROFILE_TAG: {
151 None: AZURE_NETWORK_MGMT_CLIENT_API_VERSION,
152 "firewall_policy_rule_groups": "2020-04-01",
153 "interface_endpoints": "2019-02-01",
154 "p2_svpn_server_configurations": "2019-07-01",
155 }
156 },
157 AZURE_NETWORK_MGMT_PROFILE_TAG + " osm",
158 )
159
160 def __init__(
161 self,
162 uuid,
163 name,
164 tenant_id,
165 tenant_name,
166 url,
167 url_admin=None,
168 user=None,
169 passwd=None,
170 log_level=None,
171 config={},
172 persistent_info={},
173 ):
174 """
175 Constructor of VIM. Raise an exception is some needed parameter is missing, but it must not do any connectivity
176 checking against the VIM
177 Using common constructor parameters.
178 In this case: config must include the following parameters:
179 subscription_id: assigned azure subscription identifier
180 region_name: current region for azure network
181 resource_group: used for all azure created resources
182 vnet_name: base vnet for azure, created networks will be subnets from this base network
183 config may also include the following parameter:
184 flavors_pattern: pattern that will be used to select a range of vm sizes, for example
185 "^((?!Standard_B).)*$" will filter out Standard_B range that is cheap but is very overused
186 "^Standard_B" will select a serie B maybe for test environment
187 """
188 vimconn.VimConnector.__init__(
189 self,
190 uuid,
191 name,
192 tenant_id,
193 tenant_name,
194 url,
195 url_admin,
196 user,
197 passwd,
198 log_level,
199 config,
200 persistent_info,
201 )
202
203 # Variable that indicates if client must be reloaded or initialized
204 self.reload_client = True
205
206 self.vnet_address_space = None
207
208 # LOGGER
209 self.logger = logging.getLogger("ro.vim.azure")
210 if log_level:
211 self.logger.setLevel(getattr(logging, log_level))
212
213 self.tenant = tenant_id or tenant_name
214
215 # Store config to create azure subscription later
216 self._config = {
217 "user": user,
218 "passwd": passwd,
219 "tenant": tenant_id or tenant_name,
220 }
221
222 # SUBSCRIPTION
223 if "subscription_id" in config:
224 self._config["subscription_id"] = config.get("subscription_id")
225 # self.logger.debug("Setting subscription to: %s", self.config["subscription_id"])
226 else:
227 raise vimconn.VimConnException("Subscription not specified")
228
229 # RESOURCE_GROUP
230 if "resource_group" in config:
231 self.resource_group = config.get("resource_group")
232 else:
233 raise vimconn.VimConnException(
234 "Azure resource_group is not specified at config"
235 )
236
237 # REGION
238 if "region_name" in config:
239 self.region = config.get("region_name")
240 else:
241 raise vimconn.VimConnException(
242 "Azure region_name is not specified at config"
243 )
244
245 # VNET_NAME
246 if "vnet_name" in config:
247 self.vnet_name = config["vnet_name"]
248
249 # VNET_RESOURCE_GROUP
250 self.vnet_resource_group = config.get("vnet_resource_group")
251
252 # TODO - not used, do anything about it?
253 # public ssh key
254 self.pub_key = config.get("pub_key")
255
256 # TODO - check default user for azure
257 # default admin user
258 self._default_admin_user = "azureuser"
259
260 # flavor pattern regex
261 if "flavors_pattern" in config:
262 self._config["flavors_pattern"] = config["flavors_pattern"]
263
264 def _find_in_capabilities(self, capabilities, name):
265 cap = find_in_list(capabilities, lambda c: c["name"] == name)
266 if cap:
267 return cap.get("value")
268 else:
269 return None
270
271 def _reload_connection(self):
272 """
273 Called before any operation, checks python azure clients
274 """
275 if self.reload_client:
276 self.logger.debug("reloading azure client")
277
278 try:
279 self.credentials = ClientSecretCredential(
280 client_id=self._config["user"],
281 client_secret=self._config["passwd"],
282 tenant_id=self._config["tenant"],
283 )
284 self.conn = ResourceManagementClient(
285 self.credentials,
286 self._config["subscription_id"],
287 profile=self.AZURE_RESOURCE_MGMT_PROFILE,
288 )
289 self.conn_compute = ComputeManagementClient(
290 self.credentials,
291 self._config["subscription_id"],
292 profile=self.AZURE_COMPUTE_MGMT_PROFILE,
293 )
294 self.conn_vnet = NetworkManagementClient(
295 self.credentials,
296 self._config["subscription_id"],
297 profile=self.AZURE_NETWORK_MGMT_PROFILE,
298 )
299 self._check_or_create_resource_group()
300 self._check_or_create_vnet()
301
302 # Set to client created
303 self.reload_client = False
304 except Exception as e:
305 self._format_vimconn_exception(e)
306
307 def _get_resource_name_from_resource_id(self, resource_id):
308 """
309 Obtains resource_name from the azure complete identifier: resource_name will always be last item
310 """
311 try:
312 resource = str(resource_id.split("/")[-1])
313
314 return resource
315 except Exception as e:
316 raise vimconn.VimConnException(
317 "Unable to get resource name from resource_id '{}' Error: '{}'".format(
318 resource_id, e
319 )
320 )
321
322 def _get_location_from_resource_group(self, resource_group_name):
323 try:
324 location = self.conn.resource_groups.get(resource_group_name).location
325
326 return location
327 except Exception:
328 raise vimconn.VimConnNotFoundException(
329 "Location '{}' not found".format(resource_group_name)
330 )
331
332 def _get_resource_group_name_from_resource_id(self, resource_id):
333 try:
334 rg = str(resource_id.split("/")[4])
335
336 return rg
337 except Exception:
338 raise vimconn.VimConnException(
339 "Unable to get resource group from invalid resource_id format '{}'".format(
340 resource_id
341 )
342 )
343
344 def _get_net_name_from_resource_id(self, resource_id):
345 try:
346 net_name = str(resource_id.split("/")[8])
347
348 return net_name
349 except Exception:
350 raise vimconn.VimConnException(
351 "Unable to get azure net_name from invalid resource_id format '{}'".format(
352 resource_id
353 )
354 )
355
356 def _check_subnets_for_vm(self, net_list):
357 # All subnets must belong to the same resource group and vnet
358 # All subnets must belong to the same resource group anded vnet
359 rg_vnet = set(
360 self._get_resource_group_name_from_resource_id(net["net_id"])
361 + self._get_net_name_from_resource_id(net["net_id"])
362 for net in net_list
363 )
364
365 if len(rg_vnet) != 1:
366 raise self._format_vimconn_exception(
367 "Azure VMs can only attach to subnets in same VNET"
368 )
369
370 def _format_vimconn_exception(self, e):
371 """
372 Transforms a generic or azure exception to a vimcommException
373 """
374 self.logger.error("Azure plugin error: {}".format(e))
375 if isinstance(e, vimconn.VimConnException):
376 raise e
377 elif isinstance(e, AuthenticationError):
378 raise vimconn.VimConnAuthException(type(e).__name__ + ": " + str(e))
379 elif isinstance(e, ConnectionError):
380 raise vimconn.VimConnConnectionException(type(e).__name__ + ": " + str(e))
381 else:
382 # In case of generic error recreate client
383 self.reload_client = True
384
385 raise vimconn.VimConnException(type(e).__name__ + ": " + str(e))
386
387 def _check_or_create_resource_group(self):
388 """
389 Creates the base resource group if it does not exist
390 """
391 try:
392 rg_exists = self.conn.resource_groups.check_existence(self.resource_group)
393
394 if not rg_exists:
395 self.logger.debug("create base rgroup: %s", self.resource_group)
396 self.conn.resource_groups.create_or_update(
397 self.resource_group, {"location": self.region}
398 )
399 except Exception as e:
400 self._format_vimconn_exception(e)
401
402 def _check_or_create_vnet(self):
403 """
404 Try to get existent base vnet, in case it does not exist it creates it
405 """
406 try:
407 vnet = self.conn_vnet.virtual_networks.get(
408 self.vnet_resource_group or self.resource_group, self.vnet_name
409 )
410 self.vnet_address_space = vnet.address_space.address_prefixes[0]
411 self.vnet_id = vnet.id
412
413 return
414 except CloudError as e:
415 if e.error.error and "notfound" in e.error.error.lower():
416 self.logger.exception("CloudError Exception occured.")
417 # continue and create it
418 else:
419 self._format_vimconn_exception(e)
420
421 # if it does not exist, create it
422 try:
423 vnet_params = {
424 "location": self.region,
425 "address_space": {"address_prefixes": ["10.0.0.0/8"]},
426 }
427 self.vnet_address_space = "10.0.0.0/8"
428
429 self.logger.debug("create base vnet: %s", self.vnet_name)
430 self.conn_vnet.virtual_networks.begin_create_or_update(
431 self.vnet_resource_group or self.resource_group,
432 self.vnet_name,
433 vnet_params,
434 )
435 vnet = self.conn_vnet.virtual_networks.get(
436 self.vnet_resource_group or self.resource_group, self.vnet_name
437 )
438 self.vnet_id = vnet.id
439 except Exception as e:
440 self._format_vimconn_exception(e)
441
442 def new_network(
443 self,
444 net_name,
445 net_type,
446 ip_profile=None,
447 shared=False,
448 provider_network_profile=None,
449 ):
450 """
451 Adds a tenant network to VIM
452 :param net_name: name of the network
453 :param net_type: not used for azure networks
454 :param ip_profile: is a dict containing the IP parameters of the network (Currently only IPv4 is implemented)
455 'ip-version': can be one of ['IPv4','IPv6']
456 'subnet-address': ip_prefix_schema, that is X.X.X.X/Y
457 'gateway-address': (Optional) ip_schema, that is X.X.X.X, not implemented for azure connector
458 'dns-address': (Optional) ip_schema, not implemented for azure connector
459 'dhcp': (Optional) dict containing, not implemented for azure connector
460 'enabled': {'type': 'boolean'},
461 'start-address': ip_schema, first IP to grant
462 'count': number of IPs to grant.
463 :param shared: Not allowed for Azure Connector
464 :param provider_network_profile: (optional) contains {segmentation-id: vlan, provider-network: vim_netowrk}
465 :return: a tuple with the network identifier and created_items, or raises an exception on error
466 created_items can be None or a dictionary where this method can include key-values that will be passed to
467 the method delete_network. Can be used to store created segments, created l2gw connections, etc.
468 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
469 as not present.
470 """
471 return self._new_subnet(net_name, ip_profile)
472
473 def _new_subnet(self, net_name, ip_profile):
474 """
475 Adds a tenant network to VIM. It creates a new subnet at existing base vnet
476 :param net_name: subnet name
477 :param ip_profile:
478 subnet-address: if it is not provided a subnet/24 in the default vnet is created,
479 otherwise it creates a subnet in the indicated address
480 :return: a tuple with the network identifier and created_items, or raises an exception on error
481 """
482 self.logger.debug("create subnet name %s, ip_profile %s", net_name, ip_profile)
483 self._reload_connection()
484
485 if ip_profile is None:
486 # get a non used vnet ip range /24 and allocate automatically inside the range self.vnet_address_space
487 used_subnets = self.get_network_list()
488 for ip_range in netaddr.IPNetwork(self.vnet_address_space).subnet(24):
489 for used_subnet in used_subnets:
490 subnet_range = netaddr.IPNetwork(used_subnet["cidr_block"])
491
492 if subnet_range in ip_range or ip_range in subnet_range:
493 # this range overlaps with an existing subnet ip range. Breaks and look for another
494 break
495 else:
496 ip_profile = {"subnet_address": str(ip_range)}
497 self.logger.debug("dinamically obtained ip_profile: %s", ip_range)
498 break
499 else:
500 raise vimconn.VimConnException(
501 "Cannot find a non-used subnet range in {}".format(
502 self.vnet_address_space
503 )
504 )
505 else:
506 ip_profile = {"subnet_address": ip_profile["subnet_address"]}
507
508 try:
509 # subnet_name = "{}-{}".format(net_name[:24], uuid4())
510 subnet_params = {"address_prefix": ip_profile["subnet_address"]}
511 # Assign a not duplicated net name
512 subnet_name = self._get_unused_subnet_name(net_name)
513
514 self.logger.debug("creating subnet_name: {}".format(subnet_name))
515 async_creation = self.conn_vnet.subnets.begin_create_or_update(
516 self.vnet_resource_group or self.resource_group,
517 self.vnet_name,
518 subnet_name,
519 subnet_params,
520 )
521 async_creation.wait()
522 # TODO - do not wait here, check where it is used
523 self.logger.debug("created subnet_name: {}".format(subnet_name))
524
525 return "{}/subnets/{}".format(self.vnet_id, subnet_name), None
526 except Exception as e:
527 self._format_vimconn_exception(e)
528
529 def _get_unused_subnet_name(self, subnet_name):
530 """
531 Adds a prefix to the subnet_name with a number in case the indicated name is repeated
532 Checks subnets with the indicated name (without suffix) and adds a suffix with a number
533 """
534 all_subnets = self.conn_vnet.subnets.list(
535 self.vnet_resource_group or self.resource_group, self.vnet_name
536 )
537 # Filter to subnets starting with the indicated name
538 subnets = list(
539 filter(lambda subnet: (subnet.name.startswith(subnet_name)), all_subnets)
540 )
541 net_names = [str(subnet.name) for subnet in subnets]
542
543 # get the name with the first not used suffix
544 name_suffix = 0
545 # name = subnet_name + "-" + str(name_suffix)
546 name = subnet_name # first subnet created will have no prefix
547 while name in net_names:
548 name_suffix += 1
549 name = subnet_name + "-" + str(name_suffix)
550
551 return name
552
553 def _create_nic(self, net, nic_name, region=None, static_ip=None, created_items={}):
554 self.logger.debug("create nic name %s, net_name %s", nic_name, net)
555 self._reload_connection()
556
557 subnet_id = net["net_id"]
558 location = self.region or self._get_location_from_resource_group(
559 self.resource_group
560 )
561
562 try:
563 net_ifz = {"location": location}
564 net_ip_config = {
565 "name": nic_name + "-ipconfiguration",
566 "subnet": {"id": subnet_id},
567 }
568
569 if static_ip:
570 net_ip_config["privateIPAddress"] = static_ip
571 net_ip_config["privateIPAllocationMethod"] = "Static"
572
573 net_ifz["ip_configurations"] = [net_ip_config]
574 mac_address = net.get("mac_address")
575
576 if mac_address:
577 net_ifz["mac_address"] = mac_address
578
579 async_nic_creation = (
580 self.conn_vnet.network_interfaces.begin_create_or_update(
581 self.resource_group, nic_name, net_ifz
582 )
583 )
584 nic_data = async_nic_creation.result()
585 created_items[nic_data.id] = True
586 self.logger.debug("created nic name %s", nic_name)
587
588 public_ip = net.get("floating_ip")
589 if public_ip:
590 public_ip_address_params = {
591 "location": location,
592 "public_ip_allocation_method": "Dynamic",
593 }
594 public_ip_name = nic_name + "-public-ip"
595 async_public_ip = (
596 self.conn_vnet.public_ip_addresses.begin_create_or_update(
597 self.resource_group, public_ip_name, public_ip_address_params
598 )
599 )
600 public_ip = async_public_ip.result()
601 self.logger.debug("created public IP: {}".format(public_ip))
602
603 # Associate NIC to Public IP
604 nic_data = self.conn_vnet.network_interfaces.get(
605 self.resource_group, nic_name
606 )
607
608 nic_data.ip_configurations[0].public_ip_address = public_ip
609 created_items[public_ip.id] = True
610
611 self.conn_vnet.network_interfaces.begin_create_or_update(
612 self.resource_group, nic_name, nic_data
613 )
614
615 except Exception as e:
616 self._format_vimconn_exception(e)
617
618 return nic_data, created_items
619
620 def new_flavor(self, flavor_data):
621 """
622 It is not allowed to create new flavors in Azure, must always use an existing one
623 """
624 raise vimconn.VimConnAuthException(
625 "It is not possible to create new flavors in AZURE"
626 )
627
628 def new_tenant(self, tenant_name, tenant_description):
629 """
630 It is not allowed to create new tenants in azure
631 """
632 raise vimconn.VimConnAuthException(
633 "It is not possible to create a TENANT in AZURE"
634 )
635
636 def new_image(self, image_dict):
637 """
638 It is not allowed to create new images in Azure, must always use an existing one
639 """
640 raise vimconn.VimConnAuthException(
641 "It is not possible to create new images in AZURE"
642 )
643
644 def get_image_id_from_path(self, path):
645 """Get the image id from image path in the VIM database.
646 Returns the image_id or raises a vimconnNotFoundException
647 """
648 raise vimconn.VimConnAuthException(
649 "It is not possible to obtain image from path in AZURE"
650 )
651
652 def get_image_list(self, filter_dict={}):
653 """Obtain tenant images from VIM
654 Filter_dict can be:
655 name: image name with the format: publisher:offer:sku:version
656 If some part of the name is provide ex: publisher:offer it will search all availables skus and version
657 for the provided publisher and offer
658 id: image uuid, currently not supported for azure
659 Returns the image list of dictionaries:
660 [{<the fields at Filter_dict plus some VIM specific>}, ...]
661 List can be empty
662 """
663 self.logger.debug("get_image_list filter {}".format(filter_dict))
664
665 self._reload_connection()
666 try:
667 image_list = []
668 if filter_dict.get("name"):
669 # name will have the format "publisher:offer:sku:version"
670 # publisher is required, offer sku and version will be searched if not provided
671 params = filter_dict["name"].split(":")
672 publisher = params[0]
673 if publisher:
674 # obtain offer list
675 offer_list = self._get_offer_list(params, publisher)
676
677 for offer in offer_list:
678 # obtain skus
679 sku_list = self._get_sku_list(params, publisher, offer)
680
681 for sku in sku_list:
682 # if version is defined get directly version, else list images
683 if len(params) == 4 and params[3]:
684 version = params[3]
685 if version == "latest":
686 image_list = self._get_sku_image_list(
687 publisher, offer, sku
688 )
689 image_list = [image_list[-1]]
690 else:
691 image_list = self._get_version_image_list(
692 publisher, offer, sku, version
693 )
694 else:
695 image_list = self._get_sku_image_list(
696 publisher, offer, sku
697 )
698 else:
699 raise vimconn.VimConnAuthException(
700 "List images in Azure must include name param with at least publisher"
701 )
702 else:
703 raise vimconn.VimConnAuthException(
704 "List images in Azure must include name param with at"
705 " least publisher"
706 )
707
708 return image_list
709 except Exception as e:
710 self._format_vimconn_exception(e)
711
712 def _get_offer_list(self, params, publisher):
713 """
714 Helper method to obtain offer list for defined publisher
715 """
716 if len(params) >= 2 and params[1]:
717 return [params[1]]
718 else:
719 try:
720 # get list of offers from azure
721 result_offers = self.conn_compute.virtual_machine_images.list_offers(
722 self.region, publisher
723 )
724
725 return [offer.name for offer in result_offers]
726 except CloudError as e:
727 # azure raises CloudError when not found
728 self.logger.info(
729 "error listing offers for publisher {}, Error: {}".format(
730 publisher, e
731 )
732 )
733
734 return []
735
736 def _get_sku_list(self, params, publisher, offer):
737 """
738 Helper method to obtain sku list for defined publisher and offer
739 """
740 if len(params) >= 3 and params[2]:
741 return [params[2]]
742 else:
743 try:
744 # get list of skus from azure
745 result_skus = self.conn_compute.virtual_machine_images.list_skus(
746 self.region, publisher, offer
747 )
748
749 return [sku.name for sku in result_skus]
750 except CloudError as e:
751 # azure raises CloudError when not found
752 self.logger.info(
753 "error listing skus for publisher {}, offer {}, Error: {}".format(
754 publisher, offer, e
755 )
756 )
757
758 return []
759
760 def _get_sku_image_list(self, publisher, offer, sku):
761 """
762 Helper method to obtain image list for publisher, offer and sku
763 """
764 image_list = []
765 try:
766 result_images = self.conn_compute.virtual_machine_images.list(
767 self.region, publisher, offer, sku
768 )
769 for result_image in result_images:
770 image_list.append(
771 {
772 "id": str(result_image.id),
773 "name": ":".join([publisher, offer, sku, result_image.name]),
774 }
775 )
776 except CloudError as e:
777 self.logger.info(
778 "error listing skus for publisher {}, offer {}, Error: {}".format(
779 publisher, offer, e
780 )
781 )
782 image_list = []
783
784 return image_list
785
786 def _get_version_image_list(self, publisher, offer, sku, version):
787 image_list = []
788 try:
789 result_image = self.conn_compute.virtual_machine_images.get(
790 self.region, publisher, offer, sku, version
791 )
792
793 if result_image:
794 image_list.append(
795 {
796 "id": str(result_image.id),
797 "name": ":".join([publisher, offer, sku, version]),
798 }
799 )
800 except CloudError as e:
801 # azure gives CloudError when not found
802 self.logger.info(
803 "error listing images for publisher {}, offer {}, sku {}, version {} Error: {}".format(
804 publisher, offer, sku, version, e
805 )
806 )
807 image_list = []
808
809 return image_list
810
811 def get_network_list(self, filter_dict={}):
812 """Obtain tenant networks of VIM
813 Filter_dict can be:
814 name: network name
815 id: network id
816 shared: boolean, not implemented in Azure
817 tenant_id: tenant, not used in Azure, all networks same tenants
818 admin_state_up: boolean, not implemented in Azure
819 status: 'ACTIVE', not implemented in Azure #
820 Returns the network list of dictionaries
821 """
822 # self.logger.debug("getting network list for vim, filter %s", filter_dict)
823 try:
824 self._reload_connection()
825
826 vnet = self.conn_vnet.virtual_networks.get(
827 self.vnet_resource_group or self.resource_group, self.vnet_name
828 )
829 subnet_list = []
830
831 for subnet in vnet.subnets:
832 if filter_dict:
833 if filter_dict.get("id") and str(subnet.id) != filter_dict["id"]:
834 continue
835
836 if (
837 filter_dict.get("name")
838 and str(subnet.name) != filter_dict["name"]
839 ):
840 continue
841
842 name = self._get_resource_name_from_resource_id(subnet.id)
843
844 subnet_list.append(
845 {
846 "id": str(subnet.id),
847 "name": name,
848 "status": self.provision_state2osm[subnet.provisioning_state],
849 "cidr_block": str(subnet.address_prefix),
850 "type": "bridge",
851 "shared": False,
852 }
853 )
854
855 return subnet_list
856 except Exception as e:
857 self._format_vimconn_exception(e)
858
859 def new_vminstance(
860 self,
861 name,
862 description,
863 start,
864 image_id,
865 flavor_id,
866 affinity_group_list,
867 net_list,
868 cloud_config=None,
869 disk_list=None,
870 availability_zone_index=None,
871 availability_zone_list=None,
872 ):
873 self.logger.debug(
874 "new vm instance name: %s, image_id: %s, flavor_id: %s, net_list: %s, cloud_config: %s, "
875 "disk_list: %s, availability_zone_index: %s, availability_zone_list: %s",
876 name,
877 image_id,
878 flavor_id,
879 net_list,
880 cloud_config,
881 disk_list,
882 availability_zone_index,
883 availability_zone_list,
884 )
885 self._reload_connection()
886
887 # Validate input data is valid
888 # The virtual machine name must have less or 64 characters and it can not have the following
889 # characters: (~ ! @ # $ % ^ & * ( ) = + _ [ ] { } \ | ; : ' " , < > / ?.)
890 vm_name = self._check_vm_name(name)
891 # Obtain vm unused name
892 vm_name = self._get_unused_vm_name(vm_name)
893
894 # At least one network must be provided
895 if not net_list:
896 raise vimconn.VimConnException(
897 "At least one net must be provided to create a new VM"
898 )
899
900 # image_id are several fields of the image_id
901 image_reference = self._get_image_reference(image_id)
902
903 try:
904 virtual_machine = None
905 created_items = {}
906
907 # Create nics for each subnet
908 self._check_subnets_for_vm(net_list)
909 vm_nics = []
910
911 for idx, net in enumerate(net_list):
912 # Fault with subnet_id
913 # subnet_id=net["subnet_id"]
914 # subnet_id=net["net_id"]
915 nic_name = vm_name + "-nic-" + str(idx)
916 vm_nic, nic_items = self._create_nic(
917 net, nic_name, self.region, net.get("ip_address"), created_items
918 )
919 vm_nics.append({"id": str(vm_nic.id)})
920 net["vim_id"] = vm_nic.id
921
922 vm_parameters = {
923 "location": self.region,
924 "os_profile": self._build_os_profile(vm_name, cloud_config, image_id),
925 "hardware_profile": {"vm_size": flavor_id},
926 "storage_profile": {"image_reference": image_reference},
927 }
928
929 # If the machine has several networks one must be marked as primary
930 # As it is not indicated in the interface the first interface will be marked as primary
931 if len(vm_nics) > 1:
932 for idx, vm_nic in enumerate(vm_nics):
933 if idx == 0:
934 vm_nics[0]["Primary"] = True
935 else:
936 vm_nics[idx]["Primary"] = False
937
938 vm_parameters["network_profile"] = {"network_interfaces": vm_nics}
939
940 # Obtain zone information
941 vm_zone = self._get_vm_zone(availability_zone_index, availability_zone_list)
942 if vm_zone:
943 vm_parameters["zones"] = [vm_zone]
944
945 self.logger.debug("create vm name: %s", vm_name)
946 creation_result = self.conn_compute.virtual_machines.begin_create_or_update(
947 self.resource_group, vm_name, vm_parameters, polling=False
948 )
949 self.logger.debug("obtained creation result: %s", creation_result)
950 virtual_machine = creation_result.result()
951 self.logger.debug("created vm name: %s", vm_name)
952
953 return virtual_machine.id, created_items
954
955 except Exception as e:
956 # Rollback vm creacion
957 vm_id = None
958
959 if virtual_machine:
960 vm_id = virtual_machine.id
961
962 try:
963 self.logger.debug("exception creating vm try to rollback")
964 self.delete_vminstance(vm_id, created_items)
965 except Exception as e2:
966 self.logger.error("new_vminstance rollback fail {}".format(e2))
967
968 self.logger.debug("Exception creating new vminstance: %s", e, exc_info=True)
969 self._format_vimconn_exception(e)
970
971 def _build_os_profile(self, vm_name, cloud_config, image_id):
972 # initial os_profile
973 os_profile = {"computer_name": vm_name}
974
975 # for azure os_profile admin_username is required
976 if cloud_config and cloud_config.get("users"):
977 admin_username = cloud_config.get("users")[0].get(
978 "name", self._get_default_admin_user(image_id)
979 )
980 else:
981 admin_username = self._get_default_admin_user(image_id)
982 os_profile["admin_username"] = admin_username
983
984 # if there is a cloud-init load it
985 if cloud_config:
986 _, userdata = self._create_user_data(cloud_config)
987 custom_data = base64.b64encode(userdata.encode("utf-8")).decode("latin-1")
988 os_profile["custom_data"] = custom_data
989
990 # either password of ssh-keys are required
991 # we will always use ssh-keys, in case it is not available we will generate it
992 if cloud_config and cloud_config.get("key-pairs"):
993 key_data = cloud_config.get("key-pairs")[0]
994 else:
995 _, key_data = self._generate_keys()
996
997 os_profile["linux_configuration"] = {
998 "ssh": {
999 "public_keys": [
1000 {
1001 "path": "/home/{}/.ssh/authorized_keys".format(admin_username),
1002 "key_data": key_data,
1003 }
1004 ]
1005 },
1006 }
1007
1008 return os_profile
1009
1010 def _generate_keys(self):
1011 """Method used to generate a pair of private/public keys.
1012 This method is used because to create a vm in Azure we always need a key or a password
1013 In some cases we may have a password in a cloud-init file but it may not be available
1014 """
1015 key = rsa.generate_private_key(
1016 backend=crypto_default_backend(), public_exponent=65537, key_size=2048
1017 )
1018 private_key = key.private_bytes(
1019 crypto_serialization.Encoding.PEM,
1020 crypto_serialization.PrivateFormat.PKCS8,
1021 crypto_serialization.NoEncryption(),
1022 )
1023 public_key = key.public_key().public_bytes(
1024 crypto_serialization.Encoding.OpenSSH,
1025 crypto_serialization.PublicFormat.OpenSSH,
1026 )
1027 private_key = private_key.decode("utf8")
1028 # Change first line because Paramiko needs a explicit start with 'BEGIN RSA PRIVATE KEY'
1029 i = private_key.find("\n")
1030 private_key = "-----BEGIN RSA PRIVATE KEY-----" + private_key[i:]
1031 public_key = public_key.decode("utf8")
1032
1033 return private_key, public_key
1034
1035 def _get_unused_vm_name(self, vm_name):
1036 """
1037 Checks the vm name and in case it is used adds a suffix to the name to allow creation
1038 :return:
1039 """
1040 all_vms = self.conn_compute.virtual_machines.list(self.resource_group)
1041 # Filter to vms starting with the indicated name
1042 vms = list(filter(lambda vm: (vm.name.startswith(vm_name)), all_vms))
1043 vm_names = [str(vm.name) for vm in vms]
1044
1045 # get the name with the first not used suffix
1046 name_suffix = 0
1047 # name = subnet_name + "-" + str(name_suffix)
1048 name = vm_name # first subnet created will have no prefix
1049
1050 while name in vm_names:
1051 name_suffix += 1
1052 name = vm_name + "-" + str(name_suffix)
1053
1054 return name
1055
1056 def _get_vm_zone(self, availability_zone_index, availability_zone_list):
1057 if availability_zone_index is None:
1058 return None
1059
1060 vim_availability_zones = self._get_azure_availability_zones()
1061 # check if VIM offer enough availability zones describe in the VNFD
1062 if vim_availability_zones and len(availability_zone_list) <= len(
1063 vim_availability_zones
1064 ):
1065 # check if all the names of NFV AV match VIM AV names
1066 match_by_index = False
1067
1068 if not availability_zone_list:
1069 match_by_index = True
1070 else:
1071 for av in availability_zone_list:
1072 if av not in vim_availability_zones:
1073 match_by_index = True
1074 break
1075
1076 if match_by_index:
1077 return vim_availability_zones[availability_zone_index]
1078 else:
1079 return availability_zone_list[availability_zone_index]
1080 else:
1081 raise vimconn.VimConnConflictException(
1082 "No enough availability zones at VIM for this deployment"
1083 )
1084
1085 def _get_azure_availability_zones(self):
1086 return self.AZURE_ZONES
1087
1088 def _get_image_reference(self, image_id):
1089 try:
1090 # The data input format example:
1091 # /Subscriptions/ca3d18ab-d373-4afb-a5d6-7c44f098d16a/Providers/Microsoft.Compute/Locations/westeurope/
1092 # Publishers/Canonical/ArtifactTypes/VMImage/
1093 # Offers/UbuntuServer/
1094 # Skus/18.04-LTS/
1095 # Versions/18.04.201809110
1096 publisher = str(image_id.split("/")[8])
1097 offer = str(image_id.split("/")[12])
1098 sku = str(image_id.split("/")[14])
1099 version = str(image_id.split("/")[16])
1100
1101 return {
1102 "publisher": publisher,
1103 "offer": offer,
1104 "sku": sku,
1105 "version": version,
1106 }
1107 except Exception:
1108 raise vimconn.VimConnException(
1109 "Unable to get image_reference from invalid image_id format: '{}'".format(
1110 image_id
1111 )
1112 )
1113
1114 # Azure VM names can not have some special characters
1115 def _check_vm_name(self, vm_name):
1116 """
1117 Checks vm name, in case the vm has not allowed characters they are removed, not error raised
1118 """
1119 chars_not_allowed_list = "~!@#$%^&*()=+_[]{}|;:<>/?."
1120
1121 # First: the VM name max length is 64 characters
1122 vm_name_aux = vm_name[:64]
1123
1124 # Second: replace not allowed characters
1125 for elem in chars_not_allowed_list:
1126 # Check if string is in the main string
1127 if elem in vm_name_aux:
1128 # self.logger.debug("Dentro del IF")
1129 # Replace the string
1130 vm_name_aux = vm_name_aux.replace(elem, "-")
1131
1132 return vm_name_aux
1133
1134 def get_flavor_id_from_data(self, flavor_dict):
1135 self.logger.debug("getting flavor id from data, flavor_dict: %s", flavor_dict)
1136 filter_dict = flavor_dict or {}
1137
1138 try:
1139 self._reload_connection()
1140 vm_sizes_list = [
1141 vm_size.as_dict()
1142 for vm_size in self.conn_compute.resource_skus.list(
1143 "location eq '{}'".format(self.region)
1144 )
1145 ]
1146
1147 cpus = filter_dict.get("vcpus") or 0
1148 memMB = filter_dict.get("ram") or 0
1149 numberInterfaces = len(filter_dict.get("interfaces", [])) or 0
1150
1151 # Filter
1152 filtered_sizes = []
1153 for size in vm_sizes_list:
1154 if size["resource_type"] == "virtualMachines":
1155 size_cpus = int(
1156 self._find_in_capabilities(size["capabilities"], "vCPUs")
1157 )
1158 size_memory = float(
1159 self._find_in_capabilities(size["capabilities"], "MemoryGB")
1160 )
1161 size_interfaces = self._find_in_capabilities(
1162 size["capabilities"], "MaxNetworkInterfaces"
1163 )
1164 if size_interfaces:
1165 size_interfaces = int(size_interfaces)
1166 else:
1167 self.logger.debug(
1168 "Flavor with no defined MaxNetworkInterfaces: {}".format(
1169 size["name"]
1170 )
1171 )
1172 continue
1173 if (
1174 size_cpus >= cpus
1175 and size_memory >= memMB / 1024
1176 and size_interfaces >= numberInterfaces
1177 ):
1178 if self._config.get("flavors_pattern"):
1179 if re.search(
1180 self._config.get("flavors_pattern"), size["name"]
1181 ):
1182 new_size = {
1183 e["name"]: e["value"] for e in size["capabilities"]
1184 }
1185 new_size["name"] = size["name"]
1186 filtered_sizes.append(new_size)
1187 else:
1188 new_size = {
1189 e["name"]: e["value"] for e in size["capabilities"]
1190 }
1191 new_size["name"] = size["name"]
1192 filtered_sizes.append(new_size)
1193
1194 # Sort
1195 listedFilteredSizes = sorted(
1196 filtered_sizes,
1197 key=lambda k: (
1198 int(k["vCPUs"]),
1199 float(k["MemoryGB"]),
1200 int(k["MaxNetworkInterfaces"]),
1201 int(k["MaxResourceVolumeMB"]),
1202 ),
1203 )
1204
1205 if listedFilteredSizes:
1206 return listedFilteredSizes[0]["name"]
1207
1208 raise vimconn.VimConnNotFoundException(
1209 "Cannot find any flavor matching '{}'".format(str(flavor_dict))
1210 )
1211 except Exception as e:
1212 self._format_vimconn_exception(e)
1213
1214 def _get_flavor_id_from_flavor_name(self, flavor_name):
1215 # self.logger.debug("getting flavor id from flavor name {}".format(flavor_name))
1216 try:
1217 self._reload_connection()
1218 vm_sizes_list = [
1219 vm_size.as_dict()
1220 for vm_size in self.conn_compute.resource_skus.list(
1221 "location eq '{}'".format(self.region)
1222 )
1223 ]
1224
1225 output_flavor = None
1226 for size in vm_sizes_list:
1227 if size["name"] == flavor_name:
1228 output_flavor = size
1229
1230 # None is returned if not found anything
1231 return output_flavor
1232 except Exception as e:
1233 self._format_vimconn_exception(e)
1234
1235 def check_vim_connectivity(self):
1236 try:
1237 self._reload_connection()
1238 return True
1239 except Exception as e:
1240 raise vimconn.VimConnException(
1241 "Connectivity issue with Azure API: {}".format(e)
1242 )
1243
1244 def get_network(self, net_id):
1245 # self.logger.debug("get network id: {}".format(net_id))
1246 # res_name = self._get_resource_name_from_resource_id(net_id)
1247 self._reload_connection()
1248
1249 filter_dict = {"name": net_id}
1250 network_list = self.get_network_list(filter_dict)
1251
1252 if not network_list:
1253 raise vimconn.VimConnNotFoundException(
1254 "network '{}' not found".format(net_id)
1255 )
1256 else:
1257 return network_list[0]
1258
1259 def delete_network(self, net_id, created_items=None):
1260 self.logger.debug(
1261 "deleting network {} - {}".format(
1262 self.vnet_resource_group or self.resource_group, net_id
1263 )
1264 )
1265
1266 self._reload_connection()
1267 res_name = self._get_resource_name_from_resource_id(net_id)
1268
1269 try:
1270 # Obtain subnets ant try to delete nic first
1271 subnet = self.conn_vnet.subnets.get(
1272 self.vnet_resource_group or self.resource_group,
1273 self.vnet_name,
1274 res_name,
1275 )
1276 if not subnet:
1277 raise vimconn.VimConnNotFoundException(
1278 "network '{}' not found".format(net_id)
1279 )
1280
1281 # TODO - for a quick-fix delete nics sequentially but should not wait
1282 # for each in turn
1283 if subnet.ip_configurations:
1284 for ip_configuration in subnet.ip_configurations:
1285 # obtain nic_name from ip_configuration
1286 parsed_id = azure_tools.parse_resource_id(ip_configuration.id)
1287 nic_name = parsed_id["name"]
1288 self.delete_inuse_nic(nic_name)
1289
1290 # Subnet API fails (CloudError: Azure Error: ResourceNotFound)
1291 # Put the initial virtual_network API
1292 async_delete = self.conn_vnet.subnets.begin_delete(
1293 self.vnet_resource_group or self.resource_group,
1294 self.vnet_name,
1295 res_name,
1296 )
1297 async_delete.wait()
1298
1299 return net_id
1300
1301 except ResourceNotFoundError:
1302 raise vimconn.VimConnNotFoundException(
1303 "network '{}' not found".format(net_id)
1304 )
1305 except CloudError as e:
1306 if e.error.error and "notfound" in e.error.error.lower():
1307 raise vimconn.VimConnNotFoundException(
1308 "network '{}' not found".format(net_id)
1309 )
1310 else:
1311 self._format_vimconn_exception(e)
1312 except Exception as e:
1313 self._format_vimconn_exception(e)
1314
1315 def delete_inuse_nic(self, nic_name):
1316 # Obtain nic data
1317 nic_data = self.conn_vnet.network_interfaces.get(self.resource_group, nic_name)
1318
1319 # Obtain vm associated to nic in case it exists
1320 if nic_data.virtual_machine:
1321 vm_name = azure_tools.parse_resource_id(nic_data.virtual_machine.id)["name"]
1322 self.logger.debug("vm_name: {}".format(vm_name))
1323 virtual_machine = self.conn_compute.virtual_machines.get(
1324 self.resource_group, vm_name
1325 )
1326 self.logger.debug("obtained vm")
1327
1328 # Deattach nic from vm if it has netwolk machines attached
1329 network_interfaces = virtual_machine.network_profile.network_interfaces
1330 network_interfaces[:] = [
1331 interface
1332 for interface in network_interfaces
1333 if self._get_resource_name_from_resource_id(interface.id) != nic_name
1334 ]
1335
1336 # TODO - check if there is a public ip to delete and delete it
1337 if network_interfaces:
1338 # Deallocate the vm
1339 async_vm_deallocate = (
1340 self.conn_compute.virtual_machines.begin_deallocate(
1341 self.resource_group, vm_name
1342 )
1343 )
1344 self.logger.debug("deallocating vm")
1345 async_vm_deallocate.wait()
1346 self.logger.debug("vm deallocated")
1347
1348 async_vm_update = (
1349 self.conn_compute.virtual_machines.begin_create_or_update(
1350 self.resource_group, vm_name, virtual_machine
1351 )
1352 )
1353 virtual_machine = async_vm_update.result()
1354 self.logger.debug("nic removed from interface")
1355
1356 else:
1357 self.logger.debug("There are no interfaces left, delete vm")
1358 self.delete_vminstance(virtual_machine.id)
1359 self.logger.debug("Delete vm")
1360
1361 # Delete nic
1362 self.logger.debug("delete NIC name: %s", nic_name)
1363 nic_delete = self.conn_vnet.network_interfaces.begin_delete(
1364 self.resource_group, nic_name
1365 )
1366 nic_delete.wait()
1367 self.logger.debug("deleted NIC name: %s", nic_name)
1368
1369 def delete_vminstance(self, vm_id, created_items=None, volumes_to_hold=None):
1370 """Deletes a vm instance from the vim."""
1371 self.logger.debug(
1372 "deleting VM instance {} - {}".format(self.resource_group, vm_id)
1373 )
1374 self._reload_connection()
1375
1376 created_items = created_items or {}
1377 try:
1378 # Check vm exists, we can call delete_vm to clean created_items
1379 if vm_id:
1380 res_name = self._get_resource_name_from_resource_id(vm_id)
1381 vm = self.conn_compute.virtual_machines.get(
1382 self.resource_group, res_name
1383 )
1384
1385 # Shuts down the virtual machine and releases the compute resources
1386 # vm_stop = self.conn_compute.virtual_machines.power_off(self.resource_group, resName)
1387 # vm_stop.wait()
1388
1389 vm_delete = self.conn_compute.virtual_machines.begin_delete(
1390 self.resource_group, res_name
1391 )
1392 vm_delete.wait()
1393 self.logger.debug("deleted VM name: %s", res_name)
1394
1395 # Delete OS Disk, check if exists, in case of error creating
1396 # it may not be fully created
1397 if vm.storage_profile.os_disk:
1398 os_disk_name = vm.storage_profile.os_disk.name
1399 self.logger.debug("delete OS DISK: %s", os_disk_name)
1400 async_disk_delete = self.conn_compute.disks.begin_delete(
1401 self.resource_group, os_disk_name
1402 )
1403 async_disk_delete.wait()
1404 # os disks are created always with the machine
1405 self.logger.debug("deleted OS DISK name: %s", os_disk_name)
1406
1407 for data_disk in vm.storage_profile.data_disks:
1408 self.logger.debug("delete data_disk: %s", data_disk.name)
1409 async_disk_delete = self.conn_compute.disks.begin_delete(
1410 self.resource_group, data_disk.name
1411 )
1412 async_disk_delete.wait()
1413 self._markdel_created_item(data_disk.managed_disk.id, created_items)
1414 self.logger.debug("deleted OS DISK name: %s", data_disk.name)
1415
1416 # After deleting VM, it is necessary to delete NIC, because if is not deleted delete_network
1417 # does not work because Azure says that is in use the subnet
1418 network_interfaces = vm.network_profile.network_interfaces
1419
1420 for network_interface in network_interfaces:
1421 nic_name = self._get_resource_name_from_resource_id(
1422 network_interface.id
1423 )
1424 nic_data = self.conn_vnet.network_interfaces.get(
1425 self.resource_group, nic_name
1426 )
1427
1428 public_ip_name = None
1429 exist_public_ip = nic_data.ip_configurations[0].public_ip_address
1430 if exist_public_ip:
1431 public_ip_id = nic_data.ip_configurations[
1432 0
1433 ].public_ip_address.id
1434
1435 # Delete public_ip
1436 public_ip_name = self._get_resource_name_from_resource_id(
1437 public_ip_id
1438 )
1439
1440 # Public ip must be deleted afterwards of nic that is attached
1441
1442 self.logger.debug("delete NIC name: %s", nic_name)
1443 nic_delete = self.conn_vnet.network_interfaces.begin_delete(
1444 self.resource_group, nic_name
1445 )
1446 nic_delete.wait()
1447 self._markdel_created_item(network_interface.id, created_items)
1448 self.logger.debug("deleted NIC name: %s", nic_name)
1449
1450 # Delete list of public ips
1451 if public_ip_name:
1452 self.logger.debug("delete PUBLIC IP - " + public_ip_name)
1453 ip_delete = self.conn_vnet.public_ip_addresses.begin_delete(
1454 self.resource_group, public_ip_name
1455 )
1456 ip_delete.wait()
1457 self._markdel_created_item(public_ip_id, created_items)
1458
1459 # Delete created items
1460 self._delete_created_items(created_items)
1461
1462 except ResourceNotFoundError:
1463 raise vimconn.VimConnNotFoundException(
1464 "No vm instance found '{}'".format(vm_id)
1465 )
1466 except CloudError as e:
1467 if e.error.error and "notfound" in e.error.error.lower():
1468 raise vimconn.VimConnNotFoundException(
1469 "No vm instance found '{}'".format(vm_id)
1470 )
1471 else:
1472 self._format_vimconn_exception(e)
1473 except Exception as e:
1474 self._format_vimconn_exception(e)
1475
1476 def _markdel_created_item(self, item_id, created_items):
1477 if item_id in created_items:
1478 created_items[item_id] = False
1479
1480 def _delete_created_items(self, created_items):
1481 """Delete created_items elements that have not been deleted with the virtual machine
1482 Created_items may not be deleted correctly with the created machine if the
1483 virtual machine fails creating or in other cases of error
1484 """
1485 self.logger.debug("Created items: %s", created_items)
1486 # TODO - optimize - should not wait until it is deleted
1487 # Must delete in order first nics, then public_ips
1488 # As dictionaries don't preserve order, first get items to be deleted then delete them
1489 nics_to_delete = []
1490 publics_ip_to_delete = []
1491 disks_to_delete = []
1492 for item_id, v in created_items.items():
1493 if not v: # skip already deleted
1494 continue
1495
1496 # self.logger.debug("Must delete item id: %s", item_id)
1497 # Obtain type, supported nic, disk or public ip
1498 parsed_id = azure_tools.parse_resource_id(item_id)
1499 resource_type = parsed_id.get("resource_type")
1500 name = parsed_id.get("name")
1501
1502 if resource_type == "networkInterfaces":
1503 nics_to_delete.append(name)
1504 elif resource_type == "publicIPAddresses":
1505 publics_ip_to_delete.append(name)
1506 elif resource_type == "disks":
1507 disks_to_delete.append(name)
1508
1509 # Now delete
1510 for item_name in nics_to_delete:
1511 try:
1512 self.logger.debug("deleting nic name %s:", item_name)
1513 nic_delete = self.conn_vnet.network_interfaces.begin_delete(
1514 self.resource_group, item_name
1515 )
1516 nic_delete.wait()
1517 self.logger.debug("deleted nic name %s:", item_name)
1518 except Exception as e:
1519 self.logger.error(
1520 "Error deleting item: {}: {}".format(type(e).__name__, e)
1521 )
1522
1523 for item_name in publics_ip_to_delete:
1524 try:
1525 self.logger.debug("deleting public ip name %s:", item_name)
1526 ip_delete = self.conn_vnet.public_ip_addresses.begin_delete(
1527 self.resource_group, name
1528 )
1529 ip_delete.wait()
1530 self.logger.debug("deleted public ip name %s:", item_name)
1531 except Exception as e:
1532 self.logger.error(
1533 "Error deleting item: {}: {}".format(type(e).__name__, e)
1534 )
1535
1536 for item_name in disks_to_delete:
1537 try:
1538 self.logger.debug("deleting data disk name %s:", name)
1539 async_disk_delete = self.conn_compute.disks.begin_delete(
1540 self.resource_group, item_name
1541 )
1542 async_disk_delete.wait()
1543 self.logger.debug("deleted data disk name %s:", name)
1544 except Exception as e:
1545 self.logger.error(
1546 "Error deleting item: {}: {}".format(type(e).__name__, e)
1547 )
1548
1549 def action_vminstance(self, vm_id, action_dict, created_items={}):
1550 """Send and action over a VM instance from VIM
1551 Returns the vm_id if the action was successfully sent to the VIM
1552 """
1553 self.logger.debug("Action over VM '%s': %s", vm_id, str(action_dict))
1554
1555 try:
1556 self._reload_connection()
1557 resName = self._get_resource_name_from_resource_id(vm_id)
1558
1559 if "start" in action_dict:
1560 self.conn_compute.virtual_machines.begin_start(
1561 self.resource_group, resName
1562 )
1563 elif (
1564 "stop" in action_dict
1565 or "shutdown" in action_dict
1566 or "shutoff" in action_dict
1567 ):
1568 self.conn_compute.virtual_machines.begin_power_off(
1569 self.resource_group, resName
1570 )
1571 elif "terminate" in action_dict:
1572 self.conn_compute.virtual_machines.begin_delete(
1573 self.resource_group, resName
1574 )
1575 elif "reboot" in action_dict:
1576 self.conn_compute.virtual_machines.begin_restart(
1577 self.resource_group, resName
1578 )
1579
1580 return None
1581 except ResourceNotFoundError:
1582 raise vimconn.VimConnNotFoundException("No vm found '{}'".format(vm_id))
1583 except CloudError as e:
1584 if e.error.error and "notfound" in e.error.error.lower():
1585 raise vimconn.VimConnNotFoundException("No vm found '{}'".format(vm_id))
1586 else:
1587 self._format_vimconn_exception(e)
1588 except Exception as e:
1589 self._format_vimconn_exception(e)
1590
1591 def delete_flavor(self, flavor_id):
1592 raise vimconn.VimConnAuthException(
1593 "It is not possible to delete a FLAVOR in AZURE"
1594 )
1595
1596 def delete_tenant(self, tenant_id):
1597 raise vimconn.VimConnAuthException(
1598 "It is not possible to delete a TENANT in AZURE"
1599 )
1600
1601 def delete_image(self, image_id):
1602 raise vimconn.VimConnAuthException(
1603 "It is not possible to delete a IMAGE in AZURE"
1604 )
1605
1606 def get_vminstance(self, vm_id):
1607 """
1608 Obtaing the vm instance data from v_id
1609 """
1610 self.logger.debug("get vm instance: %s", vm_id)
1611 self._reload_connection()
1612 try:
1613 resName = self._get_resource_name_from_resource_id(vm_id)
1614 vm = self.conn_compute.virtual_machines.get(self.resource_group, resName)
1615 except ResourceNotFoundError:
1616 raise vimconn.VimConnNotFoundException(
1617 "No vminstance found '{}'".format(vm_id)
1618 )
1619 except CloudError as e:
1620 if e.error.error and "notfound" in e.error.error.lower():
1621 raise vimconn.VimConnNotFoundException(
1622 "No vminstance found '{}'".format(vm_id)
1623 )
1624 else:
1625 self._format_vimconn_exception(e)
1626 except Exception as e:
1627 self._format_vimconn_exception(e)
1628
1629 return vm
1630
1631 def get_flavor(self, flavor_id):
1632 """
1633 Obtains the flavor_data from the flavor_id
1634 """
1635 self._reload_connection()
1636 self.logger.debug("get flavor from id: %s", flavor_id)
1637 flavor_data = self._get_flavor_id_from_flavor_name(flavor_id)
1638
1639 if flavor_data:
1640 flavor = {
1641 "id": flavor_id,
1642 "name": flavor_id,
1643 "ram": flavor_data["memoryInMB"],
1644 "vcpus": flavor_data["numberOfCores"],
1645 "disk": flavor_data["resourceDiskSizeInMB"] / 1024,
1646 }
1647
1648 return flavor
1649 else:
1650 raise vimconn.VimConnNotFoundException(
1651 "flavor '{}' not found".format(flavor_id)
1652 )
1653
1654 def get_tenant_list(self, filter_dict={}):
1655 """Obtains the list of tenants
1656 For the azure connector only the azure tenant will be returned if it is compatible
1657 with filter_dict
1658 """
1659 tenants_azure = [{"name": self.tenant, "id": self.tenant}]
1660 tenant_list = []
1661
1662 self.logger.debug("get tenant list: %s", filter_dict)
1663 for tenant_azure in tenants_azure:
1664 if filter_dict:
1665 if (
1666 filter_dict.get("id")
1667 and str(tenant_azure.get("id")) != filter_dict["id"]
1668 ):
1669 continue
1670
1671 if (
1672 filter_dict.get("name")
1673 and str(tenant_azure.get("name")) != filter_dict["name"]
1674 ):
1675 continue
1676
1677 tenant_list.append(tenant_azure)
1678
1679 return tenant_list
1680
1681 def refresh_nets_status(self, net_list):
1682 """Get the status of the networks
1683 Params: the list of network identifiers
1684 Returns a dictionary with:
1685 net_id: #VIM id of this network
1686 status: #Mandatory. Text with one of:
1687 # DELETED (not found at vim)
1688 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1689 # OTHER (Vim reported other status not understood)
1690 # ERROR (VIM indicates an ERROR status)
1691 # ACTIVE, INACTIVE, DOWN (admin down),
1692 # BUILD (on building process)
1693 #
1694 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1695 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1696 """
1697 out_nets = {}
1698 self._reload_connection()
1699
1700 self.logger.debug("reload nets status net_list: %s", net_list)
1701 for net_id in net_list:
1702 try:
1703 netName = self._get_net_name_from_resource_id(net_id)
1704 resName = self._get_resource_name_from_resource_id(net_id)
1705
1706 net = self.conn_vnet.subnets.get(
1707 self.vnet_resource_group or self.resource_group, netName, resName
1708 )
1709
1710 out_nets[net_id] = {
1711 "status": self.provision_state2osm[net.provisioning_state],
1712 "vim_info": str(net),
1713 }
1714 except CloudError as e:
1715 if e.error.error and "notfound" in e.error.error.lower():
1716 self.logger.info(
1717 "Not found subnet net_name: %s, subnet_name: %s",
1718 netName,
1719 resName,
1720 )
1721 out_nets[net_id] = {"status": "DELETED", "error_msg": str(e)}
1722 else:
1723 self.logger.error(
1724 "CloudError Exception %s when searching subnet", e
1725 )
1726 out_nets[net_id] = {
1727 "status": "VIM_ERROR",
1728 "error_msg": str(e),
1729 }
1730 except vimconn.VimConnNotFoundException as e:
1731 self.logger.error(
1732 "VimConnNotFoundException %s when searching subnet", e
1733 )
1734 out_nets[net_id] = {
1735 "status": "DELETED",
1736 "error_msg": str(e),
1737 }
1738 except Exception as e:
1739 self.logger.error(
1740 "Exception %s when searching subnet", e, exc_info=True
1741 )
1742 out_nets[net_id] = {
1743 "status": "VIM_ERROR",
1744 "error_msg": str(e),
1745 }
1746
1747 return out_nets
1748
1749 def refresh_vms_status(self, vm_list):
1750 """Get the status of the virtual machines and their interfaces/ports
1751 Params: the list of VM identifiers
1752 Returns a dictionary with:
1753 vm_id: # VIM id of this Virtual Machine
1754 status: # Mandatory. Text with one of:
1755 # DELETED (not found at vim)
1756 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1757 # OTHER (Vim reported other status not understood)
1758 # ERROR (VIM indicates an ERROR status)
1759 # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
1760 # BUILD (on building process), ERROR
1761 # ACTIVE:NoMgmtIP (Active but none of its interfaces has an IP address
1762 # (ACTIVE:NoMgmtIP is not returned for Azure)
1763 #
1764 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1765 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1766 interfaces: list with interface info. Each item a dictionary with:
1767 vim_interface_id - The ID of the interface
1768 mac_address - The MAC address of the interface.
1769 ip_address - The IP address of the interface within the subnet.
1770 """
1771 out_vms = {}
1772 self._reload_connection()
1773
1774 self.logger.debug("refresh vm status vm_list: %s", vm_list)
1775 search_vm_list = vm_list or {}
1776
1777 for vm_id in search_vm_list:
1778 out_vm = {}
1779 try:
1780 res_name = self._get_resource_name_from_resource_id(vm_id)
1781
1782 vm = self.conn_compute.virtual_machines.get(
1783 self.resource_group, res_name
1784 )
1785 out_vm["vim_info"] = str(vm)
1786 out_vm["status"] = self.provision_state2osm.get(
1787 vm.provisioning_state, "OTHER"
1788 )
1789
1790 if vm.provisioning_state == "Succeeded":
1791 # check if machine is running or stopped
1792 instance_view = self.conn_compute.virtual_machines.instance_view(
1793 self.resource_group, res_name
1794 )
1795
1796 for status in instance_view.statuses:
1797 splitted_status = status.code.split("/")
1798 if (
1799 len(splitted_status) == 2
1800 and splitted_status[0] == "PowerState"
1801 ):
1802 out_vm["status"] = self.power_state2osm.get(
1803 splitted_status[1], "OTHER"
1804 )
1805
1806 network_interfaces = vm.network_profile.network_interfaces
1807 out_vm["interfaces"] = self._get_vm_interfaces_status(
1808 vm_id, network_interfaces
1809 )
1810
1811 except CloudError as e:
1812 if e.error.error and "notfound" in e.error.error.lower():
1813 self.logger.debug("Not found vm id: %s", vm_id)
1814 out_vm["status"] = "DELETED"
1815 out_vm["error_msg"] = str(e)
1816 out_vm["vim_info"] = None
1817 else:
1818 # maybe connection error or another type of error, return vim error
1819 self.logger.error("Exception %s refreshing vm_status", e)
1820 out_vm["status"] = "VIM_ERROR"
1821 out_vm["error_msg"] = str(e)
1822 out_vm["vim_info"] = None
1823 except Exception as e:
1824 self.logger.error("Exception %s refreshing vm_status", e, exc_info=True)
1825 out_vm["status"] = "VIM_ERROR"
1826 out_vm["error_msg"] = str(e)
1827 out_vm["vim_info"] = None
1828
1829 out_vms[vm_id] = out_vm
1830
1831 return out_vms
1832
1833 def _get_vm_interfaces_status(self, vm_id, interfaces):
1834 """
1835 Gets the interfaces detail for a vm
1836 :param interfaces: List of interfaces.
1837 :return: Dictionary with list of interfaces including, vim_interface_id, mac_address and ip_address
1838 """
1839 try:
1840 interface_list = []
1841 for network_interface in interfaces:
1842 interface_dict = {}
1843 nic_name = self._get_resource_name_from_resource_id(
1844 network_interface.id
1845 )
1846 interface_dict["vim_interface_id"] = network_interface.id
1847
1848 nic_data = self.conn_vnet.network_interfaces.get(
1849 self.resource_group,
1850 nic_name,
1851 )
1852
1853 ips = []
1854 if nic_data.ip_configurations[0].public_ip_address:
1855 self.logger.debug("Obtain public ip address")
1856 public_ip_name = self._get_resource_name_from_resource_id(
1857 nic_data.ip_configurations[0].public_ip_address.id
1858 )
1859 public_ip = self.conn_vnet.public_ip_addresses.get(
1860 self.resource_group, public_ip_name
1861 )
1862 self.logger.debug("Public ip address is: %s", public_ip.ip_address)
1863 ips.append(public_ip.ip_address)
1864
1865 private_ip = nic_data.ip_configurations[0].private_ip_address
1866 ips.append(private_ip)
1867
1868 interface_dict["mac_address"] = nic_data.mac_address
1869 interface_dict["ip_address"] = ";".join(ips)
1870 interface_list.append(interface_dict)
1871
1872 return interface_list
1873 except Exception as e:
1874 self.logger.error(
1875 "Exception %s obtaining interface data for vm: %s",
1876 e,
1877 vm_id,
1878 exc_info=True,
1879 )
1880 self._format_vimconn_exception(e)
1881
1882 def _get_default_admin_user(self, image_id):
1883 if "ubuntu" in image_id.lower():
1884 return "ubuntu"
1885 else:
1886 return self._default_admin_user
1887
1888 def migrate_instance(self, vm_id, compute_host=None):
1889 """
1890 Migrate a vdu
1891 param:
1892 vm_id: ID of an instance
1893 compute_host: Host to migrate the vdu to
1894 """
1895 # TODO: Add support for migration
1896 raise vimconn.VimConnNotImplemented("Not implemented")
1897
1898 def resize_instance(self, vm_id, flavor_id=None):
1899 """
1900 resize a vdu
1901 param:
1902 vm_id: ID of an instance
1903 flavor_id: flavor id to resize the vdu
1904 """
1905 # TODO: Add support for resize
1906 raise vimconn.VimConnNotImplemented("Not implemented")
1907
1908
1909 if __name__ == "__main__":
1910 # Init logger
1911 log_format = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(funcName)s(): %(message)s"
1912 log_formatter = logging.Formatter(log_format, datefmt="%Y-%m-%dT%H:%M:%S")
1913 handler = logging.StreamHandler()
1914 handler.setFormatter(log_formatter)
1915 logger = logging.getLogger("ro.vim.azure")
1916 # logger.setLevel(level=logging.ERROR)
1917 # logger.setLevel(level=logging.INFO)
1918 logger.setLevel(level=logging.DEBUG)
1919 logger.addHandler(handler)
1920
1921 # Making some basic test
1922 vim_id = "azure"
1923 vim_name = "azure"
1924 needed_test_params = {
1925 "client_id": "AZURE_CLIENT_ID",
1926 "secret": "AZURE_SECRET",
1927 "tenant": "AZURE_TENANT",
1928 "resource_group": "AZURE_RESOURCE_GROUP",
1929 "subscription_id": "AZURE_SUBSCRIPTION_ID",
1930 "vnet_name": "AZURE_VNET_NAME",
1931 }
1932 test_params = {}
1933
1934 for param, env_var in needed_test_params.items():
1935 value = getenv(env_var)
1936
1937 if not value:
1938 raise Exception("Provide a valid value for env '{}'".format(env_var))
1939
1940 test_params[param] = value
1941
1942 config = {
1943 "region_name": getenv("AZURE_REGION_NAME", "northeurope"),
1944 "resource_group": getenv("AZURE_RESOURCE_GROUP"),
1945 "subscription_id": getenv("AZURE_SUBSCRIPTION_ID"),
1946 "pub_key": getenv("AZURE_PUB_KEY", None),
1947 "vnet_name": getenv("AZURE_VNET_NAME", "osm_vnet"),
1948 }
1949
1950 azure = vimconnector(
1951 vim_id,
1952 vim_name,
1953 tenant_id=test_params["tenant"],
1954 tenant_name=None,
1955 url=None,
1956 url_admin=None,
1957 user=test_params["client_id"],
1958 passwd=test_params["secret"],
1959 log_level=None,
1960 config=config,
1961 )