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