Fixing Bug 1851, 1852, 1854
[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.load(stream, Loader=yaml.Loader)
145 else:
146 self.flavor_info = yaml.load(flavor_data, Loader=yaml.Loader)
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 get_flavor_id_from_data(self, flavor_dict):
610 """Obtain flavor id that match the flavor description
611 Params:
612 'flavor_dict': dictionary that contains:
613 'disk': main hard disk in GB
614 'ram': memory in MB
615 'vcpus': number of virtual cpus
616 #todo: complete parameters for EPA
617 Returns the flavor_id or raises a vimconnNotFoundException
618 """
619 self.logger.debug("Getting flavor id from data")
620
621 try:
622 flavor = None
623 for key, values in self.flavor_info.items():
624 if (values["ram"], values["cpus"], values["disk"]) == (
625 flavor_dict["ram"],
626 flavor_dict["vcpus"],
627 flavor_dict["disk"],
628 ):
629 flavor = (key, values)
630 break
631 elif (values["ram"], values["cpus"], values["disk"]) >= (
632 flavor_dict["ram"],
633 flavor_dict["vcpus"],
634 flavor_dict["disk"],
635 ):
636 if not flavor:
637 flavor = (key, values)
638 else:
639 if (flavor[1]["ram"], flavor[1]["cpus"], flavor[1]["disk"]) >= (
640 values["ram"],
641 values["cpus"],
642 values["disk"],
643 ):
644 flavor = (key, values)
645
646 if flavor:
647 return flavor[0]
648
649 raise vimconn.VimConnNotFoundException(
650 "Cannot find flavor with this flavor ID/Name"
651 )
652 except Exception as e:
653 self.format_vimconn_exception(e)
654
655 def new_image(self, image_dict):
656 """Adds a tenant image to VIM
657 Params: image_dict
658 name (string) - The name of the AMI. Valid only for EBS-based images.
659 description (string) - The description of the AMI.
660 image_location (string) - Full path to your AMI manifest in Amazon S3 storage. Only used for S3-based AMI’s.
661 architecture (string) - The architecture of the AMI. Valid choices are: * i386 * x86_64
662 kernel_id (string) - The ID of the kernel with which to launch the instances
663 root_device_name (string) - The root device name (e.g. /dev/sdh)
664 block_device_map (boto.ec2.blockdevicemapping.BlockDeviceMapping) - A BlockDeviceMapping data structure
665 describing the EBS volumes associated with the Image.
666 virtualization_type (string) - The virutalization_type of the image. Valid choices are: * paravirtual * hvm
667 sriov_net_support (string) - Advanced networking support. Valid choices are: * simple
668 snapshot_id (string) - A snapshot ID for the snapshot to be used as root device for the image. Mutually
669 exclusive with block_device_map, requires root_device_name
670 delete_root_volume_on_termination (bool) - Whether to delete the root volume of the image after instance
671 termination. Only applies when creating image from snapshot_id. Defaults to False. Note that leaving
672 volumes behind after instance termination is not free
673 Returns: image_id - image ID of the newly created image
674 """
675 try:
676 self._reload_connection()
677 image_location = image_dict.get("image_location", None)
678
679 if image_location:
680 image_location = str(self.account_id) + str(image_location)
681
682 image_id = self.conn.register_image(
683 image_dict.get("name", None),
684 image_dict.get("description", None),
685 image_location,
686 image_dict.get("architecture", None),
687 image_dict.get("kernel_id", None),
688 image_dict.get("root_device_name", None),
689 image_dict.get("block_device_map", None),
690 image_dict.get("virtualization_type", None),
691 image_dict.get("sriov_net_support", None),
692 image_dict.get("snapshot_id", None),
693 image_dict.get("delete_root_volume_on_termination", None),
694 )
695
696 return image_id
697 except Exception as e:
698 self.format_vimconn_exception(e)
699
700 def delete_image(self, image_id):
701 """Deletes a tenant image from VIM
702 Returns the image_id if image is deleted or raises an exception on error"""
703
704 try:
705 self._reload_connection()
706 self.conn.deregister_image(image_id)
707
708 return image_id
709 except Exception as e:
710 self.format_vimconn_exception(e)
711
712 def get_image_id_from_path(self, path):
713 """
714 Params: path - location of the image
715 Returns: image_id - ID of the matching image
716 """
717 self._reload_connection()
718 try:
719 filters = {}
720
721 if path:
722 tokens = path.split("/")
723 filters["owner_id"] = tokens[0]
724 filters["name"] = "/".join(tokens[1:])
725
726 image = self.conn.get_all_images(filters=filters)[0]
727
728 return image.id
729 except Exception as e:
730 self.format_vimconn_exception(e)
731
732 def get_image_list(self, filter_dict={}):
733 """Obtain tenant images from VIM
734 Filter_dict can be:
735 name: image name
736 id: image uuid
737 checksum: image checksum
738 location: image path
739 Returns the image list of dictionaries:
740 [{<the fields at Filter_dict plus some VIM specific>}, ...]
741 List can be empty
742 """
743 self.logger.debug("Getting image list from VIM")
744
745 try:
746 self._reload_connection()
747 image_id = None
748 filters = {}
749
750 if "id" in filter_dict:
751 image_id = filter_dict["id"]
752
753 if "name" in filter_dict:
754 filters["name"] = filter_dict["name"]
755
756 if "location" in filter_dict:
757 filters["location"] = filter_dict["location"]
758
759 # filters['image_type'] = 'machine'
760 # filter_dict['owner_id'] = self.account_id
761 images = self.conn.get_all_images(image_id, filters=filters)
762 image_list = []
763
764 for image in images:
765 image_list.append(
766 {
767 "id": str(image.id),
768 "name": str(image.name),
769 "status": str(image.state),
770 "owner": str(image.owner_id),
771 "location": str(image.location),
772 "is_public": str(image.is_public),
773 "architecture": str(image.architecture),
774 "platform": str(image.platform),
775 }
776 )
777
778 return image_list
779 except Exception as e:
780 self.format_vimconn_exception(e)
781
782 def new_vminstance(
783 self,
784 name,
785 description,
786 start,
787 image_id,
788 flavor_id,
789 affinity_group_list,
790 net_list,
791 cloud_config=None,
792 disk_list=None,
793 availability_zone_index=None,
794 availability_zone_list=None,
795 ):
796 """Create a new VM/instance in AWS
797 Params: name
798 decription
799 start: (boolean) indicates if VM must start or created in pause mode.
800 image_id - image ID in AWS
801 flavor_id - instance type ID in AWS
802 net_list
803 name
804 net_id - subnet_id from AWS
805 vpci - (optional) virtual vPCI address to assign at the VM. Can be ignored depending on VIM
806 capabilities
807 model: (optional and only have sense for type==virtual) interface model: virtio, e1000, ...
808 mac_address: (optional) mac address to assign to this interface
809 type: (mandatory) can be one of:
810 virtual, in this case always connected to a network of type 'net_type=bridge'
811 'PCI-PASSTHROUGH' or 'PF' (passthrough): depending on VIM capabilities it can be connected to a
812 data/ptp network ot it
813 can created unconnected
814 'SR-IOV' or 'VF' (SRIOV with VLAN tag): same as PF for network connectivity.
815 VFnotShared - (SRIOV without VLAN tag) same as PF for network connectivity. VF where no other
816 VFs are allocated on the same physical NIC
817 bw': (optional) only for PF/VF/VFnotShared. Minimal Bandwidth required for the interface in GBPS
818 port_security': (optional) If False it must avoid any traffic filtering at this interface.
819 If missing or True, it must apply the default VIM behaviour
820 vim_id': must be filled/added by this method with the VIM identifier generated by the VIM for this
821 interface. 'net_list' is modified
822 elastic_ip - True/False to define if an elastic_ip is required
823 cloud_config': (optional) dictionary with:
824 key-pairs': (optional) list of strings with the public key to be inserted to the default user
825 users': (optional) list of users to be inserted, each item is a dict with:
826 name': (mandatory) user name,
827 key-pairs': (optional) list of strings with the public key to be inserted to the user
828 user-data': (optional) string is a text script to be passed directly to cloud-init
829 config-files': (optional). List of files to be transferred. Each item is a dict with:
830 dest': (mandatory) string with the destination absolute path
831 encoding': (optional, by default text). Can be one of:
832 b64', 'base64', 'gz', 'gz+b64', 'gz+base64', 'gzip+b64', 'gzip+base64'
833 content' (mandatory): string with the content of the file
834 permissions': (optional) string with file permissions, typically octal notation '0644'
835 owner: (optional) file owner, string with the format 'owner:group'
836 boot-data-drive: boolean to indicate if user-data must be passed using a boot drive (hard disk)
837 security-groups:
838 subnet_id
839 security_group_id
840 disk_list': (optional) list with additional disks to the VM. Each item is a dict with:
841 image_id': (optional). VIM id of an existing image. If not provided an empty disk must be mounted
842 size': (mandatory) string with the size of the disk in GB
843 Returns a tuple with the instance identifier and created_items or raises an exception on error
844 created_items can be None or a dictionary where this method can include key-values that will be passed to
845 the method delete_vminstance and action_vminstance. Can be used to store created ports, volumes, etc.
846 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
847 as not present.
848 """
849 self.logger.debug("Creating a new VM instance")
850
851 try:
852 created_items = {}
853 self._reload_connection()
854 reservation = None
855 _, userdata = self._create_user_data(cloud_config)
856
857 if not net_list:
858 reservation = self.conn.run_instances(
859 image_id,
860 key_name=self.key_pair,
861 instance_type=flavor_id,
862 security_groups=self.security_groups,
863 user_data=userdata,
864 )
865
866 else:
867 for index, subnet in enumerate(net_list):
868
869 net_intr = self.conn_vpc.create_network_interface(
870 subnet_id=subnet.get("net_id"),
871 groups=None,
872 )
873
874 interface = boto.ec2.networkinterface.NetworkInterfaceSpecification(
875 network_interface_id=net_intr.id,
876 device_index=index,
877 )
878
879 interfaces = boto.ec2.networkinterface.NetworkInterfaceCollection(
880 interface
881 )
882
883 if subnet.get("elastic_ip"):
884 eip = self.conn.allocate_address()
885 self.conn.associate_address(
886 allocation_id=eip.allocation_id,
887 network_interface_id=net_intr.id,
888 )
889
890 if index == 0:
891 try:
892 reservation = self.conn.run_instances(
893 image_id,
894 key_name=self.key_pair,
895 instance_type=flavor_id,
896 security_groups=self.security_groups,
897 network_interfaces=interfaces,
898 user_data=userdata,
899 )
900 except Exception as instance_create_error:
901 self.logger.debug(traceback.format_exc())
902 self.format_vimconn_exception(instance_create_error)
903
904 if index > 0:
905 try:
906 if reservation:
907 instance_id = self.wait_for_instance_id(reservation)
908 if instance_id and self.wait_for_vm(
909 instance_id, "running"
910 ):
911 self.conn.attach_network_interface(
912 network_interface_id=net_intr.id,
913 instance_id=instance_id,
914 device_index=index,
915 )
916 except Exception as attach_network_error:
917 self.logger.debug(traceback.format_exc())
918 self.format_vimconn_exception(attach_network_error)
919
920 if instance_id := self.wait_for_instance_id(reservation):
921 time.sleep(30)
922 instance_status = self.refresh_vms_status(instance_id)
923 refreshed_instance_status = instance_status.get(instance_id)
924 instance_interfaces = refreshed_instance_status.get(
925 "interfaces"
926 )
927 for idx, interface in enumerate(instance_interfaces):
928 if idx == index:
929 net_list[index]["vim_id"] = instance_interfaces[
930 idx
931 ].get("vim_interface_id")
932
933 instance_id = self.wait_for_instance_id(reservation)
934 created_items["vm_id:" + str(instance_id)] = True
935
936 return instance_id, created_items
937 except Exception as e:
938 self.logger.debug(traceback.format_exc())
939 self.format_vimconn_exception(e)
940
941 def get_vminstance(self, vm_id):
942 """Returns the VM instance information from VIM"""
943 try:
944 self._reload_connection()
945 reservation = self.conn.get_all_instances(vm_id)
946
947 return reservation[0].instances[0].__dict__
948 except Exception as e:
949 self.format_vimconn_exception(e)
950
951 def delete_vminstance(self, vm_id, created_items=None, volumes_2hold=None):
952 """Removes a VM instance from VIM
953 Returns the instance identifier"""
954 try:
955 self._reload_connection()
956 self.logger.debug("DELETING VM_ID: " + str(vm_id))
957 reservation = self.conn.get_all_instances(vm_id)[0]
958 if hasattr(reservation, "instances"):
959 instance = reservation.instances[0]
960
961 self.conn.terminate_instances(vm_id)
962 if self.wait_for_vm(vm_id, "terminated"):
963 for interface in instance.interfaces:
964 self.conn_vpc.delete_network_interface(
965 network_interface_id=interface.id,
966 )
967 if self.network_delete_on_termination:
968 for net in self.network_delete_on_termination:
969 try:
970 self.conn_vpc.delete_subnet(net)
971 except Exception as net_delete_error:
972 if isinstance(net_delete_error, EC2ResponseError):
973 self.logger.warning(f"Deleting network {net}: failed")
974 else:
975 self.format_vimconn_exception(net_delete_error)
976
977 return vm_id
978 except Exception as e:
979 self.format_vimconn_exception(e)
980
981 def wait_for_instance_id(self, reservation):
982 if not reservation:
983 return False
984
985 self._reload_connection()
986 elapsed_time = 0
987 while elapsed_time < 30:
988 if reservation.instances:
989 instance_id = reservation.instances[0].id
990 return instance_id
991 time.sleep(5)
992 elapsed_time += 5
993 else:
994 raise vimconn.VimConnException(
995 "Failed to get instance_id for reservation",
996 reservation,
997 http_code=vimconn.HTTP_Request_Timeout,
998 )
999
1000 def wait_for_vm(self, vm_id, status):
1001 """wait until vm is in the desired status and return True.
1002 If the timeout is reached generate an exception"""
1003
1004 self._reload_connection()
1005
1006 elapsed_time = 0
1007 while elapsed_time < self.server_timeout:
1008 if self.conn.get_all_instances(vm_id):
1009 reservation = self.conn.get_all_instances(vm_id)[0]
1010 if hasattr(reservation, "instances"):
1011 instance = reservation.instances[0]
1012 if instance.state == status:
1013 return True
1014 time.sleep(5)
1015 elapsed_time += 5
1016
1017 # if we exceeded the timeout
1018 else:
1019 raise vimconn.VimConnException(
1020 "Timeout waiting for instance " + vm_id + " to get " + status,
1021 http_code=vimconn.HTTP_Request_Timeout,
1022 )
1023
1024 def refresh_vms_status(self, vm_list):
1025 """Get the status of the virtual machines and their interfaces/ports
1026 Params: the list of VM identifiers
1027 Returns a dictionary with:
1028 vm_id: #VIM id of this Virtual Machine
1029 status: #Mandatory. Text with one of:
1030 # DELETED (not found at vim)
1031 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1032 # OTHER (Vim reported other status not understood)
1033 # ERROR (VIM indicates an ERROR status)
1034 # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
1035 # BUILD (on building process), ERROR
1036 # ACTIVE:NoMgmtIP (Active but any of its interface has an IP address
1037 #
1038 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1039 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1040 interfaces: list with interface info. Each item a dictionary with:
1041 vim_interface_id - The ID of the ENI.
1042 vim_net_id - The ID of the VPC subnet.
1043 mac_address - The MAC address of the interface.
1044 ip_address - The IP address of the interface within the subnet.
1045 """
1046 self.logger.debug("Getting VM instance information from VIM")
1047
1048 try:
1049 self._reload_connection()
1050 elapsed_time = 0
1051 while elapsed_time < self.server_timeout:
1052 reservation = self.conn.get_all_instances(vm_list)[0]
1053 if reservation:
1054 break
1055 time.sleep(5)
1056 elapsed_time += 5
1057
1058 # if we exceeded the timeout
1059 else:
1060 raise vimconn.VimConnException(
1061 vm_list + "could not be gathered, refresh vm status failed",
1062 http_code=vimconn.HTTP_Request_Timeout,
1063 )
1064
1065 instances = {}
1066 instance_dict = {}
1067
1068 for instance in reservation.instances:
1069 if hasattr(instance, "id"):
1070 try:
1071 if instance.state in ("pending"):
1072 instance_dict["status"] = "BUILD"
1073 elif instance.state in ("available", "running", "up"):
1074 instance_dict["status"] = "ACTIVE"
1075 else:
1076 instance_dict["status"] = "ERROR"
1077
1078 instance_dict["error_msg"] = ""
1079 instance_dict["interfaces"] = []
1080
1081 for interface in instance.interfaces:
1082 interface_dict = {
1083 "vim_interface_id": interface.id,
1084 "vim_net_id": interface.subnet_id,
1085 "mac_address": interface.mac_address,
1086 }
1087
1088 if (
1089 hasattr(interface, "publicIp")
1090 and interface.publicIp is not None
1091 ):
1092 interface_dict["ip_address"] = (
1093 interface.publicIp
1094 + ";"
1095 + interface.private_ip_address
1096 )
1097 else:
1098 interface_dict[
1099 "ip_address"
1100 ] = interface.private_ip_address
1101
1102 instance_dict["interfaces"].append(interface_dict)
1103 except Exception as e:
1104 self.logger.error(
1105 "Exception getting vm status: %s", str(e), exc_info=True
1106 )
1107 instance_dict["status"] = "DELETED"
1108 instance_dict["error_msg"] = str(e)
1109 finally:
1110 instance_dictionary = vars(instance)
1111 cleared_instance_dict = {
1112 key: instance_dictionary[key]
1113 for key in instance_dictionary
1114 if not (isinstance(instance_dictionary[key], object))
1115 }
1116 instance_dict["vim_info"] = cleared_instance_dict
1117
1118 instances[instance.id] = instance_dict
1119
1120 return instances
1121 except Exception as e:
1122 self.logger.error("Exception getting vm status: %s", str(e), exc_info=True)
1123 self.format_vimconn_exception(e)
1124
1125 def action_vminstance(self, vm_id, action_dict, created_items={}):
1126 """Send and action over a VM instance from VIM
1127 Returns the vm_id if the action was successfully sent to the VIM"""
1128
1129 self.logger.debug("Action over VM '%s': %s", vm_id, str(action_dict))
1130 try:
1131 self._reload_connection()
1132 if "start" in action_dict:
1133 self.conn.start_instances(vm_id)
1134 elif "stop" in action_dict or "stop" in action_dict:
1135 self.conn.stop_instances(vm_id)
1136 elif "terminate" in action_dict:
1137 self.conn.terminate_instances(vm_id)
1138 elif "reboot" in action_dict:
1139 self.conn.reboot_instances(vm_id)
1140
1141 return None
1142 except Exception as e:
1143 self.format_vimconn_exception(e)
1144
1145 def migrate_instance(self, vm_id, compute_host=None):
1146 """
1147 Migrate a vdu
1148 param:
1149 vm_id: ID of an instance
1150 compute_host: Host to migrate the vdu to
1151 """
1152 # TODO: Add support for migration
1153 raise vimconn.VimConnNotImplemented("Not implemented")
1154
1155 def resize_instance(self, vm_id, flavor_id=None):
1156 """
1157 resize a vdu
1158 param:
1159 vm_id: ID of an instance
1160 flavor_id: flavor to resize the vdu
1161 """
1162 # TODO: Add support for resize
1163 raise vimconn.VimConnNotImplemented("Not implemented")