cdc17916b6f7102b5698e787647bfdffd59c62f1
[osm/RO.git] / osm_ro / vimconn_azure.py
1 # -*- coding: utf-8 -*-
2
3 __author__='Sergio Gonzalez'
4 __date__ ='$18-apr-2019 23:59:59$'
5
6 import vimconn
7 import logging
8
9 from os import getenv
10 from uuid import uuid4
11
12 from azure.common.credentials import ServicePrincipalCredentials
13 from azure.mgmt.resource import ResourceManagementClient
14 from azure.mgmt.network import NetworkManagementClient
15 from azure.mgmt.compute import ComputeManagementClient
16
17
18 class vimconnector(vimconn.vimconnector):
19
20 def __init__(self, uuid, name, tenant_id, tenant_name, url, url_admin=None, user=None, passwd=None, log_level=None,
21 config={}, persistent_info={}):
22
23 vimconn.vimconnector.__init__(self, uuid, name, tenant_id, tenant_name, url, url_admin, user, passwd, log_level,
24 config, persistent_info)
25
26 # LOGGER
27 self.logger = logging.getLogger('openmano.vim.azure')
28 if log_level:
29 logging.basicConfig()
30 self.logger.setLevel(getattr(logging, log_level))
31
32 # CREDENTIALS
33 self.credentials = ServicePrincipalCredentials(
34 client_id=user,
35 secret=passwd,
36 tenant=(tenant_id or tenant_name)
37 )
38
39 # SUBSCRIPTION
40 if 'subscription_id' in config:
41 self.subscription_id = config.get('subscription_id')
42 self.logger.debug('Setting subscription '+str(self.subscription_id))
43 else:
44 raise vimconn.vimconnException('Subscription not specified')
45 # REGION
46 if 'region_name' in config:
47 self.region = config.get('region_name')
48 else:
49 raise vimconn.vimconnException('Azure region_name is not specified at config')
50 # RESOURCE_GROUP
51 if 'resource_group' in config:
52 self.resource_group = config.get('resource_group')
53 else:
54 raise vimconn.vimconnException('Azure resource_group is not specified at config')
55 # VNET_NAME
56 if 'vnet_name' in config:
57 self.vnet_name = config["vnet_name"]
58
59 # public ssh key
60 self.pub_key = config.get('pub_key')
61
62 def _reload_connection(self):
63 """
64 Sets connections to work with Azure service APIs
65 :return:
66 """
67 self.logger.debug('Reloading API Connection')
68 try:
69 self.conn = ResourceManagementClient(self.credentials, self.subscription_id)
70 self.conn_compute = ComputeManagementClient(self.credentials, self.subscription_id)
71 self.conn_vnet = NetworkManagementClient(self.credentials, self.subscription_id)
72 self._check_or_create_resource_group()
73 self._check_or_create_vnet()
74 except Exception as e:
75 self.format_vimconn_exception(e)
76
77 def _get_resource_name_from_resource_id(self, resource_id):
78 return str(resource_id.split('/')[-1])
79
80 def _get_location_from_resource_group(self, resource_group_name):
81 return self.conn.resource_groups.get(resource_group_name).location
82
83 def _get_resource_group_name_from_resource_id(self, resource_id):
84 return str(resource_id.split('/')[4])
85
86 def _check_subnets_for_vm(self, net_list):
87 # All subnets must belong to the same resource group and vnet
88 if len(set(self._get_resource_group_name_from_resource_id(net['id']) +
89 self._get_resource_name_from_resource_id(net['id']) for net in net_list)) != 1:
90 raise self.format_vimconn_exception('Azure VMs can only attach to subnets in same VNET')
91
92 def format_vimconn_exception(self, e):
93 """
94 Params: an Exception object
95 :param e:
96 :return: Raises the proper vimconnException
97 """
98 self.conn = None
99 self.conn_vnet = None
100 raise vimconn.vimconnConnectionException(type(e).__name__ + ': ' + str(e))
101
102 def _check_or_create_resource_group(self):
103 """
104 Creates a resource group in indicated region
105 :return: None
106 """
107 self.logger.debug('Creating RG {} in location {}'.format(self.resource_group, self.region))
108 self.conn.resource_groups.create_or_update(self.resource_group, {'location': self.region})
109
110 def _check_or_create_vnet(self):
111 try:
112 vnet_params = {
113 'location': self.region,
114 'address_space': {
115 'address_prefixes': "10.0.0.0/8"
116 },
117 }
118 self.conn_vnet.virtual_networks.create_or_update(self.resource_group, self.vnet_name, vnet_params)
119 except Exception as e:
120 self.format_vimconn_exception(e)
121
122 def new_network(self, net_name, net_type, ip_profile=None, shared=False, vlan=None):
123 """
124 Adds a tenant network to VIM
125 :param net_name: name of the network
126 :param net_type:
127 :param ip_profile: is a dict containing the IP parameters of the network (Currently only IPv4 is implemented)
128 'ip-version': can be one of ['IPv4','IPv6']
129 'subnet-address': ip_prefix_schema, that is X.X.X.X/Y
130 'gateway-address': (Optional) ip_schema, that is X.X.X.X
131 'dns-address': (Optional) ip_schema,
132 'dhcp': (Optional) dict containing
133 'enabled': {'type': 'boolean'},
134 'start-address': ip_schema, first IP to grant
135 'count': number of IPs to grant.
136 :param shared:
137 :param vlan:
138 :return: a tuple with the network identifier and created_items, or raises an exception on error
139 created_items can be None or a dictionary where this method can include key-values that will be passed to
140 the method delete_network. Can be used to store created segments, created l2gw connections, etc.
141 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
142 as not present.
143 """
144
145 return self._new_subnet(net_name, ip_profile)
146
147 def _new_subnet(self, net_name, ip_profile):
148 """
149 Adds a tenant network to VIM. It creates a new VNET with a single subnet
150 :param net_name:
151 :param ip_profile:
152 :return:
153 """
154 self.logger.debug('Adding a subnet to VNET '+self.vnet_name)
155 self._reload_connection()
156
157 if ip_profile is None:
158 # TODO get a non used vnet ip range /24 and allocate automatically
159 raise vimconn.vimconnException('Azure cannot create VNET with no CIDR')
160
161 try:
162 vnet_params= {
163 'location': self.region,
164 'address_space': {
165 'address_prefixes': [ip_profile['subnet_address']]
166 },
167 'subnets': [
168 {
169 'name': "{}-{}".format(net_name[:24], uuid4()),
170 'address_prefix': ip_profile['subnet_address']
171 }
172 ]
173 }
174 self.conn_vnet.virtual_networks.create_or_update(self.resource_group, self.vnet_name, vnet_params)
175 # TODO return a tuple (subnet-ID, None)
176 except Exception as e:
177 self.format_vimconn_exception(e)
178
179 def _create_nic(self, subnet_id, nic_name, static_ip=None):
180 self._reload_connection()
181
182 resource_group_name=self._get_resource_group_name_from_resource_id(subnet_id)
183 location = self._get_location_from_resource_group(resource_group_name)
184
185 if static_ip:
186 async_nic_creation = self.conn_vnet.network_interfaces.create_or_update(
187 resource_group_name,
188 nic_name,
189 {
190 'location': location,
191 'ip_configurations': [{
192 'name': nic_name + 'ipconfiguration',
193 'privateIPAddress': static_ip,
194 'privateIPAllocationMethod': 'Static',
195 'subnet': {
196 'id': subnet_id
197 }
198 }]
199 }
200 )
201 else:
202 async_nic_creation = self.conn_vnet.network_interfaces.create_or_update(
203 resource_group_name,
204 nic_name,
205 {
206 'location': location,
207 'ip_configurations': [{
208 'name': nic_name + 'ipconfiguration',
209 'subnet': {
210 'id': subnet_id
211 }
212 }]
213 }
214 )
215
216 return async_nic_creation.result()
217
218 def get_image_list(self, filter_dict={}):
219 """
220 The urn contains for marketplace 'publisher:offer:sku:version'
221
222 :param filter_dict:
223 :return:
224 """
225 image_list = []
226
227 self._reload_connection()
228 if filter_dict.get("name"):
229 params = filter_dict["name"].split(":")
230 if len(params) >= 3:
231 publisher = params[0]
232 offer = params[1]
233 sku = params[2]
234 version = None
235 if len(params) == 4:
236 version = params[3]
237 images = self.conn_compute.virtual_machine_images.list(self.region, publisher, offer, sku)
238 for image in images:
239 if version:
240 image_version = str(image.id).split("/")[-1]
241 if image_version != version:
242 continue
243 image_list.append({
244 'id': str(image.id),
245 'name': self._get_resource_name_from_resource_id(image.id)
246 })
247 return image_list
248
249 images = self.conn_compute.virtual_machine_images.list()
250
251 for image in images:
252 # TODO implement filter_dict
253 if filter_dict:
254 if filter_dict.get("id") and str(image.id) != filter_dict["id"]:
255 continue
256 if filter_dict.get("name") and \
257 self._get_resource_name_from_resource_id(image.id) != filter_dict["name"]:
258 continue
259 # TODO add checksum
260 image_list.append({
261 'id': str(image.id),
262 'name': self._get_resource_name_from_resource_id(image.id),
263 })
264 return image_list
265
266 def get_network_list(self, filter_dict={}):
267 """Obtain tenant networks of VIM
268 Filter_dict can be:
269 name: network name
270 id: network uuid
271 shared: boolean
272 tenant_id: tenant
273 admin_state_up: boolean
274 status: 'ACTIVE'
275 Returns the network list of dictionaries
276 """
277 self.logger.debug('Getting all subnets from VIM')
278 try:
279 self._reload_connection()
280 vnet = self.conn_vnet.virtual_networks.get(self.config["resource_group"], self.vnet_name)
281 subnet_list = []
282
283 for subnet in vnet.subnets:
284 # TODO implement filter_dict
285 if filter_dict:
286 if filter_dict.get("id") and str(subnet.id) != filter_dict["id"]:
287 continue
288 if filter_dict.get("name") and \
289 self._get_resource_name_from_resource_id(subnet.id) != filter_dict["name"]:
290 continue
291
292 subnet_list.append({
293 'id': str(subnet.id),
294 'name': self._get_resource_name_from_resource_id(subnet.id),
295 'status': str(vnet.provisioning_state), # TODO Does subnet contains status???
296 'cidr_block': str(subnet.address_prefix)
297 }
298 )
299 return subnet_list
300 except Exception as e:
301 self.format_vimconn_exception(e)
302
303 def new_vminstance(self, vm_name, description, start, image_id, flavor_id, net_list, cloud_config=None,
304 disk_list=None, availability_zone_index=None, availability_zone_list=None):
305
306 return self._new_vminstance(vm_name, image_id, flavor_id, net_list)
307
308 def _new_vminstance(self, vm_name, image_id, flavor_id, net_list, cloud_config=None, disk_list=None,
309 availability_zone_index=None, availability_zone_list=None):
310 #Create NICs
311 self._check_subnets_for_vm(net_list)
312 vm_nics = []
313 for idx, net in enumerate(net_list):
314 subnet_id=net['subnet_id']
315 nic_name = vm_name + '-nic-'+str(idx)
316 vm_nic = self._create_nic(subnet_id, nic_name)
317 vm_nics.append({ 'id': str(vm_nic.id)})
318
319 try:
320 vm_parameters = {
321 'location': self.region,
322 'os_profile': {
323 'computer_name': vm_name, # TODO if vm_name cannot be repeated add uuid4() suffix
324 'admin_username': 'sergio', # TODO is it mandatory???
325 'linuxConfiguration': {
326 'disablePasswordAuthentication': 'true',
327 'ssh': {
328 'publicKeys': [
329 {
330 'path': '/home/sergio/.ssh/authorized_keys',
331 'keyData': self.pub_key
332 }
333 ]
334 }
335 }
336
337 },
338 'hardware_profile': {
339 'vm_size':flavor_id
340 },
341 'storage_profile': {
342 'image_reference': image_id
343 },
344 'network_profile': {
345 'network_interfaces': [
346 vm_nics[0]
347 ]
348 }
349 }
350 creation_result = self.conn_compute.virtual_machines.create_or_update(
351 self.resource_group,
352 vm_name,
353 vm_parameters
354 )
355
356 run_command_parameters = {
357 'command_id': 'RunShellScript', # For linux, don't change it
358 'script': [
359 'date > /home/sergio/test.txt'
360 ]
361 }
362 poller = self.conn_compute.virtual_machines.run_command(
363 self.resource_group,
364 vm_name,
365 run_command_parameters
366 )
367 # TODO return a tuple (vm-ID, None)
368 except Exception as e:
369 self.format_vimconn_exception(e)
370
371 def get_flavor_id_from_data(self, flavor_dict):
372 self.logger.debug("Getting flavor id from data")
373 self._reload_connection()
374 vm_sizes_list = [vm_size.serialize() for vm_size in self.conn_compute.virtual_machine_sizes.list(self.region)]
375
376 cpus = flavor_dict['vcpus']
377 memMB = flavor_dict['ram']
378
379 filteredSizes = [size for size in vm_sizes_list if size['numberOfCores'] > cpus and size['memoryInMB'] > memMB]
380 listedFilteredSizes = sorted(filteredSizes, key=lambda k: k['numberOfCores'])
381
382 return listedFilteredSizes[0]['name']
383
384 def check_vim_connectivity(self):
385 try:
386 self._reload_connection()
387 return True
388 except Exception as e:
389 raise vimconn.vimconnException("Connectivity issue with Azure API: {}".format(e))
390
391 def get_network(self, net_id):
392 resGroup = self._get_resource_group_name_from_resource_id(net_id)
393 resName = self._get_resource_name_from_resource_id(net_id)
394
395 self._reload_connection()
396 vnet = self.conn_vnet.virtual_networks.get(resGroup, resName)
397
398 return vnet
399
400 def delete_network(self, net_id):
401 resGroup = self._get_resource_group_name_from_resource_id(net_id)
402 resName = self._get_resource_name_from_resource_id(net_id)
403
404 self._reload_connection()
405 self.conn_vnet.virtual_networks.delete(resGroup, resName)
406
407 def delete_vminstance(self, vm_id):
408 resGroup = self._get_resource_group_name_from_resource_id(net_id)
409 resName = self._get_resource_name_from_resource_id(net_id)
410
411 self._reload_connection()
412 self.conn_compute.virtual_machines.delete(resGroup, resName)
413
414 def get_vminstance(self, vm_id):
415 resGroup = self._get_resource_group_name_from_resource_id(net_id)
416 resName = self._get_resource_name_from_resource_id(net_id)
417
418 self._reload_connection()
419 vm=self.conn_compute.virtual_machines.get(resGroup, resName)
420
421 return vm
422
423 def get_flavor(self, flavor_id):
424 self._reload_connection()
425 for vm_size in self.conn_compute.virtual_machine_sizes.list(self.region):
426 if vm_size.name == flavor_id :
427 return vm_size
428
429 def refresh_nets_status(self, net_id):
430 resGroup = self._get_resource_group_name_from_resource_id(net_id)
431 resName = self._get_resource_name_from_resource_id(net_id)
432
433 self._reload_connection()
434 vnet = self.conn_vnet.virtual_networks.get(resGroup, resName)
435
436 return vnet
437
438 def refresh_vms_status(self, vm_id):
439 resGroup = self._get_resource_group_name_from_resource_id(net_id)
440 resName = self._get_resource_name_from_resource_id(net_id)
441
442 self._reload_connection()
443 vm=self.conn_compute.virtual_machines.get(resGroup, resName)
444
445 return vm
446
447 # TODO get_vminstance_console for getting console
448
449 if __name__ == "__main__":
450
451 # Making some basic test
452 vim_id='azure'
453 vim_name='azure'
454 needed_test_params = {
455 "client_id": "AZURE_CLIENT_ID",
456 "secret": "AZURE_SECRET",
457 "tenant": "AZURE_TENANT",
458 "resource_group": "AZURE_RESOURCE_GROUP",
459 "subscription_id": "AZURE_SUBSCRIPTION_ID",
460 "vnet_name": "AZURE_VNET_NAME",
461 }
462 test_params = {}
463
464 for param, env_var in needed_test_params.items():
465 value = getenv(env_var)
466 if not value:
467 raise Exception("Provide a valid value for env '{}'".format(env_var))
468 test_params[param] = value
469
470 config = {
471 'region_name': getenv("AZURE_REGION_NAME", 'westeurope'),
472 'resource_group': getenv("AZURE_RESOURCE_GROUP"),
473 'subscription_id': getenv("AZURE_SUBSCRIPTION_ID"),
474 'pub_key': getenv("AZURE_PUB_KEY", None),
475 'vnet_name': getenv("AZURE_VNET_NAME", 'myNetwork'),
476 }
477
478 virtualMachine = {
479 'name': 'sergio',
480 'description': 'new VM',
481 'status': 'running',
482 'image': {
483 'publisher': 'Canonical',
484 'offer': 'UbuntuServer',
485 'sku': '16.04.0-LTS',
486 'version': 'latest'
487 },
488 'hardware_profile': {
489 'vm_size': 'Standard_DS1_v2'
490 },
491 'networks': [
492 'sergio'
493 ]
494 }
495
496 vnet_config = {
497 'subnet_address': '10.1.2.0/24',
498 #'subnet_name': 'subnet-oam'
499 }
500 ###########################
501
502 azure = vimconnector(vim_id, vim_name, tenant_id=test_params["tenant"], tenant_name=None, url=None, url_admin=None,
503 user=test_params["client_id"], passwd=test_params["secret"], log_level=None, config=config)
504
505 # azure.get_flavor_id_from_data("here")
506 # subnets=azure.get_network_list()
507 # azure.new_vminstance(virtualMachine['name'], virtualMachine['description'], virtualMachine['status'],
508 # virtualMachine['image'], virtualMachine['hardware_profile']['vm_size'], subnets)
509
510 azure.refresh_nets_status("/subscriptions/82f80cc1-876b-4591-9911-1fb5788384fd/resourceGroups/osmRG/providers/Microsoft.Network/virtualNetworks/test")