blob: 24a9878d5c58ea93c0c585f47a7c5498125ce504 [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
tiernodeb74b22019-05-27 10:24:50 +000017
seryio34478552019-05-23 14:50:49 +020018class 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(
tierno30d0d6d2019-05-27 08:14:01 +000034 client_id=user,
35 secret=passwd,
36 tenant=(tenant_id or tenant_name)
seryio34478552019-05-23 14:50:49 +020037 )
tierno30d0d6d2019-05-27 08:14:01 +000038
39 # SUBSCRIPTION
seryio34478552019-05-23 14:50:49 +020040 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')
tierno30d0d6d2019-05-27 08:14:01 +000045 # REGION
seryio34478552019-05-23 14:50:49 +020046 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')
tierno30d0d6d2019-05-27 08:14:01 +000050 # RESOURCE_GROUP
seryio34478552019-05-23 14:50:49 +020051 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):
tiernodeb74b22019-05-27 10:24:50 +000063 """
64 Sets connections to work with Azure service APIs
65 :return:
66 """
seryio34478552019-05-23 14:50:49 +020067 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)
tiernodeb74b22019-05-27 10:24:50 +000072 self._check_or_create_resource_group()
73 self._check_or_create_vnet()
seryio34478552019-05-23 14:50:49 +020074 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):
tierno30d0d6d2019-05-27 08:14:01 +000087 # 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:
seryio34478552019-05-23 14:50:49 +020090 raise self.format_vimconn_exception('Azure VMs can only attach to subnets in same VNET')
91
92 def format_vimconn_exception(self, e):
tiernodeb74b22019-05-27 10:24:50 +000093 """
94 Params: an Exception object
95 :param e:
96 :return: Raises the proper vimconnException
97 """
seryio34478552019-05-23 14:50:49 +020098 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):
tiernodeb74b22019-05-27 10:24:50 +0000103 """
104 Creates a resource group in indicated region
105 :return: None
106 """
seryio34478552019-05-23 14:50:49 +0200107 self.logger.debug('Creating RG {} in location {}'.format(self.resource_group, self.region))
tiernodeb74b22019-05-27 10:24:50 +0000108 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)
seryio34478552019-05-23 14:50:49 +0200121
122 def new_network(self, net_name, net_type, ip_profile=None, shared=False, vlan=None):
tiernodeb74b22019-05-27 10:24:50 +0000123 """
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)
seryio34478552019-05-23 14:50:49 +0200128 '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.
tiernodeb74b22019-05-27 10:24:50 +0000136 :param shared:
137 :param vlan:
138 :return: a tuple with the network identifier and created_items, or raises an exception on error
seryio34478552019-05-23 14:50:49 +0200139 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.
tiernodeb74b22019-05-27 10:24:50 +0000143 """
144
seryio34478552019-05-23 14:50:49 +0200145 return self._new_subnet(net_name, ip_profile)
146
147 def _new_subnet(self, net_name, ip_profile):
tiernodeb74b22019-05-27 10:24:50 +0000148 """
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 """
seryio34478552019-05-23 14:50:49 +0200154 self.logger.debug('Adding a subnet to VNET '+self.vnet_name)
155 self._reload_connection()
seryio34478552019-05-23 14:50:49 +0200156
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
tiernodeb74b22019-05-27 10:24:50 +0000218 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
seryio34478552019-05-23 14:50:49 +0200266 def get_network_list(self, filter_dict={}):
tiernodeb74b22019-05-27 10:24:50 +0000267 """Obtain tenant networks of VIM
seryio34478552019-05-23 14:50:49 +0200268 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
tiernodeb74b22019-05-27 10:24:50 +0000276 """
seryio34478552019-05-23 14:50:49 +0200277 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
tierno30d0d6d2019-05-27 08:14:01 +0000308 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):
seryio34478552019-05-23 14:50:49 +0200310 #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):
tierno30d0d6d2019-05-27 08:14:01 +0000372 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)]
seryio34478552019-05-23 14:50:49 +0200375
tierno30d0d6d2019-05-27 08:14:01 +0000376 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):
seryio34478552019-05-23 14:50:49 +0200385 try:
386 self._reload_connection()
tierno30d0d6d2019-05-27 08:14:01 +0000387 return True
388 except Exception as e:
389 raise vimconn.vimconnException("Connectivity issue with Azure API: {}".format(e))
seryio34478552019-05-23 14:50:49 +0200390
391 def get_network(self, net_id):
tierno30d0d6d2019-05-27 08:14:01 +0000392 resGroup = self._get_resource_group_name_from_resource_id(net_id)
seryio34478552019-05-23 14:50:49 +0200393 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):
tierno30d0d6d2019-05-27 08:14:01 +0000401 resGroup = self._get_resource_group_name_from_resource_id(net_id)
seryio34478552019-05-23 14:50:49 +0200402 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):
tierno30d0d6d2019-05-27 08:14:01 +0000408 resGroup = self._get_resource_group_name_from_resource_id(net_id)
seryio34478552019-05-23 14:50:49 +0200409 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):
tierno30d0d6d2019-05-27 08:14:01 +0000415 resGroup = self._get_resource_group_name_from_resource_id(net_id)
seryio34478552019-05-23 14:50:49 +0200416 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
430# TODO refresh_nets_status ver estado activo
seryio34478552019-05-23 14:50:49 +0200431# TODO refresh_vms_status ver estado activo
432# TODO get_vminstance_console for getting console
433
434if __name__ == "__main__":
435
436 # Making some basic test
437 vim_id='azure'
438 vim_name='azure'
439 needed_test_params = {
tiernodeb74b22019-05-27 10:24:50 +0000440 "client_id": "AZURE_CLIENT_ID",
441 "secret": "AZURE_SECRET",
442 "tenant": "AZURE_TENANT",
443 "resource_group": "AZURE_RESOURCE_GROUP",
444 "subscription_id": "AZURE_SUBSCRIPTION_ID",
tierno30d0d6d2019-05-27 08:14:01 +0000445 "vnet_name": "AZURE_VNET_NAME",
seryio34478552019-05-23 14:50:49 +0200446 }
447 test_params = {}
448
449 for param, env_var in needed_test_params.items():
450 value = getenv(env_var)
451 if not value:
452 raise Exception("Provide a valid value for env '{}'".format(env_var))
453 test_params[param] = value
454
455 config = {
456 'region_name': getenv("AZURE_REGION_NAME", 'westeurope'),
457 'resource_group': getenv("AZURE_RESOURCE_GROUP"),
458 'subscription_id': getenv("AZURE_SUBSCRIPTION_ID"),
tierno30d0d6d2019-05-27 08:14:01 +0000459 'pub_key': getenv("AZURE_PUB_KEY", None),
seryio34478552019-05-23 14:50:49 +0200460 'vnet_name': getenv("AZURE_VNET_NAME", 'myNetwork'),
461 }
tierno30d0d6d2019-05-27 08:14:01 +0000462
seryio34478552019-05-23 14:50:49 +0200463 virtualMachine = {
tierno30d0d6d2019-05-27 08:14:01 +0000464 'name': 'sergio',
465 'description': 'new VM',
seryio34478552019-05-23 14:50:49 +0200466 'status': 'running',
467 'image': {
468 'publisher': 'Canonical',
469 'offer': 'UbuntuServer',
470 'sku': '16.04.0-LTS',
471 'version': 'latest'
472 },
473 'hardware_profile': {
474 'vm_size': 'Standard_DS1_v2'
475 },
476 'networks': [
477 'sergio'
478 ]
479 }
480
481 vnet_config = {
482 'subnet_address': '10.1.2.0/24',
483 #'subnet_name': 'subnet-oam'
484 }
485 ###########################
486
tierno30d0d6d2019-05-27 08:14:01 +0000487 azure = vimconnector(vim_id, vim_name, tenant_id=test_params["tenant"], tenant_name=None, url=None, url_admin=None,
488 user=test_params["client_id"], passwd=test_params["secret"], log_level=None, config=config)
seryio34478552019-05-23 14:50:49 +0200489
tiernodeb74b22019-05-27 10:24:50 +0000490 # azure.get_flavor_id_from_data("here")
491 # subnets=azure.get_network_list()
492 # azure.new_vminstance(virtualMachine['name'], virtualMachine['description'], virtualMachine['status'],
493 # virtualMachine['image'], virtualMachine['hardware_profile']['vm_size'], subnets)
494
seryio34478552019-05-23 14:50:49 +0200495 azure.get_flavor("Standard_A11")