blob: c2e7206fe62ae99f46efd8fe1941e376480245bc [file] [log] [blame]
seryio34478552019-05-23 14:50:49 +02001# -*- coding: utf-8 -*-
2
3__author__='Sergio Gonzalez'
4__date__ ='$18-apr-2019 23:59:59$'
5
6import vimconn
7import logging
8
9from os import getenv
10from uuid import uuid4
11
12from azure.common.credentials import ServicePrincipalCredentials
13from azure.mgmt.resource import ResourceManagementClient
14from azure.mgmt.network import NetworkManagementClient
15from azure.mgmt.compute import ComputeManagementClient
16
tierno84efdc12019-05-29 09:29:01 +000017from msrestazure.azure_exceptions import CloudError
tiernodeb74b22019-05-27 10:24:50 +000018
seryio34478552019-05-23 14:50:49 +020019class vimconnector(vimconn.vimconnector):
20
tierno84efdc12019-05-29 09:29:01 +000021 provision_state2osm = {
22 "Deleting": "INACTIVE",
23 "Failed": "ERROR",
24 "Succeeded": "ACTIVE",
25 "Updating": "BUILD",
26 }
27
seryio34478552019-05-23 14:50:49 +020028 def __init__(self, uuid, name, tenant_id, tenant_name, url, url_admin=None, user=None, passwd=None, log_level=None,
29 config={}, persistent_info={}):
30
31 vimconn.vimconnector.__init__(self, uuid, name, tenant_id, tenant_name, url, url_admin, user, passwd, log_level,
32 config, persistent_info)
33
tierno84efdc12019-05-29 09:29:01 +000034 self.vnet_address_space = None
seryio34478552019-05-23 14:50:49 +020035 # LOGGER
36 self.logger = logging.getLogger('openmano.vim.azure')
37 if log_level:
38 logging.basicConfig()
39 self.logger.setLevel(getattr(logging, log_level))
40
41 # CREDENTIALS
42 self.credentials = ServicePrincipalCredentials(
tierno30d0d6d2019-05-27 08:14:01 +000043 client_id=user,
44 secret=passwd,
45 tenant=(tenant_id or tenant_name)
seryio34478552019-05-23 14:50:49 +020046 )
tierno30d0d6d2019-05-27 08:14:01 +000047
48 # SUBSCRIPTION
seryio34478552019-05-23 14:50:49 +020049 if 'subscription_id' in config:
50 self.subscription_id = config.get('subscription_id')
51 self.logger.debug('Setting subscription '+str(self.subscription_id))
52 else:
53 raise vimconn.vimconnException('Subscription not specified')
tierno30d0d6d2019-05-27 08:14:01 +000054 # REGION
seryio34478552019-05-23 14:50:49 +020055 if 'region_name' in config:
56 self.region = config.get('region_name')
57 else:
58 raise vimconn.vimconnException('Azure region_name is not specified at config')
tierno30d0d6d2019-05-27 08:14:01 +000059 # RESOURCE_GROUP
seryio34478552019-05-23 14:50:49 +020060 if 'resource_group' in config:
61 self.resource_group = config.get('resource_group')
62 else:
63 raise vimconn.vimconnException('Azure resource_group is not specified at config')
64 # VNET_NAME
65 if 'vnet_name' in config:
66 self.vnet_name = config["vnet_name"]
67
68 # public ssh key
69 self.pub_key = config.get('pub_key')
70
71 def _reload_connection(self):
tiernodeb74b22019-05-27 10:24:50 +000072 """
73 Sets connections to work with Azure service APIs
74 :return:
75 """
seryio34478552019-05-23 14:50:49 +020076 self.logger.debug('Reloading API Connection')
77 try:
78 self.conn = ResourceManagementClient(self.credentials, self.subscription_id)
79 self.conn_compute = ComputeManagementClient(self.credentials, self.subscription_id)
80 self.conn_vnet = NetworkManagementClient(self.credentials, self.subscription_id)
tiernodeb74b22019-05-27 10:24:50 +000081 self._check_or_create_resource_group()
82 self._check_or_create_vnet()
seryio34478552019-05-23 14:50:49 +020083 except Exception as e:
84 self.format_vimconn_exception(e)
85
86 def _get_resource_name_from_resource_id(self, resource_id):
87 return str(resource_id.split('/')[-1])
88
89 def _get_location_from_resource_group(self, resource_group_name):
90 return self.conn.resource_groups.get(resource_group_name).location
91
92 def _get_resource_group_name_from_resource_id(self, resource_id):
93 return str(resource_id.split('/')[4])
94
95 def _check_subnets_for_vm(self, net_list):
tierno30d0d6d2019-05-27 08:14:01 +000096 # All subnets must belong to the same resource group and vnet
97 if len(set(self._get_resource_group_name_from_resource_id(net['id']) +
98 self._get_resource_name_from_resource_id(net['id']) for net in net_list)) != 1:
seryio34478552019-05-23 14:50:49 +020099 raise self.format_vimconn_exception('Azure VMs can only attach to subnets in same VNET')
100
101 def format_vimconn_exception(self, e):
tiernodeb74b22019-05-27 10:24:50 +0000102 """
103 Params: an Exception object
104 :param e:
105 :return: Raises the proper vimconnException
106 """
seryio34478552019-05-23 14:50:49 +0200107 self.conn = None
108 self.conn_vnet = None
109 raise vimconn.vimconnConnectionException(type(e).__name__ + ': ' + str(e))
110
111 def _check_or_create_resource_group(self):
tiernodeb74b22019-05-27 10:24:50 +0000112 """
113 Creates a resource group in indicated region
114 :return: None
115 """
seryio34478552019-05-23 14:50:49 +0200116 self.logger.debug('Creating RG {} in location {}'.format(self.resource_group, self.region))
tiernodeb74b22019-05-27 10:24:50 +0000117 self.conn.resource_groups.create_or_update(self.resource_group, {'location': self.region})
118
119 def _check_or_create_vnet(self):
120 try:
tierno84efdc12019-05-29 09:29:01 +0000121 vnet = self.conn_vnet.virtual_networks.get(self.resource_group, self.vnet_name)
122 self.vnet_address_space = vnet.address_space.address_prefixes[0]
123 return
124 except CloudError as e:
125 if e.error.error == "ResourceNotFound":
126 pass
127 else:
128 raise
129 # if not exist, creates it
130 try:
tiernodeb74b22019-05-27 10:24:50 +0000131 vnet_params = {
132 'location': self.region,
133 'address_space': {
tierno84efdc12019-05-29 09:29:01 +0000134 'address_prefixes': ["10.0.0.0/8"]
tiernodeb74b22019-05-27 10:24:50 +0000135 },
136 }
tierno84efdc12019-05-29 09:29:01 +0000137 self.vnet_address_space = "10.0.0.0/8"
tiernodeb74b22019-05-27 10:24:50 +0000138 self.conn_vnet.virtual_networks.create_or_update(self.resource_group, self.vnet_name, vnet_params)
139 except Exception as e:
140 self.format_vimconn_exception(e)
seryio34478552019-05-23 14:50:49 +0200141
142 def new_network(self, net_name, net_type, ip_profile=None, shared=False, vlan=None):
tiernodeb74b22019-05-27 10:24:50 +0000143 """
144 Adds a tenant network to VIM
145 :param net_name: name of the network
146 :param net_type:
147 :param ip_profile: is a dict containing the IP parameters of the network (Currently only IPv4 is implemented)
seryio34478552019-05-23 14:50:49 +0200148 'ip-version': can be one of ['IPv4','IPv6']
149 'subnet-address': ip_prefix_schema, that is X.X.X.X/Y
150 'gateway-address': (Optional) ip_schema, that is X.X.X.X
151 'dns-address': (Optional) ip_schema,
152 'dhcp': (Optional) dict containing
153 'enabled': {'type': 'boolean'},
154 'start-address': ip_schema, first IP to grant
155 'count': number of IPs to grant.
tiernodeb74b22019-05-27 10:24:50 +0000156 :param shared:
157 :param vlan:
158 :return: a tuple with the network identifier and created_items, or raises an exception on error
seryio34478552019-05-23 14:50:49 +0200159 created_items can be None or a dictionary where this method can include key-values that will be passed to
160 the method delete_network. Can be used to store created segments, created l2gw connections, etc.
161 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
162 as not present.
tiernodeb74b22019-05-27 10:24:50 +0000163 """
164
seryio34478552019-05-23 14:50:49 +0200165 return self._new_subnet(net_name, ip_profile)
166
167 def _new_subnet(self, net_name, ip_profile):
tiernodeb74b22019-05-27 10:24:50 +0000168 """
169 Adds a tenant network to VIM. It creates a new VNET with a single subnet
170 :param net_name:
171 :param ip_profile:
172 :return:
173 """
seryio34478552019-05-23 14:50:49 +0200174 self.logger.debug('Adding a subnet to VNET '+self.vnet_name)
175 self._reload_connection()
seryio34478552019-05-23 14:50:49 +0200176
177 if ip_profile is None:
tierno84efdc12019-05-29 09:29:01 +0000178 # TODO get a non used vnet ip range /24 and allocate automatically inside the range self.vnet_address_space
179 # use netaddr library
seryio34478552019-05-23 14:50:49 +0200180 raise vimconn.vimconnException('Azure cannot create VNET with no CIDR')
181
182 try:
183 vnet_params= {
184 'location': self.region,
seryio34478552019-05-23 14:50:49 +0200185 'subnets': [
186 {
187 'name': "{}-{}".format(net_name[:24], uuid4()),
188 'address_prefix': ip_profile['subnet_address']
189 }
190 ]
191 }
192 self.conn_vnet.virtual_networks.create_or_update(self.resource_group, self.vnet_name, vnet_params)
193 # TODO return a tuple (subnet-ID, None)
194 except Exception as e:
195 self.format_vimconn_exception(e)
196
197 def _create_nic(self, subnet_id, nic_name, static_ip=None):
198 self._reload_connection()
199
200 resource_group_name=self._get_resource_group_name_from_resource_id(subnet_id)
201 location = self._get_location_from_resource_group(resource_group_name)
202
203 if static_ip:
204 async_nic_creation = self.conn_vnet.network_interfaces.create_or_update(
205 resource_group_name,
206 nic_name,
207 {
208 'location': location,
209 'ip_configurations': [{
210 'name': nic_name + 'ipconfiguration',
211 'privateIPAddress': static_ip,
212 'privateIPAllocationMethod': 'Static',
213 'subnet': {
214 'id': subnet_id
215 }
216 }]
217 }
218 )
219 else:
220 async_nic_creation = self.conn_vnet.network_interfaces.create_or_update(
221 resource_group_name,
222 nic_name,
223 {
224 'location': location,
225 'ip_configurations': [{
226 'name': nic_name + 'ipconfiguration',
227 'subnet': {
228 'id': subnet_id
229 }
230 }]
231 }
232 )
233
234 return async_nic_creation.result()
235
tiernodeb74b22019-05-27 10:24:50 +0000236 def get_image_list(self, filter_dict={}):
237 """
238 The urn contains for marketplace 'publisher:offer:sku:version'
239
240 :param filter_dict:
241 :return:
242 """
243 image_list = []
244
245 self._reload_connection()
246 if filter_dict.get("name"):
247 params = filter_dict["name"].split(":")
248 if len(params) >= 3:
249 publisher = params[0]
250 offer = params[1]
251 sku = params[2]
252 version = None
253 if len(params) == 4:
254 version = params[3]
255 images = self.conn_compute.virtual_machine_images.list(self.region, publisher, offer, sku)
256 for image in images:
257 if version:
258 image_version = str(image.id).split("/")[-1]
259 if image_version != version:
260 continue
261 image_list.append({
262 'id': str(image.id),
263 'name': self._get_resource_name_from_resource_id(image.id)
264 })
265 return image_list
266
267 images = self.conn_compute.virtual_machine_images.list()
268
269 for image in images:
270 # TODO implement filter_dict
271 if filter_dict:
272 if filter_dict.get("id") and str(image.id) != filter_dict["id"]:
273 continue
274 if filter_dict.get("name") and \
275 self._get_resource_name_from_resource_id(image.id) != filter_dict["name"]:
276 continue
277 # TODO add checksum
278 image_list.append({
279 'id': str(image.id),
280 'name': self._get_resource_name_from_resource_id(image.id),
281 })
282 return image_list
283
seryio34478552019-05-23 14:50:49 +0200284 def get_network_list(self, filter_dict={}):
tiernodeb74b22019-05-27 10:24:50 +0000285 """Obtain tenant networks of VIM
seryio34478552019-05-23 14:50:49 +0200286 Filter_dict can be:
287 name: network name
288 id: network uuid
289 shared: boolean
290 tenant_id: tenant
291 admin_state_up: boolean
292 status: 'ACTIVE'
293 Returns the network list of dictionaries
tiernodeb74b22019-05-27 10:24:50 +0000294 """
seryio34478552019-05-23 14:50:49 +0200295 self.logger.debug('Getting all subnets from VIM')
296 try:
297 self._reload_connection()
298 vnet = self.conn_vnet.virtual_networks.get(self.config["resource_group"], self.vnet_name)
299 subnet_list = []
300
301 for subnet in vnet.subnets:
302 # TODO implement filter_dict
303 if filter_dict:
304 if filter_dict.get("id") and str(subnet.id) != filter_dict["id"]:
305 continue
306 if filter_dict.get("name") and \
307 self._get_resource_name_from_resource_id(subnet.id) != filter_dict["name"]:
308 continue
309
310 subnet_list.append({
311 'id': str(subnet.id),
312 'name': self._get_resource_name_from_resource_id(subnet.id),
313 'status': str(vnet.provisioning_state), # TODO Does subnet contains status???
314 'cidr_block': str(subnet.address_prefix)
315 }
316 )
317 return subnet_list
318 except Exception as e:
319 self.format_vimconn_exception(e)
320
321 def new_vminstance(self, vm_name, description, start, image_id, flavor_id, net_list, cloud_config=None,
322 disk_list=None, availability_zone_index=None, availability_zone_list=None):
323
324 return self._new_vminstance(vm_name, image_id, flavor_id, net_list)
325
tierno30d0d6d2019-05-27 08:14:01 +0000326 def _new_vminstance(self, vm_name, image_id, flavor_id, net_list, cloud_config=None, disk_list=None,
327 availability_zone_index=None, availability_zone_list=None):
seryio34478552019-05-23 14:50:49 +0200328 #Create NICs
329 self._check_subnets_for_vm(net_list)
330 vm_nics = []
331 for idx, net in enumerate(net_list):
332 subnet_id=net['subnet_id']
333 nic_name = vm_name + '-nic-'+str(idx)
334 vm_nic = self._create_nic(subnet_id, nic_name)
335 vm_nics.append({ 'id': str(vm_nic.id)})
336
337 try:
338 vm_parameters = {
339 'location': self.region,
340 'os_profile': {
341 'computer_name': vm_name, # TODO if vm_name cannot be repeated add uuid4() suffix
342 'admin_username': 'sergio', # TODO is it mandatory???
343 'linuxConfiguration': {
344 'disablePasswordAuthentication': 'true',
345 'ssh': {
346 'publicKeys': [
347 {
348 'path': '/home/sergio/.ssh/authorized_keys',
349 'keyData': self.pub_key
350 }
351 ]
352 }
353 }
354
355 },
356 'hardware_profile': {
357 'vm_size':flavor_id
358 },
359 'storage_profile': {
360 'image_reference': image_id
361 },
362 'network_profile': {
363 'network_interfaces': [
364 vm_nics[0]
365 ]
366 }
367 }
368 creation_result = self.conn_compute.virtual_machines.create_or_update(
369 self.resource_group,
370 vm_name,
371 vm_parameters
372 )
373
374 run_command_parameters = {
375 'command_id': 'RunShellScript', # For linux, don't change it
376 'script': [
377 'date > /home/sergio/test.txt'
378 ]
379 }
380 poller = self.conn_compute.virtual_machines.run_command(
381 self.resource_group,
382 vm_name,
383 run_command_parameters
384 )
385 # TODO return a tuple (vm-ID, None)
386 except Exception as e:
387 self.format_vimconn_exception(e)
388
389 def get_flavor_id_from_data(self, flavor_dict):
tierno30d0d6d2019-05-27 08:14:01 +0000390 self.logger.debug("Getting flavor id from data")
391 self._reload_connection()
392 vm_sizes_list = [vm_size.serialize() for vm_size in self.conn_compute.virtual_machine_sizes.list(self.region)]
seryio34478552019-05-23 14:50:49 +0200393
tierno30d0d6d2019-05-27 08:14:01 +0000394 cpus = flavor_dict['vcpus']
395 memMB = flavor_dict['ram']
396
397 filteredSizes = [size for size in vm_sizes_list if size['numberOfCores'] > cpus and size['memoryInMB'] > memMB]
398 listedFilteredSizes = sorted(filteredSizes, key=lambda k: k['numberOfCores'])
399
400 return listedFilteredSizes[0]['name']
401
402 def check_vim_connectivity(self):
seryio34478552019-05-23 14:50:49 +0200403 try:
404 self._reload_connection()
tierno30d0d6d2019-05-27 08:14:01 +0000405 return True
406 except Exception as e:
407 raise vimconn.vimconnException("Connectivity issue with Azure API: {}".format(e))
seryio34478552019-05-23 14:50:49 +0200408
409 def get_network(self, net_id):
tierno30d0d6d2019-05-27 08:14:01 +0000410 resGroup = self._get_resource_group_name_from_resource_id(net_id)
seryio34478552019-05-23 14:50:49 +0200411 resName = self._get_resource_name_from_resource_id(net_id)
412
413 self._reload_connection()
414 vnet = self.conn_vnet.virtual_networks.get(resGroup, resName)
415
416 return vnet
417
418 def delete_network(self, net_id):
tierno30d0d6d2019-05-27 08:14:01 +0000419 resGroup = self._get_resource_group_name_from_resource_id(net_id)
seryio34478552019-05-23 14:50:49 +0200420 resName = self._get_resource_name_from_resource_id(net_id)
421
422 self._reload_connection()
423 self.conn_vnet.virtual_networks.delete(resGroup, resName)
424
425 def delete_vminstance(self, vm_id):
tierno30d0d6d2019-05-27 08:14:01 +0000426 resGroup = self._get_resource_group_name_from_resource_id(net_id)
seryio34478552019-05-23 14:50:49 +0200427 resName = self._get_resource_name_from_resource_id(net_id)
428
429 self._reload_connection()
430 self.conn_compute.virtual_machines.delete(resGroup, resName)
431
432 def get_vminstance(self, vm_id):
tierno30d0d6d2019-05-27 08:14:01 +0000433 resGroup = self._get_resource_group_name_from_resource_id(net_id)
seryio34478552019-05-23 14:50:49 +0200434 resName = self._get_resource_name_from_resource_id(net_id)
435
436 self._reload_connection()
437 vm=self.conn_compute.virtual_machines.get(resGroup, resName)
438
439 return vm
440
441 def get_flavor(self, flavor_id):
442 self._reload_connection()
443 for vm_size in self.conn_compute.virtual_machine_sizes.list(self.region):
444 if vm_size.name == flavor_id :
445 return vm_size
446
tierno84efdc12019-05-29 09:29:01 +0000447 def refresh_nets_status(self, net_list):
448 out_nets = {}
449 self._reload_connection()
450 for net_id in net_list:
451 try:
452 resGroup = self._get_resource_group_name_from_resource_id(net_id)
453 resName = self._get_resource_name_from_resource_id(net_id)
seryio34478552019-05-23 14:50:49 +0200454
tierno84efdc12019-05-29 09:29:01 +0000455 vnet = self.conn_vnet.virtual_networks.get(resGroup, resName)
456 out_nets[net_id] ={
457 "status": self.provision_state2osm[vnet.provisioning_state],
458 "vim_info": str(vnet)
459 }
460 except CloudError as e:
461 if e.error.error == "ResourceNotFound":
462 out_nets[net_id] = {
463 "status": "DELETED",
464 }
465 else:
466 raise
467 except Exception as e:
468 # TODO distinguish when it is deleted
469 out_nets[net_id] = {
470 "status": "VIM_ERROR",
471 "vim_info": str(vnet),
472 "error_msg": str(e)
473 }
474
475 return out_nets
476
477 def refresh_vms_status(self, vm_list):
478 out_vms = {}
479 self._reload_connection()
480 for vm_id in vm_list:
481 try:
482 resGroup = self._get_resource_group_name_from_resource_id(vm_id)
483 resName = self._get_resource_name_from_resource_id(vm_id)
484
485 vm = self.conn_compute.virtual_machines.get(resGroup, resName)
486 out_vms[vm_id] ={
487 "status": self.provision_state2osm[vm.provisioning_state],
488 "vim_info": str(vm)
489 }
490 except CloudError as e:
491 if e.error.error == "ResourceNotFound":
492 out_vms[vm_id] = {
493 "status": "DELETED",
494 }
495 else:
496 raise
497 except Exception as e:
498 # TODO distinguish when it is deleted
499 out_vms[vm_id] = {
500 "status": "VIM_ERROR",
501 "vim_info": str(vm),
502 "error_msg": str(e)
503 }
504
505 return out_vms
seryio07ad1362019-05-29 09:16:24 +0200506
seryio34478552019-05-23 14:50:49 +0200507# TODO get_vminstance_console for getting console
508
509if __name__ == "__main__":
510
511 # Making some basic test
512 vim_id='azure'
513 vim_name='azure'
514 needed_test_params = {
tiernodeb74b22019-05-27 10:24:50 +0000515 "client_id": "AZURE_CLIENT_ID",
516 "secret": "AZURE_SECRET",
517 "tenant": "AZURE_TENANT",
518 "resource_group": "AZURE_RESOURCE_GROUP",
519 "subscription_id": "AZURE_SUBSCRIPTION_ID",
tierno30d0d6d2019-05-27 08:14:01 +0000520 "vnet_name": "AZURE_VNET_NAME",
seryio34478552019-05-23 14:50:49 +0200521 }
522 test_params = {}
523
524 for param, env_var in needed_test_params.items():
525 value = getenv(env_var)
526 if not value:
527 raise Exception("Provide a valid value for env '{}'".format(env_var))
528 test_params[param] = value
529
530 config = {
531 'region_name': getenv("AZURE_REGION_NAME", 'westeurope'),
532 'resource_group': getenv("AZURE_RESOURCE_GROUP"),
533 'subscription_id': getenv("AZURE_SUBSCRIPTION_ID"),
tierno30d0d6d2019-05-27 08:14:01 +0000534 'pub_key': getenv("AZURE_PUB_KEY", None),
seryio34478552019-05-23 14:50:49 +0200535 'vnet_name': getenv("AZURE_VNET_NAME", 'myNetwork'),
536 }
tierno30d0d6d2019-05-27 08:14:01 +0000537
seryio34478552019-05-23 14:50:49 +0200538 virtualMachine = {
tierno30d0d6d2019-05-27 08:14:01 +0000539 'name': 'sergio',
540 'description': 'new VM',
seryio34478552019-05-23 14:50:49 +0200541 'status': 'running',
542 'image': {
543 'publisher': 'Canonical',
544 'offer': 'UbuntuServer',
545 'sku': '16.04.0-LTS',
546 'version': 'latest'
547 },
548 'hardware_profile': {
549 'vm_size': 'Standard_DS1_v2'
550 },
551 'networks': [
552 'sergio'
553 ]
554 }
555
556 vnet_config = {
557 'subnet_address': '10.1.2.0/24',
558 #'subnet_name': 'subnet-oam'
559 }
560 ###########################
561
tierno30d0d6d2019-05-27 08:14:01 +0000562 azure = vimconnector(vim_id, vim_name, tenant_id=test_params["tenant"], tenant_name=None, url=None, url_admin=None,
563 user=test_params["client_id"], passwd=test_params["secret"], log_level=None, config=config)
seryio34478552019-05-23 14:50:49 +0200564
tiernodeb74b22019-05-27 10:24:50 +0000565 # azure.get_flavor_id_from_data("here")
566 # subnets=azure.get_network_list()
567 # azure.new_vminstance(virtualMachine['name'], virtualMachine['description'], virtualMachine['status'],
568 # virtualMachine['image'], virtualMachine['hardware_profile']['vm_size'], subnets)
569
tierno84efdc12019-05-29 09:29:01 +0000570 net_id = "/subscriptions/82f80cc1-876b-4591-9911-1fb5788384fd/resourceGroups/osmRG/providers/Microsoft."\
571 "Network/virtualNetworks/test"
572 net_id_not_found = "/subscriptions/82f80cc1-876b-4591-9911-1fb5788384fd/resourceGroups/osmRG/providers/"\
573 "Microsoft.Network/virtualNetworks/testALF"
574 azure.refresh_nets_status([net_id, net_id_not_found])