b8cc962bd4041947883c5a24ce5b52845c956b9e
[osm/RO.git] / RO-VIM-aws / osm_rovim_aws / 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 import logging
28 import random
29 import time
30 import traceback
31
32 import boto
33 import boto.ec2
34 from boto.exception import BotoServerError, EC2ResponseError
35 import boto.vpc
36 from ipconflict import check_conflicts
37 import netaddr
38 from osm_ro_plugin import vimconn
39 import yaml
40
41 __author__ = "Saboor Ahmad"
42 __date__ = "10-Apr-2017"
43
44
45 class vimconnector(vimconn.VimConnector):
46 def __init__(
47 self,
48 uuid,
49 name,
50 tenant_id,
51 tenant_name,
52 url,
53 url_admin=None,
54 user=None,
55 passwd=None,
56 log_level=None,
57 config={},
58 persistent_info={},
59 ):
60 """Params:
61 uuid - id asigned to this VIM
62 name - name assigned to this VIM, can be used for logging
63 tenant_id - ID to be used for tenant
64 tenant_name - name of tenant to be used VIM tenant to be used
65 url_admin - optional, url used for administrative tasks
66 user - credentials of the VIM user
67 passwd - credentials of the VIM user
68 log_level - if must use a different log_level than the general one
69 config - dictionary with misc VIM information
70 region_name - name of region to deploy the instances
71 vpc_cidr_block - default CIDR block for VPC
72 security_groups - default security group to specify this instance
73 persistent_info - dict where the class can store information that will be available among class
74 destroy/creation cycles. This info is unique per VIM/credential. At first call it will contain an
75 empty dict. Useful to store login/tokens information for speed up communication
76 """
77 vimconn.VimConnector.__init__(
78 self,
79 uuid,
80 name,
81 tenant_id,
82 tenant_name,
83 url,
84 url_admin,
85 user,
86 passwd,
87 log_level,
88 config,
89 persistent_info,
90 )
91
92 self.persistent_info = persistent_info
93 self.a_creds = {}
94
95 if user:
96 self.a_creds["aws_access_key_id"] = user
97 else:
98 raise vimconn.VimConnAuthException("Username is not specified")
99
100 if passwd:
101 self.a_creds["aws_secret_access_key"] = passwd
102 else:
103 raise vimconn.VimConnAuthException("Password is not specified")
104
105 if "region_name" in config:
106 self.region = config.get("region_name")
107 else:
108 raise vimconn.VimConnException("AWS region_name is not specified at config")
109
110 self.vpc_data = {}
111 self.subnet_data = {}
112 self.conn = None
113 self.conn_vpc = None
114 self.account_id = None
115 self.network_delete_on_termination = []
116 self.server_timeout = 180
117
118 self.vpc_id = self.get_tenant_list()[0]["id"]
119 # we take VPC CIDR block if specified, otherwise we use the default CIDR
120 # block suggested by AWS while creating instance
121 self.vpc_cidr_block = "10.0.0.0/24"
122
123 if tenant_name:
124 self.vpc_id = tenant_name
125
126 if "vpc_cidr_block" in config:
127 self.vpc_cidr_block = config["vpc_cidr_block"]
128
129 self.security_groups = None
130 if "security_groups" in config:
131 self.security_groups = config["security_groups"]
132
133 self.key_pair = None
134 if "key_pair" in config:
135 self.key_pair = config["key_pair"]
136
137 self.flavor_info = None
138 if "flavor_info" in config:
139 flavor_data = config.get("flavor_info")
140 if isinstance(flavor_data, str):
141 try:
142 if flavor_data[0] == "@": # read from a file
143 with open(flavor_data[1:], "r") as stream:
144 self.flavor_info = yaml.safe_load(stream)
145 else:
146 self.flavor_info = yaml.safe_load(flavor_data)
147 except yaml.YAMLError as e:
148 self.flavor_info = None
149
150 raise vimconn.VimConnException(
151 "Bad format at file '{}': {}".format(flavor_data[1:], e)
152 )
153 except IOError as e:
154 raise vimconn.VimConnException(
155 "Error reading file '{}': {}".format(flavor_data[1:], e)
156 )
157 elif isinstance(flavor_data, dict):
158 self.flavor_info = flavor_data
159
160 self.logger = logging.getLogger("ro.vim.aws")
161
162 if log_level:
163 self.logger.setLevel(getattr(logging, log_level))
164
165 def __setitem__(self, index, value):
166 """Params:
167 index - name of value of set
168 value - value to set
169 """
170 if index == "user":
171 self.a_creds["aws_access_key_id"] = value
172 elif index == "passwd":
173 self.a_creds["aws_secret_access_key"] = value
174 elif index == "region":
175 self.region = value
176 else:
177 vimconn.VimConnector.__setitem__(self, index, value)
178
179 def _reload_connection(self):
180 """Returns: sets boto.EC2 and boto.VPC connection to work with AWS services"""
181 try:
182 self.conn = boto.ec2.connect_to_region(
183 self.region,
184 aws_access_key_id=self.a_creds["aws_access_key_id"],
185 aws_secret_access_key=self.a_creds["aws_secret_access_key"],
186 )
187 self.conn_vpc = boto.vpc.connect_to_region(
188 self.region,
189 aws_access_key_id=self.a_creds["aws_access_key_id"],
190 aws_secret_access_key=self.a_creds["aws_secret_access_key"],
191 )
192 # client = boto3.client("sts", aws_access_key_id=self.a_creds['aws_access_key_id'],
193 # aws_secret_access_key=self.a_creds['aws_secret_access_key'])
194 # self.account_id = client.get_caller_identity()["Account"]
195 except Exception as e:
196 self.format_vimconn_exception(e)
197
198 def format_vimconn_exception(self, e):
199 """Params: an Exception object
200 Returns: Raises the exception 'e' passed in mehtod parameters
201 """
202 self.conn = None
203 self.conn_vpc = None
204
205 raise vimconn.VimConnConnectionException(type(e).__name__ + ": " + str(e))
206
207 def get_availability_zones_list(self):
208 """Obtain AvailabilityZones from AWS"""
209 try:
210 self._reload_connection()
211 az_list = []
212
213 for az in self.conn.get_all_zones():
214 az_list.append(az.name)
215
216 return az_list
217 except Exception as e:
218 self.format_vimconn_exception(e)
219
220 def get_tenant_list(self, filter_dict={}):
221 """Obtain tenants of VIM
222 filter_dict dictionary that can contain the following keys:
223 name: filter by tenant name
224 id: filter by tenant uuid/id
225 <other VIM specific>
226 Returns the tenant list of dictionaries, and empty list if no tenant match all the filers:
227 [{'name':'<name>, 'id':'<id>, ...}, ...]
228 """
229 try:
230 self._reload_connection()
231 vpc_ids = []
232
233 if filter_dict != {}:
234 if "id" in filter_dict:
235 vpc_ids.append(filter_dict["id"])
236
237 tenants = self.conn_vpc.get_all_vpcs(vpc_ids, None)
238 tenant_list = []
239
240 for tenant in tenants:
241 tenant_list.append(
242 {
243 "id": str(tenant.id),
244 "name": str(tenant.id),
245 "status": str(tenant.state),
246 "cidr_block": str(tenant.cidr_block),
247 }
248 )
249
250 return tenant_list
251 except Exception as e:
252 self.format_vimconn_exception(e)
253
254 def new_tenant(self, tenant_name, tenant_description):
255 """Adds a new tenant to VIM with this name and description, this is done using admin_url if provided
256 "tenant_name": string max lenght 64
257 "tenant_description": string max length 256
258 returns the tenant identifier or raise exception
259 """
260 self.logger.debug("Adding a new VPC")
261
262 try:
263 self._reload_connection()
264 vpc = self.conn_vpc.create_vpc(self.vpc_cidr_block)
265 self.conn_vpc.modify_vpc_attribute(vpc.id, enable_dns_support=True)
266 self.conn_vpc.modify_vpc_attribute(vpc.id, enable_dns_hostnames=True)
267
268 gateway = self.conn_vpc.create_internet_gateway()
269 self.conn_vpc.attach_internet_gateway(gateway.id, vpc.id)
270 route_table = self.conn_vpc.create_route_table(vpc.id)
271 self.conn_vpc.create_route(route_table.id, "0.0.0.0/0", gateway.id)
272
273 self.vpc_data[vpc.id] = {
274 "gateway": gateway.id,
275 "route_table": route_table.id,
276 "subnets": self.subnet_sizes(self.vpc_cidr_block),
277 }
278
279 return vpc.id
280 except Exception as e:
281 self.format_vimconn_exception(e)
282
283 def delete_tenant(self, tenant_id):
284 """Delete a tenant from VIM
285 tenant_id: returned VIM tenant_id on "new_tenant"
286 Returns None on success. Raises and exception of failure. If tenant is not found raises vimconnNotFoundException
287 """
288 self.logger.debug("Deleting specified VPC")
289
290 try:
291 self._reload_connection()
292 vpc = self.vpc_data.get(tenant_id)
293
294 if "gateway" in vpc and "route_table" in vpc:
295 gateway_id, route_table_id = vpc["gateway"], vpc["route_table"]
296 self.conn_vpc.detach_internet_gateway(gateway_id, tenant_id)
297 self.conn_vpc.delete_vpc(tenant_id)
298 self.conn_vpc.delete_route(route_table_id, "0.0.0.0/0")
299 else:
300 self.conn_vpc.delete_vpc(tenant_id)
301 except Exception as e:
302 self.format_vimconn_exception(e)
303
304 def subnet_sizes(self, cidr):
305 """Calculates possible subnets given CIDR value of VPC"""
306 netmasks = (
307 "255.255.0.0",
308 "255.255.128.0",
309 "255.255.192.0",
310 "255.255.224.0",
311 "255.255.240.0",
312 "255.255.248.0",
313 )
314
315 ip = netaddr.IPNetwork(cidr)
316 mask = ip.netmask
317 pub_split = ()
318
319 for netmask in netmasks:
320 if str(mask) == netmask:
321 pub_split = list(ip.subnet(24))
322 break
323
324 subnets = pub_split if pub_split else (list(ip.subnet(28)))
325
326 return map(str, subnets)
327
328 def new_network(
329 self,
330 net_name,
331 net_type,
332 ip_profile=None,
333 shared=False,
334 provider_network_profile=None,
335 ):
336 """Adds a tenant network to VIM
337 Params:
338 'net_name': name of the network
339 'net_type': one of:
340 'bridge': overlay isolated network
341 'data': underlay E-LAN network for Passthrough and SRIOV interfaces
342 'ptp': underlay E-LINE network for Passthrough and SRIOV interfaces.
343 'ip_profile': is a dict containing the IP parameters of the network (Currently only IPv4 is implemented)
344 'ip-version': can be one of ["IPv4","IPv6"]
345 'subnet-address': ip_prefix_schema, that is X.X.X.X/Y
346 'gateway-address': (Optional) ip_schema, that is X.X.X.X
347 'dns-address': (Optional) ip_schema,
348 'dhcp': (Optional) dict containing
349 'enabled': {"type": "boolean"},
350 'start-address': ip_schema, first IP to grant
351 'count': number of IPs to grant.
352 'shared': if this network can be seen/use by other tenants/organization
353 Returns a tuple with the network identifier and created_items, or raises an exception on error
354 created_items can be None or a dictionary where this method can include key-values that will be passed to
355 the method delete_network. Can be used to store created segments, created l2gw connections, etc.
356 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
357 as not present.
358 """
359 self.logger.debug("Adding a subnet to VPC")
360
361 try:
362 created_items = {}
363 self._reload_connection()
364 subnet = None
365 vpc_id = self.vpc_id
366 if self.conn_vpc.get_all_subnets():
367 existing_subnet = self.conn_vpc.get_all_subnets()[0]
368 if not self.availability_zone:
369 self.availability_zone = str(existing_subnet.availability_zone)
370
371 if self.vpc_data.get(vpc_id, None):
372 cidr_block = list(
373 set(self.vpc_data[vpc_id]["subnets"])
374 - set(
375 self.get_network_details(
376 {"tenant_id": vpc_id}, detail="cidr_block"
377 )
378 )
379 )
380 else:
381 vpc = self.get_tenant_list({"id": vpc_id})[0]
382 subnet_list = self.subnet_sizes(vpc["cidr_block"])
383 cidr_block = list(
384 set(subnet_list)
385 - set(
386 self.get_network_details(
387 {"tenant_id": vpc["id"]}, detail="cidr_block"
388 )
389 )
390 )
391
392 try:
393 selected_cidr_block = random.choice(cidr_block)
394 retry = 15
395 while retry > 0:
396 all_subnets = [
397 subnet.cidr_block for subnet in self.conn_vpc.get_all_subnets()
398 ]
399 all_subnets.append(selected_cidr_block)
400 conflict = check_conflicts(all_subnets)
401 if not conflict:
402 subnet = self.conn_vpc.create_subnet(
403 vpc_id, selected_cidr_block, self.availability_zone
404 )
405 break
406 retry -= 1
407 selected_cidr_block = random.choice(cidr_block)
408 else:
409 raise vimconn.VimConnException(
410 "Failed to find a proper CIDR which does not overlap"
411 "with existing subnets",
412 http_code=vimconn.HTTP_Request_Timeout,
413 )
414
415 except (EC2ResponseError, BotoServerError) as error:
416 self.format_vimconn_exception(error)
417
418 created_items["net:" + str(subnet.id)] = True
419
420 return subnet.id, created_items
421 except Exception as e:
422 self.format_vimconn_exception(e)
423
424 def get_network_details(self, filters, detail):
425 """Get specified details related to a subnet"""
426 detail_list = []
427 subnet_list = self.get_network_list(filters)
428
429 for net in subnet_list:
430 detail_list.append(net[detail])
431
432 return detail_list
433
434 def get_network_list(self, filter_dict={}):
435 """Obtain tenant networks of VIM
436 Params:
437 'filter_dict' (optional) contains entries to return only networks that matches ALL entries:
438 name: string => returns only networks with this name
439 id: string => returns networks with this VIM id, this imply returns one network at most
440 shared: boolean >= returns only networks that are (or are not) shared
441 tenant_id: sting => returns only networks that belong to this tenant/project
442 ,#(not used yet) admin_state_up: boolean => returns only networks that are (or are not) in admin
443 state active
444 #(not used yet) status: 'ACTIVE','ERROR',... => filter networks that are on this status
445 Returns the network list of dictionaries. each dictionary contains:
446 'id': (mandatory) VIM network id
447 'name': (mandatory) VIM network name
448 'status': (mandatory) can be 'ACTIVE', 'INACTIVE', 'DOWN', 'BUILD', 'ERROR', 'VIM_ERROR', 'OTHER'
449 'error_msg': (optional) text that explains the ERROR status
450 other VIM specific fields: (optional) whenever possible using the same naming of filter_dict param
451 List can be empty if no network map the filter_dict. Raise an exception only upon VIM connectivity,
452 authorization, or some other unspecific error
453 """
454 self.logger.debug("Getting all subnets from VIM")
455
456 try:
457 self._reload_connection()
458 tfilters = {}
459
460 if filter_dict != {}:
461 if "tenant_id" in filter_dict:
462 tfilters["vpcId"] = filter_dict.get("tenant_id")
463
464 subnets = self.conn_vpc.get_all_subnets(
465 subnet_ids=filter_dict.get("SubnetId", None), filters=tfilters
466 )
467
468 net_list = []
469
470 for net in subnets:
471 if net.id == filter_dict.get("name"):
472 self.availability_zone = str(net.availability_zone)
473 net_list.append(
474 {
475 "id": str(net.id),
476 "name": str(net.id),
477 "status": str(net.state),
478 "vpc_id": str(net.vpc_id),
479 "cidr_block": str(net.cidr_block),
480 "type": "bridge",
481 }
482 )
483
484 return net_list
485 except Exception as e:
486 self.format_vimconn_exception(e)
487
488 def get_network(self, net_id):
489 """Obtain network details from the 'net_id' VIM network
490 Return a dict that contains:
491 'id': (mandatory) VIM network id, that is, net_id
492 'name': (mandatory) VIM network name
493 'status': (mandatory) can be 'ACTIVE', 'INACTIVE', 'DOWN', 'BUILD', 'ERROR', 'VIM_ERROR', 'OTHER'
494 'error_msg': (optional) text that explains the ERROR status
495 other VIM specific fields: (optional) whenever possible using the same naming of filter_dict param
496 Raises an exception upon error or when network is not found
497 """
498 self.logger.debug("Getting Subnet from VIM")
499
500 try:
501 self._reload_connection()
502 subnet = self.conn_vpc.get_all_subnets(net_id)[0]
503 return {
504 "id": str(subnet.id),
505 "name": str(subnet.id),
506 "status": str(subnet.state),
507 "vpc_id": str(subnet.vpc_id),
508 "cidr_block": str(subnet.cidr_block),
509 "availability_zone": str(subnet.availability_zone),
510 }
511 except Exception as e:
512 self.format_vimconn_exception(e)
513
514 def delete_network(self, net_id, created_items=None):
515 """
516 Removes a tenant network from VIM and its associated elements
517 :param net_id: VIM identifier of the network, provided by method new_network
518 :param created_items: dictionary with extra items to be deleted. provided by method new_network
519 Returns the network identifier or raises an exception upon error or when network is not found
520 """
521 self.logger.debug("Deleting subnet from VIM")
522
523 try:
524 self._reload_connection()
525 self.logger.debug("DELETING NET_ID: " + str(net_id))
526 self.conn_vpc.delete_subnet(net_id)
527
528 return net_id
529
530 except Exception as e:
531 if isinstance(e, EC2ResponseError):
532 self.network_delete_on_termination.append(net_id)
533 self.logger.warning(
534 f"{net_id} could not be deleted, deletion will retry after dependencies resolved"
535 )
536 else:
537 self.format_vimconn_exception(e)
538
539 def refresh_nets_status(self, net_list):
540 """Get the status of the networks
541 Params:
542 'net_list': a list with the VIM network id to be get the status
543 Returns a dictionary with:
544 'net_id': #VIM id of this network
545 status: #Mandatory. Text with one of:
546 # DELETED (not found at vim)
547 # VIM_ERROR (Cannot connect to VIM, authentication problems, VIM response error, ...)
548 # OTHER (Vim reported other status not understood)
549 # ERROR (VIM indicates an ERROR status)
550 # ACTIVE, INACTIVE, DOWN (admin down),
551 # BUILD (on building process)
552 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
553 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
554 'net_id2': ...
555 """
556 self._reload_connection()
557
558 try:
559 dict_entry = {}
560
561 for net_id in net_list:
562 subnet_dict = {}
563 subnet = None
564
565 try:
566 subnet = self.conn_vpc.get_all_subnets(net_id)[0]
567
568 if subnet.state == "pending":
569 subnet_dict["status"] = "BUILD"
570 elif subnet.state == "available":
571 subnet_dict["status"] = "ACTIVE"
572 else:
573 subnet_dict["status"] = "ERROR"
574 subnet_dict["error_msg"] = ""
575 except Exception:
576 subnet_dict["status"] = "DELETED"
577 subnet_dict["error_msg"] = "Network not found"
578 finally:
579 subnet_dictionary = vars(subnet)
580 cleared_subnet_dict = {
581 key: subnet_dictionary[key]
582 for key in subnet_dictionary
583 if not isinstance(subnet_dictionary[key], object)
584 }
585 subnet_dict["vim_info"] = cleared_subnet_dict
586 dict_entry[net_id] = subnet_dict
587
588 return dict_entry
589 except Exception as e:
590 self.format_vimconn_exception(e)
591
592 def get_flavor(self, flavor_id):
593 """Obtain flavor details from the VIM
594 Returns the flavor dict details {'id':<>, 'name':<>, other vim specific }
595 Raises an exception upon error or if not found
596 """
597 self.logger.debug("Getting instance type")
598
599 try:
600 if flavor_id in self.flavor_info:
601 return self.flavor_info[flavor_id]
602 else:
603 raise vimconn.VimConnNotFoundException(
604 "Cannot find flavor with this flavor ID/Name"
605 )
606 except Exception as e:
607 self.format_vimconn_exception(e)
608
609 def new_image(self, image_dict):
610 """Adds a tenant image to VIM
611 Params: image_dict
612 name (string) - The name of the AMI. Valid only for EBS-based images.
613 description (string) - The description of the AMI.
614 image_location (string) - Full path to your AMI manifest in Amazon S3 storage. Only used for S3-based AMI’s.
615 architecture (string) - The architecture of the AMI. Valid choices are: * i386 * x86_64
616 kernel_id (string) - The ID of the kernel with which to launch the instances
617 root_device_name (string) - The root device name (e.g. /dev/sdh)
618 block_device_map (boto.ec2.blockdevicemapping.BlockDeviceMapping) - A BlockDeviceMapping data structure
619 describing the EBS volumes associated with the Image.
620 virtualization_type (string) - The virutalization_type of the image. Valid choices are: * paravirtual * hvm
621 sriov_net_support (string) - Advanced networking support. Valid choices are: * simple
622 snapshot_id (string) - A snapshot ID for the snapshot to be used as root device for the image. Mutually
623 exclusive with block_device_map, requires root_device_name
624 delete_root_volume_on_termination (bool) - Whether to delete the root volume of the image after instance
625 termination. Only applies when creating image from snapshot_id. Defaults to False. Note that leaving
626 volumes behind after instance termination is not free
627 Returns: image_id - image ID of the newly created image
628 """
629 try:
630 self._reload_connection()
631 image_location = image_dict.get("image_location", None)
632
633 if image_location:
634 image_location = str(self.account_id) + str(image_location)
635
636 image_id = self.conn.register_image(
637 image_dict.get("name", None),
638 image_dict.get("description", None),
639 image_location,
640 image_dict.get("architecture", None),
641 image_dict.get("kernel_id", None),
642 image_dict.get("root_device_name", None),
643 image_dict.get("block_device_map", None),
644 image_dict.get("virtualization_type", None),
645 image_dict.get("sriov_net_support", None),
646 image_dict.get("snapshot_id", None),
647 image_dict.get("delete_root_volume_on_termination", None),
648 )
649
650 return image_id
651 except Exception as e:
652 self.format_vimconn_exception(e)
653
654 def delete_image(self, image_id):
655 """Deletes a tenant image from VIM
656 Returns the image_id if image is deleted or raises an exception on error"""
657
658 try:
659 self._reload_connection()
660 self.conn.deregister_image(image_id)
661
662 return image_id
663 except Exception as e:
664 self.format_vimconn_exception(e)
665
666 def get_image_id_from_path(self, path):
667 """
668 Params: path - location of the image
669 Returns: image_id - ID of the matching image
670 """
671 self._reload_connection()
672 try:
673 filters = {}
674
675 if path:
676 tokens = path.split("/")
677 filters["owner_id"] = tokens[0]
678 filters["name"] = "/".join(tokens[1:])
679
680 image = self.conn.get_all_images(filters=filters)[0]
681
682 return image.id
683 except Exception as e:
684 self.format_vimconn_exception(e)
685
686 def get_image_list(self, filter_dict={}):
687 """Obtain tenant images from VIM
688 Filter_dict can be:
689 name: image name
690 id: image uuid
691 checksum: image checksum
692 location: image path
693 Returns the image list of dictionaries:
694 [{<the fields at Filter_dict plus some VIM specific>}, ...]
695 List can be empty
696 """
697 self.logger.debug("Getting image list from VIM")
698
699 try:
700 self._reload_connection()
701 image_id = None
702 filters = {}
703
704 if "id" in filter_dict:
705 image_id = filter_dict["id"]
706
707 if "name" in filter_dict:
708 filters["name"] = filter_dict["name"]
709
710 if "location" in filter_dict:
711 filters["location"] = filter_dict["location"]
712
713 # filters['image_type'] = 'machine'
714 # filter_dict['owner_id'] = self.account_id
715 images = self.conn.get_all_images(image_id, filters=filters)
716 image_list = []
717
718 for image in images:
719 image_list.append(
720 {
721 "id": str(image.id),
722 "name": str(image.name),
723 "status": str(image.state),
724 "owner": str(image.owner_id),
725 "location": str(image.location),
726 "is_public": str(image.is_public),
727 "architecture": str(image.architecture),
728 "platform": str(image.platform),
729 }
730 )
731
732 return image_list
733 except Exception as e:
734 self.format_vimconn_exception(e)
735
736 def new_vminstance(
737 self,
738 name,
739 description,
740 start,
741 image_id,
742 flavor_id,
743 affinity_group_list,
744 net_list,
745 cloud_config=None,
746 disk_list=None,
747 availability_zone_index=None,
748 availability_zone_list=None,
749 ):
750 """Create a new VM/instance in AWS
751 Params: name
752 decription
753 start: (boolean) indicates if VM must start or created in pause mode.
754 image_id - image ID in AWS
755 flavor_id - instance type ID in AWS
756 net_list
757 name
758 net_id - subnet_id from AWS
759 vpci - (optional) virtual vPCI address to assign at the VM. Can be ignored depending on VIM
760 capabilities
761 model: (optional and only have sense for type==virtual) interface model: virtio, e1000, ...
762 mac_address: (optional) mac address to assign to this interface
763 type: (mandatory) can be one of:
764 virtual, in this case always connected to a network of type 'net_type=bridge'
765 'PCI-PASSTHROUGH' or 'PF' (passthrough): depending on VIM capabilities it can be connected to a
766 data/ptp network ot it
767 can created unconnected
768 'SR-IOV' or 'VF' (SRIOV with VLAN tag): same as PF for network connectivity.
769 VFnotShared - (SRIOV without VLAN tag) same as PF for network connectivity. VF where no other
770 VFs are allocated on the same physical NIC
771 bw': (optional) only for PF/VF/VFnotShared. Minimal Bandwidth required for the interface in GBPS
772 port_security': (optional) If False it must avoid any traffic filtering at this interface.
773 If missing or True, it must apply the default VIM behaviour
774 vim_id': must be filled/added by this method with the VIM identifier generated by the VIM for this
775 interface. 'net_list' is modified
776 elastic_ip - True/False to define if an elastic_ip is required
777 cloud_config': (optional) dictionary with:
778 key-pairs': (optional) list of strings with the public key to be inserted to the default user
779 users': (optional) list of users to be inserted, each item is a dict with:
780 name': (mandatory) user name,
781 key-pairs': (optional) list of strings with the public key to be inserted to the user
782 user-data': (optional) string is a text script to be passed directly to cloud-init
783 config-files': (optional). List of files to be transferred. Each item is a dict with:
784 dest': (mandatory) string with the destination absolute path
785 encoding': (optional, by default text). Can be one of:
786 b64', 'base64', 'gz', 'gz+b64', 'gz+base64', 'gzip+b64', 'gzip+base64'
787 content' (mandatory): string with the content of the file
788 permissions': (optional) string with file permissions, typically octal notation '0644'
789 owner: (optional) file owner, string with the format 'owner:group'
790 boot-data-drive: boolean to indicate if user-data must be passed using a boot drive (hard disk)
791 security-groups:
792 subnet_id
793 security_group_id
794 disk_list': (optional) list with additional disks to the VM. Each item is a dict with:
795 image_id': (optional). VIM id of an existing image. If not provided an empty disk must be mounted
796 size': (mandatory) string with the size of the disk in GB
797 Returns a tuple with the instance identifier and created_items or raises an exception on error
798 created_items can be None or a dictionary where this method can include key-values that will be passed to
799 the method delete_vminstance and action_vminstance. Can be used to store created ports, volumes, etc.
800 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
801 as not present.
802 """
803 self.logger.debug("Creating a new VM instance")
804
805 try:
806 created_items = {}
807 self._reload_connection()
808 reservation = None
809 _, userdata = self._create_user_data(cloud_config)
810
811 if not net_list:
812 reservation = self.conn.run_instances(
813 image_id,
814 key_name=self.key_pair,
815 instance_type=flavor_id,
816 security_groups=self.security_groups,
817 user_data=userdata,
818 )
819
820 else:
821 for index, subnet in enumerate(net_list):
822 net_intr = self.conn_vpc.create_network_interface(
823 subnet_id=subnet.get("net_id"),
824 groups=None,
825 )
826
827 interface = boto.ec2.networkinterface.NetworkInterfaceSpecification(
828 network_interface_id=net_intr.id,
829 device_index=index,
830 )
831
832 interfaces = boto.ec2.networkinterface.NetworkInterfaceCollection(
833 interface
834 )
835
836 if subnet.get("elastic_ip"):
837 eip = self.conn.allocate_address()
838 self.conn.associate_address(
839 allocation_id=eip.allocation_id,
840 network_interface_id=net_intr.id,
841 )
842
843 if index == 0:
844 try:
845 reservation = self.conn.run_instances(
846 image_id,
847 key_name=self.key_pair,
848 instance_type=flavor_id,
849 security_groups=self.security_groups,
850 network_interfaces=interfaces,
851 user_data=userdata,
852 )
853 except Exception as instance_create_error:
854 self.logger.debug(traceback.format_exc())
855 self.format_vimconn_exception(instance_create_error)
856
857 if index > 0:
858 try:
859 if reservation:
860 instance_id = self.wait_for_instance_id(reservation)
861 if instance_id and self.wait_for_vm(
862 instance_id, "running"
863 ):
864 self.conn.attach_network_interface(
865 network_interface_id=net_intr.id,
866 instance_id=instance_id,
867 device_index=index,
868 )
869 except Exception as attach_network_error:
870 self.logger.debug(traceback.format_exc())
871 self.format_vimconn_exception(attach_network_error)
872
873 if instance_id := self.wait_for_instance_id(reservation):
874 time.sleep(30)
875 instance_status = self.refresh_vms_status(instance_id)
876 refreshed_instance_status = instance_status.get(instance_id)
877 instance_interfaces = refreshed_instance_status.get(
878 "interfaces"
879 )
880 for idx, interface in enumerate(instance_interfaces):
881 if idx == index:
882 net_list[index]["vim_id"] = instance_interfaces[
883 idx
884 ].get("vim_interface_id")
885
886 instance_id = self.wait_for_instance_id(reservation)
887 created_items["vm_id:" + str(instance_id)] = True
888
889 return instance_id, created_items
890 except Exception as e:
891 self.logger.debug(traceback.format_exc())
892 self.format_vimconn_exception(e)
893
894 def get_vminstance(self, vm_id):
895 """Returns the VM instance information from VIM"""
896 try:
897 self._reload_connection()
898 reservation = self.conn.get_all_instances(vm_id)
899
900 return reservation[0].instances[0].__dict__
901 except Exception as e:
902 self.format_vimconn_exception(e)
903
904 def delete_vminstance(self, vm_id, created_items=None, volumes_to_hold=None):
905 """Removes a VM instance from VIM
906 Returns the instance identifier"""
907 try:
908 self._reload_connection()
909 self.logger.debug("DELETING VM_ID: " + str(vm_id))
910 reservation = self.conn.get_all_instances(vm_id)[0]
911 if hasattr(reservation, "instances"):
912 instance = reservation.instances[0]
913
914 self.conn.terminate_instances(vm_id)
915 if self.wait_for_vm(vm_id, "terminated"):
916 for interface in instance.interfaces:
917 self.conn_vpc.delete_network_interface(
918 network_interface_id=interface.id,
919 )
920 if self.network_delete_on_termination:
921 for net in self.network_delete_on_termination:
922 try:
923 self.conn_vpc.delete_subnet(net)
924 except Exception as net_delete_error:
925 if isinstance(net_delete_error, EC2ResponseError):
926 self.logger.warning(f"Deleting network {net}: failed")
927 else:
928 self.format_vimconn_exception(net_delete_error)
929
930 return vm_id
931 except Exception as e:
932 self.format_vimconn_exception(e)
933
934 def wait_for_instance_id(self, reservation):
935 if not reservation:
936 return False
937
938 self._reload_connection()
939 elapsed_time = 0
940 while elapsed_time < 30:
941 if reservation.instances:
942 instance_id = reservation.instances[0].id
943 return instance_id
944 time.sleep(5)
945 elapsed_time += 5
946 else:
947 raise vimconn.VimConnException(
948 "Failed to get instance_id for reservation",
949 http_code=vimconn.HTTP_Request_Timeout,
950 )
951
952 def wait_for_vm(self, vm_id, status):
953 """wait until vm is in the desired status and return True.
954 If the timeout is reached generate an exception"""
955
956 self._reload_connection()
957
958 elapsed_time = 0
959 while elapsed_time < self.server_timeout:
960 if self.conn.get_all_instances(vm_id):
961 reservation = self.conn.get_all_instances(vm_id)[0]
962 if hasattr(reservation, "instances"):
963 instance = reservation.instances[0]
964 if instance.state == status:
965 return True
966 time.sleep(5)
967 elapsed_time += 5
968
969 # if we exceeded the timeout
970 else:
971 raise vimconn.VimConnException(
972 "Timeout waiting for instance " + vm_id + " to get " + status,
973 http_code=vimconn.HTTP_Request_Timeout,
974 )
975
976 def refresh_vms_status(self, vm_list):
977 """Get the status of the virtual machines and their interfaces/ports
978 Params: the list of VM identifiers
979 Returns a dictionary with:
980 vm_id: #VIM id of this Virtual Machine
981 status: #Mandatory. Text with one of:
982 # DELETED (not found at vim)
983 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
984 # OTHER (Vim reported other status not understood)
985 # ERROR (VIM indicates an ERROR status)
986 # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
987 # BUILD (on building process), ERROR
988 # ACTIVE:NoMgmtIP (Active but any of its interface has an IP address
989 #
990 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
991 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
992 interfaces: list with interface info. Each item a dictionary with:
993 vim_interface_id - The ID of the ENI.
994 vim_net_id - The ID of the VPC subnet.
995 mac_address - The MAC address of the interface.
996 ip_address - The IP address of the interface within the subnet.
997 """
998 self.logger.debug("Getting VM instance information from VIM")
999
1000 try:
1001 self._reload_connection()
1002 elapsed_time = 0
1003 while elapsed_time < self.server_timeout:
1004 reservation = self.conn.get_all_instances(vm_list)[0]
1005 if reservation:
1006 break
1007 time.sleep(5)
1008 elapsed_time += 5
1009
1010 # if we exceeded the timeout
1011 else:
1012 raise vimconn.VimConnException(
1013 vm_list + "could not be gathered, refresh vm status failed",
1014 http_code=vimconn.HTTP_Request_Timeout,
1015 )
1016
1017 instances = {}
1018 instance_dict = {}
1019
1020 for instance in reservation.instances:
1021 if hasattr(instance, "id"):
1022 try:
1023 if instance.state in ("pending"):
1024 instance_dict["status"] = "BUILD"
1025 elif instance.state in ("available", "running", "up"):
1026 instance_dict["status"] = "ACTIVE"
1027 else:
1028 instance_dict["status"] = "ERROR"
1029
1030 instance_dict["error_msg"] = ""
1031 instance_dict["interfaces"] = []
1032
1033 for interface in instance.interfaces:
1034 interface_dict = {
1035 "vim_interface_id": interface.id,
1036 "vim_net_id": interface.subnet_id,
1037 "mac_address": interface.mac_address,
1038 }
1039
1040 if (
1041 hasattr(interface, "publicIp")
1042 and interface.publicIp is not None
1043 ):
1044 interface_dict["ip_address"] = (
1045 interface.publicIp
1046 + ";"
1047 + interface.private_ip_address
1048 )
1049 else:
1050 interface_dict[
1051 "ip_address"
1052 ] = interface.private_ip_address
1053
1054 instance_dict["interfaces"].append(interface_dict)
1055 except Exception as e:
1056 self.logger.error(
1057 "Exception getting vm status: %s", str(e), exc_info=True
1058 )
1059 instance_dict["status"] = "DELETED"
1060 instance_dict["error_msg"] = str(e)
1061 finally:
1062 instance_dictionary = vars(instance)
1063 cleared_instance_dict = {
1064 key: instance_dictionary[key]
1065 for key in instance_dictionary
1066 if not (isinstance(instance_dictionary[key], object))
1067 }
1068 instance_dict["vim_info"] = cleared_instance_dict
1069
1070 instances[instance.id] = instance_dict
1071
1072 return instances
1073 except Exception as e:
1074 self.logger.error("Exception getting vm status: %s", str(e), exc_info=True)
1075 self.format_vimconn_exception(e)
1076
1077 def action_vminstance(self, vm_id, action_dict, created_items={}):
1078 """Send and action over a VM instance from VIM
1079 Returns the vm_id if the action was successfully sent to the VIM"""
1080
1081 self.logger.debug("Action over VM '%s': %s", vm_id, str(action_dict))
1082 try:
1083 self._reload_connection()
1084 if "start" in action_dict:
1085 self.conn.start_instances(vm_id)
1086 elif "stop" in action_dict or "stop" in action_dict:
1087 self.conn.stop_instances(vm_id)
1088 elif "terminate" in action_dict:
1089 self.conn.terminate_instances(vm_id)
1090 elif "reboot" in action_dict:
1091 self.conn.reboot_instances(vm_id)
1092
1093 return None
1094 except Exception as e:
1095 self.format_vimconn_exception(e)
1096
1097 def migrate_instance(self, vm_id, compute_host=None):
1098 """
1099 Migrate a vdu
1100 param:
1101 vm_id: ID of an instance
1102 compute_host: Host to migrate the vdu to
1103 """
1104 # TODO: Add support for migration
1105 raise vimconn.VimConnNotImplemented("Not implemented")
1106
1107 def resize_instance(self, vm_id, flavor_id=None):
1108 """
1109 resize a vdu
1110 param:
1111 vm_id: ID of an instance
1112 flavor_id: flavor to resize the vdu
1113 """
1114 # TODO: Add support for resize
1115 raise vimconn.VimConnNotImplemented("Not implemented")