Reformat files according to new black validation
[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 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 net_intr = self.conn_vpc.create_network_interface(
869 subnet_id=subnet.get("net_id"),
870 groups=None,
871 )
872
873 interface = boto.ec2.networkinterface.NetworkInterfaceSpecification(
874 network_interface_id=net_intr.id,
875 device_index=index,
876 )
877
878 interfaces = boto.ec2.networkinterface.NetworkInterfaceCollection(
879 interface
880 )
881
882 if subnet.get("elastic_ip"):
883 eip = self.conn.allocate_address()
884 self.conn.associate_address(
885 allocation_id=eip.allocation_id,
886 network_interface_id=net_intr.id,
887 )
888
889 if index == 0:
890 try:
891 reservation = self.conn.run_instances(
892 image_id,
893 key_name=self.key_pair,
894 instance_type=flavor_id,
895 security_groups=self.security_groups,
896 network_interfaces=interfaces,
897 user_data=userdata,
898 )
899 except Exception as instance_create_error:
900 self.logger.debug(traceback.format_exc())
901 self.format_vimconn_exception(instance_create_error)
902
903 if index > 0:
904 try:
905 if reservation:
906 instance_id = self.wait_for_instance_id(reservation)
907 if instance_id and self.wait_for_vm(
908 instance_id, "running"
909 ):
910 self.conn.attach_network_interface(
911 network_interface_id=net_intr.id,
912 instance_id=instance_id,
913 device_index=index,
914 )
915 except Exception as attach_network_error:
916 self.logger.debug(traceback.format_exc())
917 self.format_vimconn_exception(attach_network_error)
918
919 if instance_id := self.wait_for_instance_id(reservation):
920 time.sleep(30)
921 instance_status = self.refresh_vms_status(instance_id)
922 refreshed_instance_status = instance_status.get(instance_id)
923 instance_interfaces = refreshed_instance_status.get(
924 "interfaces"
925 )
926 for idx, interface in enumerate(instance_interfaces):
927 if idx == index:
928 net_list[index]["vim_id"] = instance_interfaces[
929 idx
930 ].get("vim_interface_id")
931
932 instance_id = self.wait_for_instance_id(reservation)
933 created_items["vm_id:" + str(instance_id)] = True
934
935 return instance_id, created_items
936 except Exception as e:
937 self.logger.debug(traceback.format_exc())
938 self.format_vimconn_exception(e)
939
940 def get_vminstance(self, vm_id):
941 """Returns the VM instance information from VIM"""
942 try:
943 self._reload_connection()
944 reservation = self.conn.get_all_instances(vm_id)
945
946 return reservation[0].instances[0].__dict__
947 except Exception as e:
948 self.format_vimconn_exception(e)
949
950 def delete_vminstance(self, vm_id, created_items=None, volumes_to_hold=None):
951 """Removes a VM instance from VIM
952 Returns the instance identifier"""
953 try:
954 self._reload_connection()
955 self.logger.debug("DELETING VM_ID: " + str(vm_id))
956 reservation = self.conn.get_all_instances(vm_id)[0]
957 if hasattr(reservation, "instances"):
958 instance = reservation.instances[0]
959
960 self.conn.terminate_instances(vm_id)
961 if self.wait_for_vm(vm_id, "terminated"):
962 for interface in instance.interfaces:
963 self.conn_vpc.delete_network_interface(
964 network_interface_id=interface.id,
965 )
966 if self.network_delete_on_termination:
967 for net in self.network_delete_on_termination:
968 try:
969 self.conn_vpc.delete_subnet(net)
970 except Exception as net_delete_error:
971 if isinstance(net_delete_error, EC2ResponseError):
972 self.logger.warning(f"Deleting network {net}: failed")
973 else:
974 self.format_vimconn_exception(net_delete_error)
975
976 return vm_id
977 except Exception as e:
978 self.format_vimconn_exception(e)
979
980 def wait_for_instance_id(self, reservation):
981 if not reservation:
982 return False
983
984 self._reload_connection()
985 elapsed_time = 0
986 while elapsed_time < 30:
987 if reservation.instances:
988 instance_id = reservation.instances[0].id
989 return instance_id
990 time.sleep(5)
991 elapsed_time += 5
992 else:
993 raise vimconn.VimConnException(
994 "Failed to get instance_id for reservation",
995 reservation,
996 http_code=vimconn.HTTP_Request_Timeout,
997 )
998
999 def wait_for_vm(self, vm_id, status):
1000 """wait until vm is in the desired status and return True.
1001 If the timeout is reached generate an exception"""
1002
1003 self._reload_connection()
1004
1005 elapsed_time = 0
1006 while elapsed_time < self.server_timeout:
1007 if self.conn.get_all_instances(vm_id):
1008 reservation = self.conn.get_all_instances(vm_id)[0]
1009 if hasattr(reservation, "instances"):
1010 instance = reservation.instances[0]
1011 if instance.state == status:
1012 return True
1013 time.sleep(5)
1014 elapsed_time += 5
1015
1016 # if we exceeded the timeout
1017 else:
1018 raise vimconn.VimConnException(
1019 "Timeout waiting for instance " + vm_id + " to get " + status,
1020 http_code=vimconn.HTTP_Request_Timeout,
1021 )
1022
1023 def refresh_vms_status(self, vm_list):
1024 """Get the status of the virtual machines and their interfaces/ports
1025 Params: the list of VM identifiers
1026 Returns a dictionary with:
1027 vm_id: #VIM id of this Virtual Machine
1028 status: #Mandatory. Text with one of:
1029 # DELETED (not found at vim)
1030 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1031 # OTHER (Vim reported other status not understood)
1032 # ERROR (VIM indicates an ERROR status)
1033 # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
1034 # BUILD (on building process), ERROR
1035 # ACTIVE:NoMgmtIP (Active but any of its interface has an IP address
1036 #
1037 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1038 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1039 interfaces: list with interface info. Each item a dictionary with:
1040 vim_interface_id - The ID of the ENI.
1041 vim_net_id - The ID of the VPC subnet.
1042 mac_address - The MAC address of the interface.
1043 ip_address - The IP address of the interface within the subnet.
1044 """
1045 self.logger.debug("Getting VM instance information from VIM")
1046
1047 try:
1048 self._reload_connection()
1049 elapsed_time = 0
1050 while elapsed_time < self.server_timeout:
1051 reservation = self.conn.get_all_instances(vm_list)[0]
1052 if reservation:
1053 break
1054 time.sleep(5)
1055 elapsed_time += 5
1056
1057 # if we exceeded the timeout
1058 else:
1059 raise vimconn.VimConnException(
1060 vm_list + "could not be gathered, refresh vm status failed",
1061 http_code=vimconn.HTTP_Request_Timeout,
1062 )
1063
1064 instances = {}
1065 instance_dict = {}
1066
1067 for instance in reservation.instances:
1068 if hasattr(instance, "id"):
1069 try:
1070 if instance.state in ("pending"):
1071 instance_dict["status"] = "BUILD"
1072 elif instance.state in ("available", "running", "up"):
1073 instance_dict["status"] = "ACTIVE"
1074 else:
1075 instance_dict["status"] = "ERROR"
1076
1077 instance_dict["error_msg"] = ""
1078 instance_dict["interfaces"] = []
1079
1080 for interface in instance.interfaces:
1081 interface_dict = {
1082 "vim_interface_id": interface.id,
1083 "vim_net_id": interface.subnet_id,
1084 "mac_address": interface.mac_address,
1085 }
1086
1087 if (
1088 hasattr(interface, "publicIp")
1089 and interface.publicIp is not None
1090 ):
1091 interface_dict["ip_address"] = (
1092 interface.publicIp
1093 + ";"
1094 + interface.private_ip_address
1095 )
1096 else:
1097 interface_dict[
1098 "ip_address"
1099 ] = interface.private_ip_address
1100
1101 instance_dict["interfaces"].append(interface_dict)
1102 except Exception as e:
1103 self.logger.error(
1104 "Exception getting vm status: %s", str(e), exc_info=True
1105 )
1106 instance_dict["status"] = "DELETED"
1107 instance_dict["error_msg"] = str(e)
1108 finally:
1109 instance_dictionary = vars(instance)
1110 cleared_instance_dict = {
1111 key: instance_dictionary[key]
1112 for key in instance_dictionary
1113 if not (isinstance(instance_dictionary[key], object))
1114 }
1115 instance_dict["vim_info"] = cleared_instance_dict
1116
1117 instances[instance.id] = instance_dict
1118
1119 return instances
1120 except Exception as e:
1121 self.logger.error("Exception getting vm status: %s", str(e), exc_info=True)
1122 self.format_vimconn_exception(e)
1123
1124 def action_vminstance(self, vm_id, action_dict, created_items={}):
1125 """Send and action over a VM instance from VIM
1126 Returns the vm_id if the action was successfully sent to the VIM"""
1127
1128 self.logger.debug("Action over VM '%s': %s", vm_id, str(action_dict))
1129 try:
1130 self._reload_connection()
1131 if "start" in action_dict:
1132 self.conn.start_instances(vm_id)
1133 elif "stop" in action_dict or "stop" in action_dict:
1134 self.conn.stop_instances(vm_id)
1135 elif "terminate" in action_dict:
1136 self.conn.terminate_instances(vm_id)
1137 elif "reboot" in action_dict:
1138 self.conn.reboot_instances(vm_id)
1139
1140 return None
1141 except Exception as e:
1142 self.format_vimconn_exception(e)
1143
1144 def migrate_instance(self, vm_id, compute_host=None):
1145 """
1146 Migrate a vdu
1147 param:
1148 vm_id: ID of an instance
1149 compute_host: Host to migrate the vdu to
1150 """
1151 # TODO: Add support for migration
1152 raise vimconn.VimConnNotImplemented("Not implemented")
1153
1154 def resize_instance(self, vm_id, flavor_id=None):
1155 """
1156 resize a vdu
1157 param:
1158 vm_id: ID of an instance
1159 flavor_id: flavor to resize the vdu
1160 """
1161 # TODO: Add support for resize
1162 raise vimconn.VimConnNotImplemented("Not implemented")