77adecd8f0b3cadaeedde4c120c6364ac9c6c9e2
[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 net_list,
771 cloud_config=None,
772 disk_list=None,
773 availability_zone_index=None,
774 availability_zone_list=None,
775 ):
776 """Create a new VM/instance in AWS
777 Params: name
778 decription
779 start: (boolean) indicates if VM must start or created in pause mode.
780 image_id - image ID in AWS
781 flavor_id - instance type ID in AWS
782 net_list
783 name
784 net_id - subnet_id from AWS
785 vpci - (optional) virtual vPCI address to assign at the VM. Can be ignored depending on VIM
786 capabilities
787 model: (optional and only have sense for type==virtual) interface model: virtio, e1000, ...
788 mac_address: (optional) mac address to assign to this interface
789 type: (mandatory) can be one of:
790 virtual, in this case always connected to a network of type 'net_type=bridge'
791 'PCI-PASSTHROUGH' or 'PF' (passthrough): depending on VIM capabilities it can be connected to a
792 data/ptp network ot it
793 can created unconnected
794 'SR-IOV' or 'VF' (SRIOV with VLAN tag): same as PF for network connectivity.
795 VFnotShared - (SRIOV without VLAN tag) same as PF for network connectivity. VF where no other
796 VFs are allocated on the same physical NIC
797 bw': (optional) only for PF/VF/VFnotShared. Minimal Bandwidth required for the interface in GBPS
798 port_security': (optional) If False it must avoid any traffic filtering at this interface.
799 If missing or True, it must apply the default VIM behaviour
800 vim_id': must be filled/added by this method with the VIM identifier generated by the VIM for this
801 interface. 'net_list' is modified
802 elastic_ip - True/False to define if an elastic_ip is required
803 cloud_config': (optional) dictionary with:
804 key-pairs': (optional) list of strings with the public key to be inserted to the default user
805 users': (optional) list of users to be inserted, each item is a dict with:
806 name': (mandatory) user name,
807 key-pairs': (optional) list of strings with the public key to be inserted to the user
808 user-data': (optional) string is a text script to be passed directly to cloud-init
809 config-files': (optional). List of files to be transferred. Each item is a dict with:
810 dest': (mandatory) string with the destination absolute path
811 encoding': (optional, by default text). Can be one of:
812 b64', 'base64', 'gz', 'gz+b64', 'gz+base64', 'gzip+b64', 'gzip+base64'
813 content' (mandatory): string with the content of the file
814 permissions': (optional) string with file permissions, typically octal notation '0644'
815 owner: (optional) file owner, string with the format 'owner:group'
816 boot-data-drive: boolean to indicate if user-data must be passed using a boot drive (hard disk)
817 security-groups:
818 subnet_id
819 security_group_id
820 disk_list': (optional) list with additional disks to the VM. Each item is a dict with:
821 image_id': (optional). VIM id of an existing image. If not provided an empty disk must be mounted
822 size': (mandatory) string with the size of the disk in GB
823 Returns a tuple with the instance identifier and created_items or raises an exception on error
824 created_items can be None or a dictionary where this method can include key-values that will be passed to
825 the method delete_vminstance and action_vminstance. Can be used to store created ports, volumes, etc.
826 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
827 as not present.
828 """
829 self.logger.debug("Creating a new VM instance")
830
831 try:
832 self._reload_connection()
833 instance = None
834 _, userdata = self._create_user_data(cloud_config)
835
836 if not net_list:
837 reservation = self.conn.run_instances(
838 image_id,
839 key_name=self.key_pair,
840 instance_type=flavor_id,
841 security_groups=self.security_groups,
842 user_data=userdata,
843 )
844 else:
845 for index, subnet in enumerate(net_list):
846 net_intr = boto.ec2.networkinterface.NetworkInterfaceSpecification(
847 subnet_id=subnet.get("net_id"),
848 groups=None,
849 associate_public_ip_address=True,
850 )
851
852 if subnet.get("elastic_ip"):
853 eip = self.conn.allocate_address()
854 self.conn.associate_address(
855 allocation_id=eip.allocation_id,
856 network_interface_id=net_intr.id,
857 )
858
859 if index == 0:
860 reservation = self.conn.run_instances(
861 image_id,
862 key_name=self.key_pair,
863 instance_type=flavor_id,
864 security_groups=self.security_groups,
865 network_interfaces=boto.ec2.networkinterface.NetworkInterfaceCollection(
866 net_intr
867 ),
868 user_data=userdata,
869 )
870 else:
871 while True:
872 try:
873 self.conn.attach_network_interface(
874 network_interface_id=boto.ec2.networkinterface.NetworkInterfaceCollection(
875 net_intr
876 ),
877 instance_id=instance.id,
878 device_index=0,
879 )
880 break
881 except Exception:
882 time.sleep(10)
883
884 net_list[index]["vim_id"] = (
885 reservation.instances[0].interfaces[index].id
886 )
887
888 instance = reservation.instances[0]
889
890 return instance.id, None
891 except Exception as e:
892 self.format_vimconn_exception(e)
893
894 def get_vminstance(self, vm_id):
895 """Returns the VM instance information from VIM"""
896 try:
897 self._reload_connection()
898 reservation = self.conn.get_all_instances(vm_id)
899
900 return reservation[0].instances[0].__dict__
901 except Exception as e:
902 self.format_vimconn_exception(e)
903
904 def delete_vminstance(self, vm_id, created_items=None):
905 """Removes a VM instance from VIM
906 Returns the instance identifier"""
907 try:
908 self._reload_connection()
909 self.logger.debug("DELETING VM_ID: " + str(vm_id))
910 self.conn.terminate_instances(vm_id)
911
912 return vm_id
913 except Exception as e:
914 self.format_vimconn_exception(e)
915
916 def refresh_vms_status(self, vm_list):
917 """Get the status of the virtual machines and their interfaces/ports
918 Params: the list of VM identifiers
919 Returns a dictionary with:
920 vm_id: #VIM id of this Virtual Machine
921 status: #Mandatory. Text with one of:
922 # DELETED (not found at vim)
923 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
924 # OTHER (Vim reported other status not understood)
925 # ERROR (VIM indicates an ERROR status)
926 # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
927 # BUILD (on building process), ERROR
928 # ACTIVE:NoMgmtIP (Active but any of its interface has an IP address
929 #
930 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
931 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
932 interfaces: list with interface info. Each item a dictionary with:
933 vim_interface_id - The ID of the ENI.
934 vim_net_id - The ID of the VPC subnet.
935 mac_address - The MAC address of the interface.
936 ip_address - The IP address of the interface within the subnet.
937 """
938 self.logger.debug("Getting VM instance information from VIM")
939
940 try:
941 self._reload_connection()
942 reservation = self.conn.get_all_instances(vm_list)[0]
943 instances = {}
944 instance_dict = {}
945
946 for instance in reservation.instances:
947 try:
948 if instance.state in ("pending"):
949 instance_dict["status"] = "BUILD"
950 elif instance.state in ("available", "running", "up"):
951 instance_dict["status"] = "ACTIVE"
952 else:
953 instance_dict["status"] = "ERROR"
954
955 instance_dict["error_msg"] = ""
956 instance_dict["interfaces"] = []
957 interface_dict = {}
958
959 for interface in instance.interfaces:
960 interface_dict["vim_interface_id"] = interface.id
961 interface_dict["vim_net_id"] = interface.subnet_id
962 interface_dict["mac_address"] = interface.mac_address
963
964 if (
965 hasattr(interface, "publicIp")
966 and interface.publicIp is not None
967 ):
968 interface_dict["ip_address"] = (
969 interface.publicIp + ";" + interface.private_ip_address
970 )
971 else:
972 interface_dict["ip_address"] = interface.private_ip_address
973
974 instance_dict["interfaces"].append(interface_dict)
975 except Exception as e:
976 self.logger.error(
977 "Exception getting vm status: %s", str(e), exc_info=True
978 )
979 instance_dict["status"] = "DELETED"
980 instance_dict["error_msg"] = str(e)
981 finally:
982 try:
983 instance_dict["vim_info"] = yaml.safe_dump(
984 instance, default_flow_style=True, width=256
985 )
986 except yaml.YAMLError:
987 # self.logger.error("Exception getting vm status: %s", str(e), exc_info=True)
988 instance_dict["vim_info"] = str(instance)
989
990 instances[instance.id] = instance_dict
991
992 return instances
993 except Exception as e:
994 self.logger.error("Exception getting vm status: %s", str(e), exc_info=True)
995 self.format_vimconn_exception(e)
996
997 def action_vminstance(self, vm_id, action_dict, created_items={}):
998 """Send and action over a VM instance from VIM
999 Returns the vm_id if the action was successfully sent to the VIM"""
1000
1001 self.logger.debug("Action over VM '%s': %s", vm_id, str(action_dict))
1002 try:
1003 self._reload_connection()
1004 if "start" in action_dict:
1005 self.conn.start_instances(vm_id)
1006 elif "stop" in action_dict or "stop" in action_dict:
1007 self.conn.stop_instances(vm_id)
1008 elif "terminate" in action_dict:
1009 self.conn.terminate_instances(vm_id)
1010 elif "reboot" in action_dict:
1011 self.conn.reboot_instances(vm_id)
1012
1013 return None
1014 except Exception as e:
1015 self.format_vimconn_exception(e)