1 # -*- coding: utf-8 -*-
3 __author__
='Sergio Gonzalez'
4 __date__
='$18-apr-2019 23:59:59$'
11 from uuid
import uuid4
13 from azure
.common
.credentials
import ServicePrincipalCredentials
14 from azure
.mgmt
.resource
import ResourceManagementClient
15 from azure
.mgmt
.network
import NetworkManagementClient
16 from azure
.mgmt
.compute
import ComputeManagementClient
18 from msrestazure
.azure_exceptions
import CloudError
20 class vimconnector(vimconn
.vimconnector
):
22 provision_state2osm
= {
23 "Deleting": "INACTIVE",
25 "Succeeded": "ACTIVE",
29 def __init__(self
, uuid
, name
, tenant_id
, tenant_name
, url
, url_admin
=None, user
=None, passwd
=None, log_level
=None,
30 config
={}, persistent_info
={}):
32 vimconn
.vimconnector
.__init
__(self
, uuid
, name
, tenant_id
, tenant_name
, url
, url_admin
, user
, passwd
, log_level
,
33 config
, persistent_info
)
35 self
.vnet_address_space
= None
37 self
.logger
= logging
.getLogger('openmano.vim.azure')
40 self
.logger
.setLevel(getattr(logging
, log_level
))
43 self
.credentials
= ServicePrincipalCredentials(
46 tenant
=(tenant_id
or tenant_name
)
50 if 'subscription_id' in config
:
51 self
.subscription_id
= config
.get('subscription_id')
52 self
.logger
.debug('Setting subscription '+str(self
.subscription_id
))
54 raise vimconn
.vimconnException('Subscription not specified')
56 if 'region_name' in config
:
57 self
.region
= config
.get('region_name')
59 raise vimconn
.vimconnException('Azure region_name is not specified at config')
61 if 'resource_group' in config
:
62 self
.resource_group
= config
.get('resource_group')
64 raise vimconn
.vimconnException('Azure resource_group is not specified at config')
66 if 'vnet_name' in config
:
67 self
.vnet_name
= config
["vnet_name"]
70 self
.pub_key
= config
.get('pub_key')
72 def _reload_connection(self
):
74 Sets connections to work with Azure service APIs
77 self
.logger
.debug('Reloading API Connection')
79 self
.conn
= ResourceManagementClient(self
.credentials
, self
.subscription_id
)
80 self
.conn_compute
= ComputeManagementClient(self
.credentials
, self
.subscription_id
)
81 self
.conn_vnet
= NetworkManagementClient(self
.credentials
, self
.subscription_id
)
82 self
._check
_or
_create
_resource
_group
()
83 self
._check
_or
_create
_vnet
()
84 except Exception as e
:
85 self
.format_vimconn_exception(e
)
87 def _get_resource_name_from_resource_id(self
, resource_id
):
88 return str(resource_id
.split('/')[-1])
90 def _get_location_from_resource_group(self
, resource_group_name
):
91 return self
.conn
.resource_groups
.get(resource_group_name
).location
93 def _get_resource_group_name_from_resource_id(self
, resource_id
):
94 return str(resource_id
.split('/')[4])
96 def _check_subnets_for_vm(self
, net_list
):
97 # All subnets must belong to the same resource group and vnet
98 if len(set(self
._get
_resource
_group
_name
_from
_resource
_id
(net
['id']) +
99 self
._get
_resource
_name
_from
_resource
_id
(net
['id']) for net
in net_list
)) != 1:
100 raise self
.format_vimconn_exception('Azure VMs can only attach to subnets in same VNET')
102 def format_vimconn_exception(self
, e
):
104 Params: an Exception object
106 :return: Raises the proper vimconnException
109 self
.conn_vnet
= None
110 raise vimconn
.vimconnConnectionException(type(e
).__name
__ + ': ' + str(e
))
112 def _check_or_create_resource_group(self
):
114 Creates a resource group in indicated region
117 self
.logger
.debug('Creating RG {} in location {}'.format(self
.resource_group
, self
.region
))
118 self
.conn
.resource_groups
.create_or_update(self
.resource_group
, {'location': self
.region
})
120 def _check_or_create_vnet(self
):
122 vnet
= self
.conn_vnet
.virtual_networks
.get(self
.resource_group
, self
.vnet_name
)
123 self
.vnet_address_space
= vnet
.address_space
.address_prefixes
[0]
124 self
.vnet_id
= vnet
.id
126 except CloudError
as e
:
127 if e
.error
.error
== "ResourceNotFound":
131 # if not exist, creates it
134 'location': self
.region
,
136 'address_prefixes': ["10.0.0.0/8"]
139 self
.vnet_address_space
= "10.0.0.0/8"
140 self
.conn_vnet
.virtual_networks
.create_or_update(self
.resource_group
, self
.vnet_name
, vnet_params
)
141 vnet
= self
.conn_vnet
.virtual_networks
.get(self
.resource_group
, self
.vnet_name
)
142 self
.vnet_id
= vnet
.id
143 except Exception as e
:
144 self
.format_vimconn_exception(e
)
146 def new_network(self
, net_name
, net_type
, ip_profile
=None, shared
=False, vlan
=None):
148 Adds a tenant network to VIM
149 :param net_name: name of the network
151 :param ip_profile: is a dict containing the IP parameters of the network (Currently only IPv4 is implemented)
152 'ip-version': can be one of ['IPv4','IPv6']
153 'subnet-address': ip_prefix_schema, that is X.X.X.X/Y
154 'gateway-address': (Optional) ip_schema, that is X.X.X.X
155 'dns-address': (Optional) ip_schema,
156 'dhcp': (Optional) dict containing
157 'enabled': {'type': 'boolean'},
158 'start-address': ip_schema, first IP to grant
159 'count': number of IPs to grant.
162 :return: a tuple with the network identifier and created_items, or raises an exception on error
163 created_items can be None or a dictionary where this method can include key-values that will be passed to
164 the method delete_network. Can be used to store created segments, created l2gw connections, etc.
165 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
169 return self
._new
_subnet
(net_name
, ip_profile
)
171 def _new_subnet(self
, net_name
, ip_profile
):
173 Adds a tenant network to VIM. It creates a new VNET with a single subnet
178 self
.logger
.debug('Adding a subnet to VNET '+self
.vnet_name
)
179 self
._reload
_connection
()
181 if ip_profile
is None:
182 # get a non used vnet ip range /24 and allocate automatically inside the range self.vnet_address_space
183 used_subnets
= self
.get_network_list()
184 for ip_range
in netaddr
.IPNetwork(self
.vnet_address_space
).subnet(24):
185 for used_subnet
in used_subnets
:
186 subnet_range
= netaddr
.IPNetwork(used_subnet
["cidr_block"])
187 if subnet_range
in ip_range
or ip_range
in subnet_range
:
188 # this range overlaps with an existing subnet ip range. Breaks and look for another
191 ip_profile
= {"subnet_address": str(ip_range
)}
194 vimconn
.vimconnException("Cannot find a non-used subnet range in {}".format(self
.vnet_address_space
))
196 subnet_name
= "{}-{}".format(net_name
[:24], uuid4())
198 'address_prefix': ip_profile
['subnet_address']
200 self
.conn_vnet
.subnets
.create_or_update(self
.resource_group
, self
.vnet_name
, subnet_name
, subnet_params
)
201 return "{}/subnet/{}".format(self
.vnet_id
, subnet_name
), None
202 except Exception as e
:
203 self
.format_vimconn_exception(e
)
205 def _create_nic(self
, subnet_id
, nic_name
, static_ip
=None):
206 self
._reload
_connection
()
208 resource_group_name
=self
._get
_resource
_group
_name
_from
_resource
_id
(subnet_id
)
209 location
= self
._get
_location
_from
_resource
_group
(resource_group_name
)
212 async_nic_creation
= self
.conn_vnet
.network_interfaces
.create_or_update(
216 'location': location
,
217 'ip_configurations': [{
218 'name': nic_name
+ 'ipconfiguration',
219 'privateIPAddress': static_ip
,
220 'privateIPAllocationMethod': 'Static',
228 async_nic_creation
= self
.conn_vnet
.network_interfaces
.create_or_update(
232 'location': location
,
233 'ip_configurations': [{
234 'name': nic_name
+ 'ipconfiguration',
242 return async_nic_creation
.result()
244 def get_image_list(self
, filter_dict
={}):
246 The urn contains for marketplace 'publisher:offer:sku:version'
253 self
._reload
_connection
()
254 if filter_dict
.get("name"):
255 params
= filter_dict
["name"].split(":")
257 publisher
= params
[0]
263 images
= self
.conn_compute
.virtual_machine_images
.list(self
.region
, publisher
, offer
, sku
)
266 image_version
= str(image
.id).split("/")[-1]
267 if image_version
!= version
:
271 'name': self
._get
_resource
_name
_from
_resource
_id
(image
.id)
275 images
= self
.conn_compute
.virtual_machine_images
.list()
278 # TODO implement filter_dict
280 if filter_dict
.get("id") and str(image
.id) != filter_dict
["id"]:
282 if filter_dict
.get("name") and \
283 self
._get
_resource
_name
_from
_resource
_id
(image
.id) != filter_dict
["name"]:
288 'name': self
._get
_resource
_name
_from
_resource
_id
(image
.id),
292 def get_network_list(self
, filter_dict
={}):
293 """Obtain tenant networks of VIM
299 admin_state_up: boolean
301 Returns the network list of dictionaries
303 self
.logger
.debug('Getting all subnets from VIM')
305 self
._reload
_connection
()
306 vnet
= self
.conn_vnet
.virtual_networks
.get(self
.config
["resource_group"], self
.vnet_name
)
309 for subnet
in vnet
.subnets
:
310 # TODO implement filter_dict
312 if filter_dict
.get("id") and str(subnet
.id) != filter_dict
["id"]:
314 if filter_dict
.get("name") and \
315 self
._get
_resource
_name
_from
_resource
_id
(subnet
.id) != filter_dict
["name"]:
319 'id': str(subnet
.id),
320 'name': self
._get
_resource
_name
_from
_resource
_id
(subnet
.id),
321 'status': str(vnet
.provisioning_state
), # TODO Does subnet contains status???
322 'cidr_block': str(subnet
.address_prefix
)
326 except Exception as e
:
327 self
.format_vimconn_exception(e
)
329 def new_vminstance(self
, vm_name
, description
, start
, image_id
, flavor_id
, net_list
, cloud_config
=None,
330 disk_list
=None, availability_zone_index
=None, availability_zone_list
=None):
332 return self
._new
_vminstance
(vm_name
, image_id
, flavor_id
, net_list
)
334 def _new_vminstance(self
, vm_name
, image_id
, flavor_id
, net_list
, cloud_config
=None, disk_list
=None,
335 availability_zone_index
=None, availability_zone_list
=None):
337 self
._check
_subnets
_for
_vm
(net_list
)
339 for idx
, net
in enumerate(net_list
):
340 subnet_id
=net
['subnet_id']
341 nic_name
= vm_name
+ '-nic-'+str(idx
)
342 vm_nic
= self
._create
_nic
(subnet_id
, nic_name
)
343 vm_nics
.append({ 'id': str(vm_nic
.id)})
347 'location': self
.region
,
349 'computer_name': vm_name
, # TODO if vm_name cannot be repeated add uuid4() suffix
350 'admin_username': 'sergio', # TODO is it mandatory???
351 'linuxConfiguration': {
352 'disablePasswordAuthentication': 'true',
356 'path': '/home/sergio/.ssh/authorized_keys',
357 'keyData': self
.pub_key
364 'hardware_profile': {
368 'image_reference': image_id
371 'network_interfaces': [
376 creation_result
= self
.conn_compute
.virtual_machines
.create_or_update(
382 run_command_parameters
= {
383 'command_id': 'RunShellScript', # For linux, don't change it
385 'date > /home/sergio/test.txt'
388 poller
= self
.conn_compute
.virtual_machines
.run_command(
391 run_command_parameters
393 # TODO return a tuple (vm-ID, None)
394 except Exception as e
:
395 self
.format_vimconn_exception(e
)
397 def get_flavor_id_from_data(self
, flavor_dict
):
398 self
.logger
.debug("Getting flavor id from data")
399 self
._reload
_connection
()
400 vm_sizes_list
= [vm_size
.serialize() for vm_size
in self
.conn_compute
.virtual_machine_sizes
.list(self
.region
)]
402 cpus
= flavor_dict
['vcpus']
403 memMB
= flavor_dict
['ram']
405 filteredSizes
= [size
for size
in vm_sizes_list
if size
['numberOfCores'] > cpus
and size
['memoryInMB'] > memMB
]
406 listedFilteredSizes
= sorted(filteredSizes
, key
=lambda k
: k
['numberOfCores'])
408 return listedFilteredSizes
[0]['name']
410 def check_vim_connectivity(self
):
412 self
._reload
_connection
()
414 except Exception as e
:
415 raise vimconn
.vimconnException("Connectivity issue with Azure API: {}".format(e
))
417 def get_network(self
, net_id
):
418 resGroup
= self
._get
_resource
_group
_name
_from
_resource
_id
(net_id
)
419 resName
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
421 self
._reload
_connection
()
422 vnet
= self
.conn_vnet
.virtual_networks
.get(resGroup
, resName
)
425 def delete_network(self
, net_id
):
426 resGroup
= self
._get
_resource
_group
_name
_from
_resource
_id
(net_id
)
427 resName
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
429 self
._reload
_connection
()
430 self
.conn_vnet
.virtual_networks
.delete(resGroup
, resName
)
432 def delete_vminstance(self
, vm_id
):
433 resGroup
= self
._get
_resource
_group
_name
_from
_resource
_id
(net_id
)
434 resName
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
436 self
._reload
_connection
()
437 self
.conn_compute
.virtual_machines
.delete(resGroup
, resName
)
439 def get_vminstance(self
, vm_id
):
440 resGroup
= self
._get
_resource
_group
_name
_from
_resource
_id
(net_id
)
441 resName
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
443 self
._reload
_connection
()
444 vm
=self
.conn_compute
.virtual_machines
.get(resGroup
, resName
)
448 def get_flavor(self
, flavor_id
):
449 self
._reload
_connection
()
450 for vm_size
in self
.conn_compute
.virtual_machine_sizes
.list(self
.region
):
451 if vm_size
.name
== flavor_id
:
454 def refresh_nets_status(self
, net_list
):
456 self
._reload
_connection
()
457 for net_id
in net_list
:
459 resGroup
= self
._get
_resource
_group
_name
_from
_resource
_id
(net_id
)
460 resName
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
462 vnet
= self
.conn_vnet
.virtual_networks
.get(resGroup
, resName
)
464 "status": self
.provision_state2osm
[vnet
.provisioning_state
],
465 "vim_info": str(vnet
)
467 except CloudError
as e
:
468 if e
.error
.error
== "ResourceNotFound":
474 except Exception as e
:
475 # TODO distinguish when it is deleted
477 "status": "VIM_ERROR",
478 "vim_info": str(vnet
),
484 def refresh_vms_status(self
, vm_list
):
486 self
._reload
_connection
()
487 for vm_id
in vm_list
:
489 resGroup
= self
._get
_resource
_group
_name
_from
_resource
_id
(vm_id
)
490 resName
= self
._get
_resource
_name
_from
_resource
_id
(vm_id
)
492 vm
= self
.conn_compute
.virtual_machines
.get(resGroup
, resName
)
494 "status": self
.provision_state2osm
[vm
.provisioning_state
],
497 except CloudError
as e
:
498 if e
.error
.error
== "ResourceNotFound":
504 except Exception as e
:
505 # TODO distinguish when it is deleted
507 "status": "VIM_ERROR",
514 # TODO get_vminstance_console for getting console
516 if __name__
== "__main__":
518 # Making some basic test
521 needed_test_params
= {
522 "client_id": "AZURE_CLIENT_ID",
523 "secret": "AZURE_SECRET",
524 "tenant": "AZURE_TENANT",
525 "resource_group": "AZURE_RESOURCE_GROUP",
526 "subscription_id": "AZURE_SUBSCRIPTION_ID",
527 "vnet_name": "AZURE_VNET_NAME",
531 for param
, env_var
in needed_test_params
.items():
532 value
= getenv(env_var
)
534 raise Exception("Provide a valid value for env '{}'".format(env_var
))
535 test_params
[param
] = value
538 'region_name': getenv("AZURE_REGION_NAME", 'westeurope'),
539 'resource_group': getenv("AZURE_RESOURCE_GROUP"),
540 'subscription_id': getenv("AZURE_SUBSCRIPTION_ID"),
541 'pub_key': getenv("AZURE_PUB_KEY", None),
542 'vnet_name': getenv("AZURE_VNET_NAME", 'myNetwork'),
547 'description': 'new VM',
550 'publisher': 'Canonical',
551 'offer': 'UbuntuServer',
552 'sku': '16.04.0-LTS',
555 'hardware_profile': {
556 'vm_size': 'Standard_DS1_v2'
564 'subnet_address': '10.1.2.0/24',
565 #'subnet_name': 'subnet-oam'
567 ###########################
569 azure
= vimconnector(vim_id
, vim_name
, tenant_id
=test_params
["tenant"], tenant_name
=None, url
=None, url_admin
=None,
570 user
=test_params
["client_id"], passwd
=test_params
["secret"], log_level
=None, config
=config
)
572 # azure.get_flavor_id_from_data("here")
573 # subnets=azure.get_network_list()
574 # azure.new_vminstance(virtualMachine['name'], virtualMachine['description'], virtualMachine['status'],
575 # virtualMachine['image'], virtualMachine['hardware_profile']['vm_size'], subnets)
577 azure
.new_network("mynet", None)
578 net_id
= "/subscriptions/82f80cc1-876b-4591-9911-1fb5788384fd/resourceGroups/osmRG/providers/Microsoft."\
579 "Network/virtualNetworks/test"
580 net_id_not_found
= "/subscriptions/82f80cc1-876b-4591-9911-1fb5788384fd/resourceGroups/osmRG/providers/"\
581 "Microsoft.Network/virtualNetworks/testALF"
582 azure
.refresh_nets_status([net_id
, net_id_not_found
])