Merge "AWS - VIM Support"
[osm/RO.git] / osm_ro / vimconn_aws.py
1 # -*- coding: utf-8 -*-
2
3 ##
4 # Copyright 2017 xFlow Research Pvt. Ltd
5 # This file is part of openmano
6 # All Rights Reserved.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License"); you may
9 # not use this file except in compliance with the License. You may obtain
10 # a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17 # License for the specific language governing permissions and limitations
18 # under the License.
19 #
20 # For those usages not covered by the Apache License, Version 2.0 please
21 # contact with: saboor.ahmad@xflowresearch.com
22 ##
23
24 '''
25 AWS-connector implements all the methods to interact with AWS using the BOTO client
26 '''
27
28 __author__ = "Saboor Ahmad"
29 __date__ = "10-Apr-2017"
30
31 import vimconn
32 import yaml
33 import logging
34 import netaddr
35 import time
36
37 try:
38 import boto
39 import boto.ec2
40 import boto.vpc
41 except:
42 exit("Boto not avialable. Try activating your virtualenv OR `pip install boto`")
43
44
45 class vimconnector(vimconn.vimconnector):
46 def __init__(self, uuid, name, tenant_id, tenant_name, url, url_admin=None, user=None, passwd=None, log_level=None,
47 config={}):
48 """ Params: uuid - id asigned to this VIM
49 name - name assigned to this VIM, can be used for logging
50 tenant_id - ID to be used for tenant
51 tenant_name - name of tenant to be used VIM tenant to be used
52 url_admin - optional, url used for administrative tasks
53 user - credentials of the VIM user
54 passwd - credentials of the VIM user
55 log_level - if must use a different log_level than the general one
56 config - dictionary with misc VIM information
57 region_name - name of region to deploy the instances
58 vpc_cidr_block - default CIDR block for VPC
59 security_groups - default security group to specify this instance
60 """
61
62 vimconn.vimconnector.__init__(self, uuid, name, tenant_id, tenant_name, url, url_admin, user, passwd, log_level,
63 config)
64 self.a_creds = {}
65 if user:
66 self.a_creds['aws_access_key_id'] = user
67 else:
68 raise vimconn.vimconnAuthException("Username is not specified")
69 if passwd:
70 self.a_creds['aws_secret_access_key'] = passwd
71 else:
72 raise vimconn.vimconnAuthException("Password is not specified")
73 if 'region_name' in config:
74 self.region = config.get('region_name')
75 else:
76 raise vimconn.vimconnNotFoundException("AWS region_name is not specified at config")
77
78 self.vpc_data = {}
79 self.subnet_data = {}
80 self.conn = None
81 self.conn_vpc = None
82 self.account_id = None
83
84 self.vpc_id = self.get_tenant_list()[0]['id']
85 # we take VPC CIDR block if specified, otherwise we use the default CIDR
86 # block suggested by AWS while creating instance
87 self.vpc_cidr_block = '10.0.0.0/24'
88
89 if tenant_id:
90 self.vpc_id = tenant_id
91 if 'vpc_cidr_block' in config:
92 self.vpc_cidr_block = config['vpc_cidr_block']
93
94 self.security_groups = None
95 if 'security_groups' in config:
96 self.security_groups = config['security_groups']
97
98 self.key_pair = None
99 if 'key_pair' in config:
100 self.key_pair = config['key_pair']
101
102 self.flavor_info = None
103 if 'flavor_info' in config:
104 flavor_data = config.get('flavor_info')
105 if isinstance(flavor_data, str):
106 try:
107 with open(flavor_data[1:], 'r') as stream:
108 self.flavor_info = yaml.load(stream)
109 except yaml.YAMLError as e:
110 self.flavor_info = None
111 raise vimconn.vimconnException("Bad format at file '{}': {}".format(flavor_data[1:], e))
112 except IOError as e:
113 raise vimconn.vimconnException("Error reading file '{}': {}".format(flavor_data[1:], e))
114 elif isinstance(flavor_data, dict):
115 self.flavor_data = flavor_data
116
117 self.logger = logging.getLogger('openmano.vim.aws')
118 if log_level:
119 self.logger.setLevel(getattr(logging, log_level))
120
121 def __setitem__(self, index, value):
122 """Params: index - name of value of set
123 value - value to set
124 """
125 if index == 'user':
126 self.a_creds['aws_access_key_id'] = value
127 elif index == 'passwd':
128 self.a_creds['aws_secret_access_key'] = value
129 elif index == 'region':
130 self.region = value
131 else:
132 vimconn.vimconnector.__setitem__(self, index, value)
133
134 def _reload_connection(self):
135 """Returns: sets boto.EC2 and boto.VPC connection to work with AWS services
136 """
137
138 try:
139 self.conn = boto.ec2.connect_to_region(self.region, aws_access_key_id=self.a_creds['aws_access_key_id'],
140 aws_secret_access_key=self.a_creds['aws_secret_access_key'])
141 self.conn_vpc = boto.vpc.connect_to_region(self.region, aws_access_key_id=self.a_creds['aws_access_key_id'],
142 aws_secret_access_key=self.a_creds['aws_secret_access_key'])
143 # client = boto3.client("sts", aws_access_key_id=self.a_creds['aws_access_key_id'], aws_secret_access_key=self.a_creds['aws_secret_access_key'])
144 # self.account_id = client.get_caller_identity()["Account"]
145 except Exception as e:
146 self.format_vimconn_exception(e)
147
148 def format_vimconn_exception(self, e):
149 """Params: an Exception object
150 Returns: Raises the exception 'e' passed in mehtod parameters
151 """
152
153 self.conn = None
154 self.conn_vpc = None
155 raise vimconn.vimconnConnectionException(type(e).__name__ + ": " + str(e))
156
157 def get_availability_zones_list(self):
158 """Obtain AvailabilityZones from AWS
159 """
160
161 try:
162 self._reload_connection()
163 az_list = []
164 for az in self.conn.get_all_zones():
165 az_list.append(az.name)
166 return az_list
167 except Exception as e:
168 self.format_vimconn_exception(e)
169
170 def get_tenant_list(self, filter_dict={}):
171 """Obtain tenants of VIM
172 filter_dict dictionary that can contain the following keys:
173 name: filter by tenant name
174 id: filter by tenant uuid/id
175 <other VIM specific>
176 Returns the tenant list of dictionaries, and empty list if no tenant match all the filers:
177 [{'name':'<name>, 'id':'<id>, ...}, ...]
178 """
179
180 try:
181 self._reload_connection()
182 vpc_ids = []
183 tfilters = {}
184 if filter_dict != {}:
185 if 'id' in filter_dict:
186 vpc_ids.append(filter_dict['id'])
187 tfilters['name'] = filter_dict['id']
188 tenants = self.conn_vpc.get_all_vpcs(vpc_ids, tfilters)
189 tenant_list = []
190 for tenant in tenants:
191 tenant_list.append({'id': str(tenant.id), 'name': str(tenant.id), 'status': str(tenant.state),
192 'cidr_block': str(tenant.cidr_block)})
193 return tenant_list
194 except Exception as e:
195 self.format_vimconn_exception(e)
196
197 def new_tenant(self, tenant_name, tenant_description):
198 """Adds a new tenant to VIM with this name and description, this is done using admin_url if provided
199 "tenant_name": string max lenght 64
200 "tenant_description": string max length 256
201 returns the tenant identifier or raise exception
202 """
203
204 self.logger.debug("Adding a new VPC")
205 try:
206 self._reload_connection()
207 vpc = self.conn_vpc.create_vpc(self.vpc_cidr_block)
208 self.conn_vpc.modify_vpc_attribute(vpc.id, enable_dns_support=True)
209 self.conn_vpc.modify_vpc_attribute(vpc.id, enable_dns_hostnames=True)
210
211 gateway = self.conn_vpc.create_internet_gateway()
212 self.conn_vpc.attach_internet_gateway(gateway.id, vpc.id)
213 route_table = self.conn_vpc.create_route_table(vpc.id)
214 self.conn_vpc.create_route(route_table.id, '0.0.0.0/0', gateway.id)
215
216 self.vpc_data[vpc.id] = {'gateway': gateway.id, 'route_table': route_table.id,
217 'subnets': self.subnet_sizes(len(self.get_availability_zones_list()),
218 self.vpc_cidr_block)}
219 return vpc.id
220 except Exception as e:
221 self.format_vimconn_exception(e)
222
223 def delete_tenant(self, tenant_id):
224 """Delete a tenant from VIM
225 tenant_id: returned VIM tenant_id on "new_tenant"
226 Returns None on success. Raises and exception of failure. If tenant is not found raises vimconnNotFoundException
227 """
228
229 self.logger.debug("Deleting specified VPC")
230 try:
231 self._reload_connection()
232 vpc = self.vpc_data.get(tenant_id)
233 if 'gateway' in vpc and 'route_table' in vpc:
234 gateway_id, route_table_id = vpc['gateway'], vpc['route_table']
235 self.conn_vpc.detach_internet_gateway(gateway_id, tenant_id)
236 self.conn_vpc.delete_vpc(tenant_id)
237 self.conn_vpc.delete_route(route_table_id, '0.0.0.0/0')
238 else:
239 self.conn_vpc.delete_vpc(tenant_id)
240 except Exception as e:
241 self.format_vimconn_exception(e)
242
243 def subnet_sizes(self, availability_zones, cidr):
244 """Calcualtes possible subnets given CIDR value of VPC
245 """
246
247 if availability_zones != 2 and availability_zones != 3:
248 self.logger.debug("Number of AZs should be 2 or 3")
249 raise vimconn.vimconnNotSupportedException("Number of AZs should be 2 or 3")
250
251 netmasks = ('255.255.252.0', '255.255.254.0', '255.255.255.0', '255.255.255.128')
252 ip = netaddr.IPNetwork(cidr)
253 mask = ip.netmask
254
255 if str(mask) not in netmasks:
256 self.logger.debug("Netmask " + str(mask) + " not found")
257 raise vimconn.vimconnNotFoundException("Netmask " + str(mask) + " not found")
258
259 if availability_zones == 2:
260 for n, netmask in enumerate(netmasks):
261 if str(mask) == netmask:
262 subnets = list(ip.subnet(n + 24))
263 else:
264 for n, netmask in enumerate(netmasks):
265 if str(mask) == netmask:
266 pub_net = list(ip.subnet(n + 24))
267 pri_subs = pub_net[1:]
268 pub_mask = pub_net[0].netmask
269 pub_split = list(ip.subnet(26)) if (str(pub_mask) == '255.255.255.0') else list(ip.subnet(27))
270 pub_subs = pub_split[:3]
271 subnets = pub_subs + pri_subs
272
273 return map(str, subnets)
274
275 def new_network(self, net_name, net_type, ip_profile=None, shared=False, vlan=None):
276 """Adds a tenant network to VIM
277 Params:
278 'net_name': name of the network
279 'net_type': one of:
280 'bridge': overlay isolated network
281 'data': underlay E-LAN network for Passthrough and SRIOV interfaces
282 'ptp': underlay E-LINE network for Passthrough and SRIOV interfaces.
283 'ip_profile': is a dict containing the IP parameters of the network (Currently only IPv4 is implemented)
284 'ip-version': can be one of ["IPv4","IPv6"]
285 'subnet-address': ip_prefix_schema, that is X.X.X.X/Y
286 'gateway-address': (Optional) ip_schema, that is X.X.X.X
287 'dns-address': (Optional) ip_schema,
288 'dhcp': (Optional) dict containing
289 'enabled': {"type": "boolean"},
290 'start-address': ip_schema, first IP to grant
291 'count': number of IPs to grant.
292 'shared': if this network can be seen/use by other tenants/organization
293 'vlan': in case of a data or ptp net_type, the intended vlan tag to be used for the network
294 Returns the network identifier on success or raises and exception on failure
295 """
296
297 self.logger.debug("Adding a subnet to VPC")
298 try:
299 self._reload_connection()
300 subnet = None
301 vpc_id = self.vpc_id
302 if self.vpc_data.get(vpc_id, None):
303 cidr_block = list(set(self.vpc_data[vpc_id]['subnets']) - set(self.get_network_details({'tenant_id': vpc_id}, detail='cidr_block')))[0]
304 else:
305 vpc = self.get_tenant_list({'id': vpc_id})[0]
306 subnet_list = self.subnet_sizes(len(self.get_availability_zones_list()), vpc['cidr_block'])
307 cidr_block = list(set(subnet_list) - set(self.get_network_details({'tenant_id': vpc['id']}, detail='cidr_block')))[0]
308 subnet = self.conn_vpc.create_subnet(vpc_id, cidr_block)
309 return subnet.id
310 except Exception as e:
311 self.format_vimconn_exception(e)
312
313 def get_network_details(self, filters, detail):
314 """Get specified details related to a subnet
315 """
316 detail_list = []
317 subnet_list = self.get_network_list(filters)
318 for net in subnet_list:
319 detail_list.append(net[detail])
320 return detail_list
321
322 def get_network_list(self, filter_dict={}):
323 """Obtain tenant networks of VIM
324 Params:
325 'filter_dict' (optional) contains entries to return only networks that matches ALL entries:
326 name: string => returns only networks with this name
327 id: string => returns networks with this VIM id, this imply returns one network at most
328 shared: boolean >= returns only networks that are (or are not) shared
329 tenant_id: sting => returns only networks that belong to this tenant/project
330 ,#(not used yet) admin_state_up: boolean => returns only networks that are (or are not) in admin state active
331 #(not used yet) status: 'ACTIVE','ERROR',... => filter networks that are on this status
332 Returns the network list of dictionaries. each dictionary contains:
333 'id': (mandatory) VIM network id
334 'name': (mandatory) VIM network name
335 'status': (mandatory) can be 'ACTIVE', 'INACTIVE', 'DOWN', 'BUILD', 'ERROR', 'VIM_ERROR', 'OTHER'
336 'error_msg': (optional) text that explains the ERROR status
337 other VIM specific fields: (optional) whenever possible using the same naming of filter_dict param
338 List can be empty if no network map the filter_dict. Raise an exception only upon VIM connectivity,
339 authorization, or some other unspecific error
340 """
341
342 self.logger.debug("Getting all subnets from VIM")
343 try:
344 self._reload_connection()
345 tfilters = {}
346 if filter_dict != {}:
347 if 'tenant_id' in filter_dict:
348 tfilters['vpcId'] = filter_dict['tenant_id']
349 subnets = self.conn_vpc.get_all_subnets(subnet_ids=filter_dict.get('id', None), filters=tfilters)
350 net_list = []
351 for net in subnets:
352 net_list.append(
353 {'id': str(net.id), 'name': str(net.id), 'status': str(net.state), 'vpc_id': str(net.vpc_id),
354 'cidr_block': str(net.cidr_block)})
355 return net_list
356 except Exception as e:
357 self.format_vimconn_exception(e)
358
359 def get_network(self, net_id):
360 """Obtain network details from the 'net_id' VIM network
361 Return a dict that contains:
362 'id': (mandatory) VIM network id, that is, net_id
363 'name': (mandatory) VIM network name
364 'status': (mandatory) can be 'ACTIVE', 'INACTIVE', 'DOWN', 'BUILD', 'ERROR', 'VIM_ERROR', 'OTHER'
365 'error_msg': (optional) text that explains the ERROR status
366 other VIM specific fields: (optional) whenever possible using the same naming of filter_dict param
367 Raises an exception upon error or when network is not found
368 """
369
370 self.logger.debug("Getting Subnet from VIM")
371 try:
372 self._reload_connection()
373 subnet = self.conn_vpc.get_all_subnets(net_id)[0]
374 return {'id': str(subnet.id), 'name': str(subnet.id), 'status': str(subnet.state),
375 'vpc_id': str(subnet.vpc_id), 'cidr_block': str(subnet.cidr_block)}
376 except Exception as e:
377 self.format_vimconn_exception(e)
378
379 def delete_network(self, net_id):
380 """Deletes a tenant network from VIM
381 Returns the network identifier or raises an exception upon error or when network is not found
382 """
383
384 self.logger.debug("Deleting subnet from VIM")
385 try:
386 self._reload_connection()
387 self.logger.debug("DELETING NET_ID: " + str(net_id))
388 self.conn_vpc.delete_subnet(net_id)
389 return net_id
390 except Exception as e:
391 self.format_vimconn_exception(e)
392
393 def refresh_nets_status(self, net_list):
394 """Get the status of the networks
395 Params:
396 'net_list': a list with the VIM network id to be get the status
397 Returns a dictionary with:
398 'net_id': #VIM id of this network
399 status: #Mandatory. Text with one of:
400 # DELETED (not found at vim)
401 # VIM_ERROR (Cannot connect to VIM, authentication problems, VIM response error, ...)
402 # OTHER (Vim reported other status not understood)
403 # ERROR (VIM indicates an ERROR status)
404 # ACTIVE, INACTIVE, DOWN (admin down),
405 # BUILD (on building process)
406 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
407 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
408 'net_id2': ...
409 """
410
411 self._reload_connection()
412 try:
413 dict_entry = {}
414 for net_id in net_list:
415 subnet_dict = {}
416 subnet = None
417 try:
418 subnet = self.conn_vpc.get_all_subnets(net_id)[0]
419 if subnet.state == "pending":
420 subnet_dict['status'] = "BUILD"
421 elif subnet.state == "available":
422 subnet_dict['status'] = 'ACTIVE'
423 else:
424 subnet_dict['status'] = 'ERROR'
425 subnet_dict['error_msg'] = ''
426 except Exception as e:
427 subnet_dict['status'] = 'DELETED'
428 subnet_dict['error_msg'] = 'Network not found'
429 finally:
430 try:
431 subnet_dict['vim_info'] = yaml.safe_dump(subnet, default_flow_style=True, width=256)
432 except yaml.YAMLError as e:
433 subnet_dict['vim_info'] = str(subnet)
434 dict_entry[net_id] = subnet_dict
435 return dict_entry
436 except Exception as e:
437 self.format_vimconn_exception(e)
438
439 def get_flavor(self, flavor_id):
440 """Obtain flavor details from the VIM
441 Returns the flavor dict details {'id':<>, 'name':<>, other vim specific }
442 Raises an exception upon error or if not found
443 """
444
445 self.logger.debug("Getting instance type")
446 try:
447 if flavor_id in self.flavor_info:
448 return self.flavor_info[flavor_id]
449 else:
450 raise vimconn.vimconnNotFoundException("Cannot find flavor with this flavor ID/Name")
451 except Exception as e:
452 self.format_vimconn_exception(e)
453
454 def get_flavor_id_from_data(self, flavor_dict):
455 """Obtain flavor id that match the flavor description
456 Params:
457 'flavor_dict': dictionary that contains:
458 'disk': main hard disk in GB
459 'ram': memory in MB
460 'vcpus': number of virtual cpus
461 #todo: complete parameters for EPA
462 Returns the flavor_id or raises a vimconnNotFoundException
463 """
464
465 self.logger.debug("Getting flavor id from data")
466 try:
467 flavor = None
468 for key, values in self.flavor_info.iteritems():
469 if (values["memory"], values["cores"], values["disk"]) == (
470 flavor_dict["ram"], flavor_dict["cpus"], flavor_dict["disk"]):
471 flavor = (key, values)
472 break
473 elif (values["memory"], values["cores"], values["disk"]) >= (
474 flavor_dict["ram"], flavor_dict["cpus"], flavor_dict["disk"]):
475 if not flavor:
476 flavor = (key, values)
477 else:
478 if (flavor[1]["memory"], flavor[1]["cores"], flavor[1]["disk"]) >= (
479 values["memory"], values["cores"], values["disk"]):
480 flavor = (key, values)
481 if flavor:
482 return flavor[0]
483 raise vimconn.vimconnNotFoundException("Cannot find flavor with this flavor ID/Name")
484 except Exception as e:
485 self.format_vimconn_exception(e)
486
487 def new_image(self, image_dict):
488 """ Adds a tenant image to VIM
489 Params: image_dict
490 name (string) - The name of the AMI. Valid only for EBS-based images.
491 description (string) - The description of the AMI.
492 image_location (string) - Full path to your AMI manifest in Amazon S3 storage. Only used for S3-based AMI’s.
493 architecture (string) - The architecture of the AMI. Valid choices are: * i386 * x86_64
494 kernel_id (string) - The ID of the kernel with which to launch the instances
495 root_device_name (string) - The root device name (e.g. /dev/sdh)
496 block_device_map (boto.ec2.blockdevicemapping.BlockDeviceMapping) - A BlockDeviceMapping data structure describing the EBS volumes associated with the Image.
497 virtualization_type (string) - The virutalization_type of the image. Valid choices are: * paravirtual * hvm
498 sriov_net_support (string) - Advanced networking support. Valid choices are: * simple
499 snapshot_id (string) - A snapshot ID for the snapshot to be used as root device for the image. Mutually exclusive with block_device_map, requires root_device_name
500 delete_root_volume_on_termination (bool) - Whether to delete the root volume of the image after instance termination. Only applies when creating image from snapshot_id. Defaults to False. Note that leaving volumes behind after instance termination is not free
501 Returns: image_id - image ID of the newly created image
502 """
503
504 try:
505 self._reload_connection()
506 image_location = image_dict.get('image_location', None)
507 if image_location:
508 image_location = str(self.account_id) + str(image_location)
509
510 image_id = self.conn.register_image(image_dict.get('name', None), image_dict.get('description', None),
511 image_location, image_dict.get('architecture', None),
512 image_dict.get('kernel_id', None),
513 image_dict.get('root_device_name', None),
514 image_dict.get('block_device_map', None),
515 image_dict.get('virtualization_type', None),
516 image_dict.get('sriov_net_support', None),
517 image_dict.get('snapshot_id', None),
518 image_dict.get('delete_root_volume_on_termination', None))
519 return image_id
520 except Exception as e:
521 self.format_vimconn_exception(e)
522
523 def delete_image(self, image_id):
524 """Deletes a tenant image from VIM
525 Returns the image_id if image is deleted or raises an exception on error"""
526
527 try:
528 self._reload_connection()
529 self.conn.deregister_image(image_id)
530 return image_id
531 except Exception as e:
532 self.format_vimconn_exception(e)
533
534 def get_image_id_from_path(self, path):
535 '''
536 Params: path - location of the image
537 Returns: image_id - ID of the matching image
538 '''
539 self._reload_connection()
540 try:
541 filters = {}
542 if path:
543 tokens = path.split('/')
544 filters['owner_id'] = tokens[0]
545 filters['name'] = '/'.join(tokens[1:])
546 image = self.conn.get_all_images(filters=filters)[0]
547 return image.id
548 except Exception as e:
549 self.format_vimconn_exception(e)
550
551 def get_image_list(self, filter_dict={}):
552 """Obtain tenant images from VIM
553 Filter_dict can be:
554 name: image name
555 id: image uuid
556 checksum: image checksum
557 location: image path
558 Returns the image list of dictionaries:
559 [{<the fields at Filter_dict plus some VIM specific>}, ...]
560 List can be empty
561 """
562
563 self.logger.debug("Getting image list from VIM")
564 try:
565 self._reload_connection()
566 image_id = None
567 filters = {}
568 if 'id' in filter_dict:
569 image_id = filter_dict['id']
570 if 'name' in filter_dict:
571 filters['name'] = filter_dict['name']
572 if 'location' in filter_dict:
573 filters['location'] = filter_dict['location']
574 # filters['image_type'] = 'machine'
575 # filter_dict['owner_id'] = self.account_id
576 images = self.conn.get_all_images(image_id, filters=filters)
577 image_list = []
578 for image in images:
579 image_list.append({'id': str(image.id), 'name': str(image.name), 'status': str(image.state),
580 'owner': str(image.owner_id), 'location': str(image.location),
581 'is_public': str(image.is_public), 'architecture': str(image.architecture),
582 'platform': str(image.platform)})
583 return image_list
584 except Exception as e:
585 self.format_vimconn_exception(e)
586
587 def new_vminstance(self, name, description, start, image_id, flavor_id, net_list, cloud_config=None,
588 disk_list=None):
589 """Create a new VM/instance in AWS
590 Params: name
591 decription
592 start: (boolean) indicates if VM must start or created in pause mode.
593 image_id - image ID in AWS
594 flavor_id - instance type ID in AWS
595 net_list
596 name
597 net_id - subnet_id from AWS
598 vpci - (optional) virtual vPCI address to assign at the VM. Can be ignored depending on VIM capabilities
599 model: (optional and only have sense for type==virtual) interface model: virtio, e2000, ...
600 mac_address: (optional) mac address to assign to this interface
601 type: (mandatory) can be one of:
602 virtual, in this case always connected to a network of type 'net_type=bridge'
603 PF - (passthrough): depending on VIM capabilities it can be connected to a data/ptp network ot it
604 can created unconnected
605 VF - (SRIOV with VLAN tag): same as PF for network connectivity.
606 VFnotShared - (SRIOV without VLAN tag) same as PF for network connectivity. VF where no other VFs
607 are allocated on the same physical NIC
608 bw': (optional) only for PF/VF/VFnotShared. Minimal Bandwidth required for the interface in GBPS
609 port_security': (optional) If False it must avoid any traffic filtering at this interface. If missing or True, it must apply the default VIM behaviour
610 vim_id': must be filled/added by this method with the VIM identifier generated by the VIM for this interface. 'net_list' is modified
611 elastic_ip - True/False to define if an elastic_ip is required
612 cloud_config': (optional) dictionary with:
613 key-pairs': (optional) list of strings with the public key to be inserted to the default user
614 users': (optional) list of users to be inserted, each item is a dict with:
615 name': (mandatory) user name,
616 key-pairs': (optional) list of strings with the public key to be inserted to the user
617 user-data': (optional) string is a text script to be passed directly to cloud-init
618 config-files': (optional). List of files to be transferred. Each item is a dict with:
619 dest': (mandatory) string with the destination absolute path
620 encoding': (optional, by default text). Can be one of:
621 b64', 'base64', 'gz', 'gz+b64', 'gz+base64', 'gzip+b64', 'gzip+base64'
622 content' (mandatory): string with the content of the file
623 permissions': (optional) string with file permissions, typically octal notation '0644'
624 owner: (optional) file owner, string with the format 'owner:group'
625 boot-data-drive: boolean to indicate if user-data must be passed using a boot drive (hard disk)
626 security-groups:
627 subnet_id
628 security_group_id
629 disk_list': (optional) list with additional disks to the VM. Each item is a dict with:
630 image_id': (optional). VIM id of an existing image. If not provided an empty disk must be mounted
631 size': (mandatory) string with the size of the disk in GB
632 Returns: instance identifier or raises an exception on error
633 """
634
635 self.logger.debug("Creating a new VM instance")
636 try:
637 self._reload_connection()
638 instance = None
639 userdata = None
640 if isinstance(cloud_config, dict):
641 if cloud_config.get("user-data"):
642 userdata = cloud_config["user-data"]
643 if cloud_config.get("config-files") or cloud_config.get("users") or cloud_config.get("key-pairs"):
644 if userdata:
645 raise vimconn.vimconnConflictException(
646 "Cloud-config cannot contain both 'userdata' and 'config-files'/'users'/'key-pairs'")
647 userdata_dict = {}
648 # default user
649 if cloud_config.get("key-pairs"):
650 userdata_dict["ssh-authorized-keys"] = cloud_config["key-pairs"]
651 userdata_dict["users"] = [{"default": None, "ssh-authorized-keys": cloud_config["key-pairs"]}]
652 if cloud_config.get("users"):
653 if "users" not in userdata_dict:
654 userdata_dict["users"] = ["default"]
655 for user in cloud_config["users"]:
656 user_info = {
657 "name": user["name"],
658 "sudo": "ALL = (ALL)NOPASSWD:ALL"
659 }
660 if "user-info" in user:
661 user_info["gecos"] = user["user-info"]
662 if user.get("key-pairs"):
663 user_info["ssh-authorized-keys"] = user["key-pairs"]
664 userdata_dict["users"].append(user_info)
665
666 if cloud_config.get("config-files"):
667 userdata_dict["write_files"] = []
668 for file in cloud_config["config-files"]:
669 file_info = {
670 "path": file["dest"],
671 "content": file["content"]
672 }
673 if file.get("encoding"):
674 file_info["encoding"] = file["encoding"]
675 if file.get("permissions"):
676 file_info["permissions"] = file["permissions"]
677 if file.get("owner"):
678 file_info["owner"] = file["owner"]
679 userdata_dict["write_files"].append(file_info)
680 userdata = "#cloud-config\n"
681 userdata += yaml.safe_dump(userdata_dict, indent=4, default_flow_style=False)
682 self.logger.debug("userdata: %s", userdata)
683 elif isinstance(cloud_config, str):
684 userdata = cloud_config
685
686 if not net_list:
687 reservation = self.conn.run_instances(
688 image_id,
689 key_name=self.key_pair,
690 instance_type=flavor_id,
691 security_groups=self.security_groups,
692 user_data=userdata
693 )
694 instance = reservation.instances[0]
695 else:
696 net_list = [net_list[0]]
697 for index, subnet in enumerate(net_list):
698 net_intr = boto.ec2.networkinterface.NetworkInterfaceSpecification(subnet_id=subnet.get('net_id'),
699 groups=None,
700 associate_public_ip_address=True)
701
702 if subnet.get('elastic_ip'):
703 eip = self.conn.allocate_address()
704 self.conn.associate_address(allocation_id=eip.allocation_id, network_interface_id=net_intr.id)
705
706 if index == 0:
707 reservation = self.conn.run_instances(
708 image_id,
709 key_name=self.key_pair,
710 instance_type=flavor_id,
711 security_groups=self.security_groups,
712 network_interfaces=boto.ec2.networkinterface.NetworkInterfaceCollection(net_intr),
713 user_data=userdata
714 )
715 instance = reservation.instances[0]
716 else:
717 while True:
718 try:
719 self.conn.attach_network_interface(
720 network_interface_id=boto.ec2.networkinterface.NetworkInterfaceCollection(net_intr),
721 instance_id=instance.id, device_index=0)
722 break
723 except:
724 time.sleep(10)
725 return instance.id
726 except Exception as e:
727 self.format_vimconn_exception(e)
728
729 def get_vminstance(self, vm_id):
730 """Returns the VM instance information from VIM"""
731
732 try:
733 self._reload_connection()
734 reservation = self.conn.get_all_instances(vm_id)
735 return reservation[0].instances[0].__dict__
736 except Exception as e:
737 self.format_vimconn_exception(e)
738
739 def delete_vminstance(self, vm_id):
740 """Removes a VM instance from VIM
741 Returns the instance identifier"""
742
743 try:
744 self._reload_connection()
745 self.logger.debug("DELETING VM_ID: " + str(vm_id))
746 self.conn.terminate_instances(vm_id)
747 return vm_id
748 except Exception as e:
749 self.format_vimconn_exception(e)
750
751 def refresh_vms_status(self, vm_list):
752 """ Get the status of the virtual machines and their interfaces/ports
753 Params: the list of VM identifiers
754 Returns a dictionary with:
755 vm_id: #VIM id of this Virtual Machine
756 status: #Mandatory. Text with one of:
757 # DELETED (not found at vim)
758 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
759 # OTHER (Vim reported other status not understood)
760 # ERROR (VIM indicates an ERROR status)
761 # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
762 # BUILD (on building process), ERROR
763 # ACTIVE:NoMgmtIP (Active but any of its interface has an IP address
764 #
765 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
766 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
767 interfaces: list with interface info. Each item a dictionary with:
768 vim_interface_id - The ID of the ENI.
769 vim_net_id - The ID of the VPC subnet.
770 mac_address - The MAC address of the interface.
771 ip_address - The IP address of the interface within the subnet.
772 """
773 self.logger.debug("Getting VM instance information from VIM")
774 try:
775 self._reload_connection()
776 reservation = self.conn.get_all_instances(vm_list)[0]
777 instances = {}
778 instance_dict = {}
779 for instance in reservation.instances:
780 try:
781 if instance.state in ("pending"):
782 instance_dict['status'] = "BUILD"
783 elif instance.state in ("available", "running", "up"):
784 instance_dict['status'] = 'ACTIVE'
785 else:
786 instance_dict['status'] = 'ERROR'
787 instance_dict['error_msg'] = ""
788 instance_dict['interfaces'] = []
789 interface_dict = {}
790 for interface in instance.interfaces:
791 interface_dict['vim_interface_id'] = interface.id
792 interface_dict['vim_net_id'] = interface.subnet_id
793 interface_dict['mac_address'] = interface.mac_address
794 interface_dict['ip_address'] = interface.private_ip_address
795 instance_dict['interfaces'].append(interface_dict)
796 except Exception as e:
797 self.logger.error("Exception getting vm status: %s", str(e), exc_info=True)
798 instance_dict['status'] = "DELETED"
799 instance_dict['error_msg'] = str(e)
800 finally:
801 try:
802 instance_dict['vim_info'] = yaml.safe_dump(instance, default_flow_style=True, width=256)
803 except yaml.YAMLError as e:
804 # self.logger.error("Exception getting vm status: %s", str(e), exc_info=True)
805 instance_dict['vim_info'] = str(instance)
806 instances[instance.id] = instance_dict
807 return instances
808 except Exception as e:
809 self.logger.error("Exception getting vm status: %s", str(e), exc_info=True)
810 self.format_vimconn_exception(e)
811
812 def action_vminstance(self, vm_id, action_dict):
813 """Send and action over a VM instance from VIM
814 Returns the vm_id if the action was successfully sent to the VIM"""
815
816 self.logger.debug("Action over VM '%s': %s", vm_id, str(action_dict))
817 try:
818 self._reload_connection()
819 if "start" in action_dict:
820 self.conn.start_instances(vm_id)
821 elif "stop" in action_dict or "stop" in action_dict:
822 self.conn.stop_instances(vm_id)
823 elif "terminate" in action_dict:
824 self.conn.terminate_instances(vm_id)
825 elif "reboot" in action_dict:
826 self.conn.reboot_instances(vm_id)
827 return vm_id
828 except Exception as e:
829 self.format_vimconn_exception(e)