blob: 393acba0704c431e40944bd8eaacd3adc8941b7f [file] [log] [blame]
# -*- coding: utf-8 -*-
##
# Copyright 2017 xFlow Research Pvt. Ltd
# This file is part of openmano
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact with: saboor.ahmad@xflowresearch.com
##
"""
AWS-connector implements all the methods to interact with AWS using the BOTO client
"""
import logging
import random
import time
import traceback
import boto
import boto.ec2
from boto.exception import BotoServerError, EC2ResponseError
import boto.vpc
from ipconflict import check_conflicts
import netaddr
from osm_ro_plugin import vimconn
import yaml
__author__ = "Saboor Ahmad"
__date__ = "10-Apr-2017"
class vimconnector(vimconn.VimConnector):
def __init__(
self,
uuid,
name,
tenant_id,
tenant_name,
url,
url_admin=None,
user=None,
passwd=None,
log_level=None,
config={},
persistent_info={},
):
"""Params:
uuid - id asigned to this VIM
name - name assigned to this VIM, can be used for logging
tenant_id - ID to be used for tenant
tenant_name - name of tenant to be used VIM tenant to be used
url_admin - optional, url used for administrative tasks
user - credentials of the VIM user
passwd - credentials of the VIM user
log_level - if must use a different log_level than the general one
config - dictionary with misc VIM information
region_name - name of region to deploy the instances
vpc_cidr_block - default CIDR block for VPC
security_groups - default security group to specify this instance
persistent_info - dict where the class can store information that will be available among class
destroy/creation cycles. This info is unique per VIM/credential. At first call it will contain an
empty dict. Useful to store login/tokens information for speed up communication
"""
vimconn.VimConnector.__init__(
self,
uuid,
name,
tenant_id,
tenant_name,
url,
url_admin,
user,
passwd,
log_level,
config,
persistent_info,
)
self.persistent_info = persistent_info
self.a_creds = {}
if user:
self.a_creds["aws_access_key_id"] = user
else:
raise vimconn.VimConnAuthException("Username is not specified")
if passwd:
self.a_creds["aws_secret_access_key"] = passwd
else:
raise vimconn.VimConnAuthException("Password is not specified")
if "region_name" in config:
self.region = config.get("region_name")
else:
raise vimconn.VimConnException("AWS region_name is not specified at config")
self.vpc_data = {}
self.subnet_data = {}
self.conn = None
self.conn_vpc = None
self.account_id = None
self.network_delete_on_termination = []
self.server_timeout = 180
self.vpc_id = self.get_tenant_list()[0]["id"]
# we take VPC CIDR block if specified, otherwise we use the default CIDR
# block suggested by AWS while creating instance
self.vpc_cidr_block = "10.0.0.0/24"
if tenant_name:
self.vpc_id = tenant_name
if "vpc_cidr_block" in config:
self.vpc_cidr_block = config["vpc_cidr_block"]
self.security_groups = None
if "security_groups" in config:
self.security_groups = config["security_groups"]
self.key_pair = None
if "key_pair" in config:
self.key_pair = config["key_pair"]
self.flavor_info = None
if "flavor_info" in config:
flavor_data = config.get("flavor_info")
if isinstance(flavor_data, str):
try:
if flavor_data[0] == "@": # read from a file
with open(flavor_data[1:], "r") as stream:
self.flavor_info = yaml.safe_load(stream)
else:
self.flavor_info = yaml.safe_load(flavor_data)
except yaml.YAMLError as e:
self.flavor_info = None
raise vimconn.VimConnException(
"Bad format at file '{}': {}".format(flavor_data[1:], e)
)
except IOError as e:
raise vimconn.VimConnException(
"Error reading file '{}': {}".format(flavor_data[1:], e)
)
elif isinstance(flavor_data, dict):
self.flavor_info = flavor_data
self.logger = logging.getLogger("ro.vim.aws")
if log_level:
self.logger.setLevel(getattr(logging, log_level))
def __setitem__(self, index, value):
"""Params:
index - name of value of set
value - value to set
"""
if index == "user":
self.a_creds["aws_access_key_id"] = value
elif index == "passwd":
self.a_creds["aws_secret_access_key"] = value
elif index == "region":
self.region = value
else:
vimconn.VimConnector.__setitem__(self, index, value)
def _reload_connection(self):
"""Returns: sets boto.EC2 and boto.VPC connection to work with AWS services"""
try:
self.conn = boto.ec2.connect_to_region(
self.region,
aws_access_key_id=self.a_creds["aws_access_key_id"],
aws_secret_access_key=self.a_creds["aws_secret_access_key"],
)
self.conn_vpc = boto.vpc.connect_to_region(
self.region,
aws_access_key_id=self.a_creds["aws_access_key_id"],
aws_secret_access_key=self.a_creds["aws_secret_access_key"],
)
# client = boto3.client("sts", aws_access_key_id=self.a_creds['aws_access_key_id'],
# aws_secret_access_key=self.a_creds['aws_secret_access_key'])
# self.account_id = client.get_caller_identity()["Account"]
except Exception as e:
self.format_vimconn_exception(e)
def format_vimconn_exception(self, e):
"""Params: an Exception object
Returns: Raises the exception 'e' passed in mehtod parameters
"""
self.conn = None
self.conn_vpc = None
raise vimconn.VimConnConnectionException(type(e).__name__ + ": " + str(e))
def get_tenant_list(self, filter_dict={}):
"""Obtain tenants of VIM
filter_dict dictionary that can contain the following keys:
name: filter by tenant name
id: filter by tenant uuid/id
<other VIM specific>
Returns the tenant list of dictionaries, and empty list if no tenant match all the filers:
[{'name':'<name>, 'id':'<id>, ...}, ...]
"""
try:
self._reload_connection()
vpc_ids = []
if filter_dict != {}:
if "id" in filter_dict:
vpc_ids.append(filter_dict["id"])
tenants = self.conn_vpc.get_all_vpcs(vpc_ids, None)
tenant_list = []
for tenant in tenants:
tenant_list.append(
{
"id": str(tenant.id),
"name": str(tenant.id),
"status": str(tenant.state),
"cidr_block": str(tenant.cidr_block),
}
)
return tenant_list
except Exception as e:
self.format_vimconn_exception(e)
def new_tenant(self, tenant_name, tenant_description):
"""Adds a new tenant to VIM with this name and description, this is done using admin_url if provided
"tenant_name": string max lenght 64
"tenant_description": string max length 256
returns the tenant identifier or raise exception
"""
self.logger.debug("Adding a new VPC")
try:
self._reload_connection()
vpc = self.conn_vpc.create_vpc(self.vpc_cidr_block)
self.conn_vpc.modify_vpc_attribute(vpc.id, enable_dns_support=True)
self.conn_vpc.modify_vpc_attribute(vpc.id, enable_dns_hostnames=True)
gateway = self.conn_vpc.create_internet_gateway()
self.conn_vpc.attach_internet_gateway(gateway.id, vpc.id)
route_table = self.conn_vpc.create_route_table(vpc.id)
self.conn_vpc.create_route(route_table.id, "0.0.0.0/0", gateway.id)
self.vpc_data[vpc.id] = {
"gateway": gateway.id,
"route_table": route_table.id,
"subnets": self.subnet_sizes(self.vpc_cidr_block),
}
return vpc.id
except Exception as e:
self.format_vimconn_exception(e)
def delete_tenant(self, tenant_id):
"""Delete a tenant from VIM
tenant_id: returned VIM tenant_id on "new_tenant"
Returns None on success. Raises and exception of failure. If tenant is not found raises vimconnNotFoundException
"""
self.logger.debug("Deleting specified VPC")
try:
self._reload_connection()
vpc = self.vpc_data.get(tenant_id)
if "gateway" in vpc and "route_table" in vpc:
gateway_id, route_table_id = vpc["gateway"], vpc["route_table"]
self.conn_vpc.detach_internet_gateway(gateway_id, tenant_id)
self.conn_vpc.delete_vpc(tenant_id)
self.conn_vpc.delete_route(route_table_id, "0.0.0.0/0")
else:
self.conn_vpc.delete_vpc(tenant_id)
except Exception as e:
self.format_vimconn_exception(e)
def subnet_sizes(self, cidr):
"""Calculates possible subnets given CIDR value of VPC"""
netmasks = (
"255.255.0.0",
"255.255.128.0",
"255.255.192.0",
"255.255.224.0",
"255.255.240.0",
"255.255.248.0",
)
ip = netaddr.IPNetwork(cidr)
mask = ip.netmask
pub_split = ()
for netmask in netmasks:
if str(mask) == netmask:
pub_split = list(ip.subnet(24))
break
subnets = pub_split if pub_split else (list(ip.subnet(28)))
return map(str, subnets)
def new_network(
self,
net_name,
net_type,
ip_profile=None,
shared=False,
provider_network_profile=None,
):
"""Adds a tenant network to VIM
Params:
'net_name': name of the network
'net_type': one of:
'bridge': overlay isolated network
'data': underlay E-LAN network for Passthrough and SRIOV interfaces
'ptp': underlay E-LINE network for Passthrough and SRIOV interfaces.
'ip_profile': is a dict containing the IP parameters of the network (Currently only IPv4 is implemented)
'ip-version': can be one of ["IPv4","IPv6"]
'subnet-address': ip_prefix_schema, that is X.X.X.X/Y
'gateway-address': (Optional) ip_schema, that is X.X.X.X
'dns-address': (Optional) ip_schema,
'dhcp': (Optional) dict containing
'enabled': {"type": "boolean"},
'start-address': ip_schema, first IP to grant
'count': number of IPs to grant.
'shared': if this network can be seen/use by other tenants/organization
Returns a tuple with the network identifier and created_items, or raises an exception on error
created_items can be None or a dictionary where this method can include key-values that will be passed to
the method delete_network. Can be used to store created segments, created l2gw connections, etc.
Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
as not present.
"""
self.logger.debug("Adding a subnet to VPC")
try:
created_items = {}
self._reload_connection()
subnet = None
vpc_id = self.vpc_id
if self.conn_vpc.get_all_subnets():
existing_subnet = self.conn_vpc.get_all_subnets()[0]
if not self.availability_zone:
self.availability_zone = str(existing_subnet.availability_zone)
if self.vpc_data.get(vpc_id, None):
cidr_block = list(
set(self.vpc_data[vpc_id]["subnets"])
- set(
self.get_network_details(
{"tenant_id": vpc_id}, detail="cidr_block"
)
)
)
else:
vpc = self.get_tenant_list({"id": vpc_id})[0]
subnet_list = self.subnet_sizes(vpc["cidr_block"])
cidr_block = list(
set(subnet_list)
- set(
self.get_network_details(
{"tenant_id": vpc["id"]}, detail="cidr_block"
)
)
)
try:
selected_cidr_block = random.choice(cidr_block)
retry = 15
while retry > 0:
all_subnets = [
subnet.cidr_block for subnet in self.conn_vpc.get_all_subnets()
]
all_subnets.append(selected_cidr_block)
conflict = check_conflicts(all_subnets)
if not conflict:
subnet = self.conn_vpc.create_subnet(
vpc_id, selected_cidr_block, self.availability_zone
)
break
retry -= 1
selected_cidr_block = random.choice(cidr_block)
else:
raise vimconn.VimConnException(
"Failed to find a proper CIDR which does not overlap"
"with existing subnets",
http_code=vimconn.HTTP_Request_Timeout,
)
except (EC2ResponseError, BotoServerError) as error:
self.format_vimconn_exception(error)
created_items["net:" + str(subnet.id)] = True
return subnet.id, created_items
except Exception as e:
self.format_vimconn_exception(e)
def get_network_details(self, filters, detail):
"""Get specified details related to a subnet"""
detail_list = []
subnet_list = self.get_network_list(filters)
for net in subnet_list:
detail_list.append(net[detail])
return detail_list
def get_network_list(self, filter_dict={}):
"""Obtain tenant networks of VIM
Params:
'filter_dict' (optional) contains entries to return only networks that matches ALL entries:
name: string => returns only networks with this name
id: string => returns networks with this VIM id, this imply returns one network at most
shared: boolean >= returns only networks that are (or are not) shared
tenant_id: sting => returns only networks that belong to this tenant/project
,#(not used yet) admin_state_up: boolean => returns only networks that are (or are not) in admin
state active
#(not used yet) status: 'ACTIVE','ERROR',... => filter networks that are on this status
Returns the network list of dictionaries. each dictionary contains:
'id': (mandatory) VIM network id
'name': (mandatory) VIM network name
'status': (mandatory) can be 'ACTIVE', 'INACTIVE', 'DOWN', 'BUILD', 'ERROR', 'VIM_ERROR', 'OTHER'
'error_msg': (optional) text that explains the ERROR status
other VIM specific fields: (optional) whenever possible using the same naming of filter_dict param
List can be empty if no network map the filter_dict. Raise an exception only upon VIM connectivity,
authorization, or some other unspecific error
"""
self.logger.debug("Getting all subnets from VIM")
try:
self._reload_connection()
tfilters = {}
if filter_dict != {}:
if "tenant_id" in filter_dict:
tfilters["vpcId"] = filter_dict.get("tenant_id")
subnets = self.conn_vpc.get_all_subnets(
subnet_ids=filter_dict.get("SubnetId", None), filters=tfilters
)
net_list = []
for net in subnets:
if net.id == filter_dict.get("name"):
self.availability_zone = str(net.availability_zone)
net_list.append(
{
"id": str(net.id),
"name": str(net.id),
"status": str(net.state),
"vpc_id": str(net.vpc_id),
"cidr_block": str(net.cidr_block),
"type": "bridge",
}
)
return net_list
except Exception as e:
self.format_vimconn_exception(e)
def get_network(self, net_id):
"""Obtain network details from the 'net_id' VIM network
Return a dict that contains:
'id': (mandatory) VIM network id, that is, net_id
'name': (mandatory) VIM network name
'status': (mandatory) can be 'ACTIVE', 'INACTIVE', 'DOWN', 'BUILD', 'ERROR', 'VIM_ERROR', 'OTHER'
'error_msg': (optional) text that explains the ERROR status
other VIM specific fields: (optional) whenever possible using the same naming of filter_dict param
Raises an exception upon error or when network is not found
"""
self.logger.debug("Getting Subnet from VIM")
try:
self._reload_connection()
subnet = self.conn_vpc.get_all_subnets(net_id)[0]
return {
"id": str(subnet.id),
"name": str(subnet.id),
"status": str(subnet.state),
"vpc_id": str(subnet.vpc_id),
"cidr_block": str(subnet.cidr_block),
"availability_zone": str(subnet.availability_zone),
}
except Exception as e:
self.format_vimconn_exception(e)
def delete_network(self, net_id, created_items=None):
"""
Removes a tenant network from VIM and its associated elements
:param net_id: VIM identifier of the network, provided by method new_network
:param created_items: dictionary with extra items to be deleted. provided by method new_network
Returns the network identifier or raises an exception upon error or when network is not found
"""
self.logger.debug("Deleting subnet from VIM")
try:
self._reload_connection()
self.logger.debug("DELETING NET_ID: " + str(net_id))
self.conn_vpc.delete_subnet(net_id)
return net_id
except Exception as e:
if isinstance(e, EC2ResponseError):
self.network_delete_on_termination.append(net_id)
self.logger.warning(
f"{net_id} could not be deleted, deletion will retry after dependencies resolved"
)
else:
self.format_vimconn_exception(e)
def refresh_nets_status(self, net_list):
"""Get the status of the networks
Params:
'net_list': a list with the VIM network id to be get the status
Returns a dictionary with:
'net_id': #VIM id of this network
status: #Mandatory. Text with one of:
# DELETED (not found at vim)
# VIM_ERROR (Cannot connect to VIM, authentication problems, VIM response error, ...)
# OTHER (Vim reported other status not understood)
# ERROR (VIM indicates an ERROR status)
# ACTIVE, INACTIVE, DOWN (admin down),
# BUILD (on building process)
error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
'net_id2': ...
"""
self._reload_connection()
try:
dict_entry = {}
for net_id in net_list:
subnet_dict = {}
subnet = None
try:
subnet = self.conn_vpc.get_all_subnets(net_id)[0]
if subnet.state == "pending":
subnet_dict["status"] = "BUILD"
elif subnet.state == "available":
subnet_dict["status"] = "ACTIVE"
else:
subnet_dict["status"] = "ERROR"
subnet_dict["error_msg"] = ""
except Exception:
subnet_dict["status"] = "DELETED"
subnet_dict["error_msg"] = "Network not found"
finally:
subnet_dictionary = vars(subnet)
cleared_subnet_dict = {
key: subnet_dictionary[key]
for key in subnet_dictionary
if not isinstance(subnet_dictionary[key], object)
}
subnet_dict["vim_info"] = cleared_subnet_dict
dict_entry[net_id] = subnet_dict
return dict_entry
except Exception as e:
self.format_vimconn_exception(e)
def get_flavor(self, flavor_id):
"""Obtain flavor details from the VIM
Returns the flavor dict details {'id':<>, 'name':<>, other vim specific }
Raises an exception upon error or if not found
"""
self.logger.debug("Getting instance type")
try:
if flavor_id in self.flavor_info:
return self.flavor_info[flavor_id]
else:
raise vimconn.VimConnNotFoundException(
"Cannot find flavor with this flavor ID/Name"
)
except Exception as e:
self.format_vimconn_exception(e)
def new_image(self, image_dict):
"""Adds a tenant image to VIM
Params: image_dict
name (string) - The name of the AMI. Valid only for EBS-based images.
description (string) - The description of the AMI.
image_location (string) - Full path to your AMI manifest in Amazon S3 storage. Only used for S3-based AMI’s.
architecture (string) - The architecture of the AMI. Valid choices are: * i386 * x86_64
kernel_id (string) - The ID of the kernel with which to launch the instances
root_device_name (string) - The root device name (e.g. /dev/sdh)
block_device_map (boto.ec2.blockdevicemapping.BlockDeviceMapping) - A BlockDeviceMapping data structure
describing the EBS volumes associated with the Image.
virtualization_type (string) - The virutalization_type of the image. Valid choices are: * paravirtual * hvm
sriov_net_support (string) - Advanced networking support. Valid choices are: * simple
snapshot_id (string) - A snapshot ID for the snapshot to be used as root device for the image. Mutually
exclusive with block_device_map, requires root_device_name
delete_root_volume_on_termination (bool) - Whether to delete the root volume of the image after instance
termination. Only applies when creating image from snapshot_id. Defaults to False. Note that leaving
volumes behind after instance termination is not free
Returns: image_id - image ID of the newly created image
"""
try:
self._reload_connection()
image_location = image_dict.get("image_location", None)
if image_location:
image_location = str(self.account_id) + str(image_location)
image_id = self.conn.register_image(
image_dict.get("name", None),
image_dict.get("description", None),
image_location,
image_dict.get("architecture", None),
image_dict.get("kernel_id", None),
image_dict.get("root_device_name", None),
image_dict.get("block_device_map", None),
image_dict.get("virtualization_type", None),
image_dict.get("sriov_net_support", None),
image_dict.get("snapshot_id", None),
image_dict.get("delete_root_volume_on_termination", None),
)
return image_id
except Exception as e:
self.format_vimconn_exception(e)
def delete_image(self, image_id):
"""Deletes a tenant image from VIM
Returns the image_id if image is deleted or raises an exception on error"""
try:
self._reload_connection()
self.conn.deregister_image(image_id)
return image_id
except Exception as e:
self.format_vimconn_exception(e)
def get_image_id_from_path(self, path):
"""
Params: path - location of the image
Returns: image_id - ID of the matching image
"""
self._reload_connection()
try:
filters = {}
if path:
tokens = path.split("/")
filters["owner_id"] = tokens[0]
filters["name"] = "/".join(tokens[1:])
image = self.conn.get_all_images(filters=filters)[0]
return image.id
except Exception as e:
self.format_vimconn_exception(e)
def get_image_list(self, filter_dict={}):
"""Obtain tenant images from VIM
Filter_dict can be:
name: image name
id: image uuid
checksum: image checksum
location: image path
Returns the image list of dictionaries:
[{<the fields at Filter_dict plus some VIM specific>}, ...]
List can be empty
"""
self.logger.debug("Getting image list from VIM")
try:
self._reload_connection()
image_id = None
filters = {}
if "id" in filter_dict:
image_id = filter_dict["id"]
if "name" in filter_dict:
filters["name"] = filter_dict["name"]
if "location" in filter_dict:
filters["location"] = filter_dict["location"]
# filters['image_type'] = 'machine'
# filter_dict['owner_id'] = self.account_id
images = self.conn.get_all_images(image_id, filters=filters)
image_list = []
for image in images:
image_list.append(
{
"id": str(image.id),
"name": str(image.name),
"status": str(image.state),
"owner": str(image.owner_id),
"location": str(image.location),
"is_public": str(image.is_public),
"architecture": str(image.architecture),
"platform": str(image.platform),
}
)
return image_list
except Exception as e:
self.format_vimconn_exception(e)
def new_vminstance(
self,
name,
description,
start,
image_id,
flavor_id,
affinity_group_list,
net_list,
cloud_config=None,
disk_list=None,
availability_zone_index=None,
availability_zone_list=None,
):
"""Create a new VM/instance in AWS
Params: name
decription
start: (boolean) indicates if VM must start or created in pause mode.
image_id - image ID in AWS
flavor_id - instance type ID in AWS
net_list
name
net_id - subnet_id from AWS
vpci - (optional) virtual vPCI address to assign at the VM. Can be ignored depending on VIM
capabilities
model: (optional and only have sense for type==virtual) interface model: virtio, e1000, ...
mac_address: (optional) mac address to assign to this interface
type: (mandatory) can be one of:
virtual, in this case always connected to a network of type 'net_type=bridge'
'PCI-PASSTHROUGH' or 'PF' (passthrough): depending on VIM capabilities it can be connected to a
data/ptp network ot it
can created unconnected
'SR-IOV' or 'VF' (SRIOV with VLAN tag): same as PF for network connectivity.
VFnotShared - (SRIOV without VLAN tag) same as PF for network connectivity. VF where no other
VFs are allocated on the same physical NIC
bw': (optional) only for PF/VF/VFnotShared. Minimal Bandwidth required for the interface in GBPS
port_security': (optional) If False it must avoid any traffic filtering at this interface.
If missing or True, it must apply the default VIM behaviour
vim_id': must be filled/added by this method with the VIM identifier generated by the VIM for this
interface. 'net_list' is modified
elastic_ip - True/False to define if an elastic_ip is required
cloud_config': (optional) dictionary with:
key-pairs': (optional) list of strings with the public key to be inserted to the default user
users': (optional) list of users to be inserted, each item is a dict with:
name': (mandatory) user name,
key-pairs': (optional) list of strings with the public key to be inserted to the user
user-data': (optional) string is a text script to be passed directly to cloud-init
config-files': (optional). List of files to be transferred. Each item is a dict with:
dest': (mandatory) string with the destination absolute path
encoding': (optional, by default text). Can be one of:
b64', 'base64', 'gz', 'gz+b64', 'gz+base64', 'gzip+b64', 'gzip+base64'
content' (mandatory): string with the content of the file
permissions': (optional) string with file permissions, typically octal notation '0644'
owner: (optional) file owner, string with the format 'owner:group'
boot-data-drive: boolean to indicate if user-data must be passed using a boot drive (hard disk)
security-groups:
subnet_id
security_group_id
disk_list': (optional) list with additional disks to the VM. Each item is a dict with:
image_id': (optional). VIM id of an existing image. If not provided an empty disk must be mounted
size': (mandatory) string with the size of the disk in GB
Returns a tuple with the instance identifier and created_items or raises an exception on error
created_items can be None or a dictionary where this method can include key-values that will be passed to
the method delete_vminstance and action_vminstance. Can be used to store created ports, volumes, etc.
Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
as not present.
"""
self.logger.debug("Creating a new VM instance")
try:
created_items = {}
self._reload_connection()
reservation = None
_, userdata = self._create_user_data(cloud_config)
if not net_list:
reservation = self.conn.run_instances(
image_id,
key_name=self.key_pair,
instance_type=flavor_id,
security_groups=self.security_groups,
user_data=userdata,
)
else:
for index, subnet in enumerate(net_list):
net_intr = self.conn_vpc.create_network_interface(
subnet_id=subnet.get("net_id"),
groups=None,
)
interface = boto.ec2.networkinterface.NetworkInterfaceSpecification(
network_interface_id=net_intr.id,
device_index=index,
)
interfaces = boto.ec2.networkinterface.NetworkInterfaceCollection(
interface
)
if subnet.get("elastic_ip"):
eip = self.conn.allocate_address()
self.conn.associate_address(
allocation_id=eip.allocation_id,
network_interface_id=net_intr.id,
)
if index == 0:
try:
reservation = self.conn.run_instances(
image_id,
key_name=self.key_pair,
instance_type=flavor_id,
security_groups=self.security_groups,
network_interfaces=interfaces,
user_data=userdata,
)
except Exception as instance_create_error:
self.logger.debug(traceback.format_exc())
self.format_vimconn_exception(instance_create_error)
if index > 0:
try:
if reservation:
instance_id = self.wait_for_instance_id(reservation)
if instance_id and self.wait_for_vm(
instance_id, "running"
):
self.conn.attach_network_interface(
network_interface_id=net_intr.id,
instance_id=instance_id,
device_index=index,
)
except Exception as attach_network_error:
self.logger.debug(traceback.format_exc())
self.format_vimconn_exception(attach_network_error)
if instance_id := self.wait_for_instance_id(reservation):
time.sleep(30)
instance_status = self.refresh_vms_status(instance_id)
refreshed_instance_status = instance_status.get(instance_id)
instance_interfaces = refreshed_instance_status.get(
"interfaces"
)
for idx, interface in enumerate(instance_interfaces):
if idx == index:
net_list[index]["vim_id"] = instance_interfaces[
idx
].get("vim_interface_id")
instance_id = self.wait_for_instance_id(reservation)
created_items["vm_id:" + str(instance_id)] = True
return instance_id, created_items
except Exception as e:
self.logger.debug(traceback.format_exc())
self.format_vimconn_exception(e)
def get_vminstance(self, vm_id):
"""Returns the VM instance information from VIM"""
try:
self._reload_connection()
reservation = self.conn.get_all_instances(vm_id)
return reservation[0].instances[0].__dict__
except Exception as e:
self.format_vimconn_exception(e)
def delete_vminstance(self, vm_id, created_items=None, volumes_to_hold=None):
"""Removes a VM instance from VIM
Returns the instance identifier"""
try:
self._reload_connection()
self.logger.debug("DELETING VM_ID: " + str(vm_id))
reservation = self.conn.get_all_instances(vm_id)[0]
if hasattr(reservation, "instances"):
instance = reservation.instances[0]
self.conn.terminate_instances(vm_id)
if self.wait_for_vm(vm_id, "terminated"):
for interface in instance.interfaces:
self.conn_vpc.delete_network_interface(
network_interface_id=interface.id,
)
if self.network_delete_on_termination:
for net in self.network_delete_on_termination:
try:
self.conn_vpc.delete_subnet(net)
except Exception as net_delete_error:
if isinstance(net_delete_error, EC2ResponseError):
self.logger.warning(f"Deleting network {net}: failed")
else:
self.format_vimconn_exception(net_delete_error)
return vm_id
except Exception as e:
self.format_vimconn_exception(e)
def wait_for_instance_id(self, reservation):
if not reservation:
return False
self._reload_connection()
elapsed_time = 0
while elapsed_time < 30:
if reservation.instances:
instance_id = reservation.instances[0].id
return instance_id
time.sleep(5)
elapsed_time += 5
else:
raise vimconn.VimConnException(
"Failed to get instance_id for reservation",
http_code=vimconn.HTTP_Request_Timeout,
)
def wait_for_vm(self, vm_id, status):
"""wait until vm is in the desired status and return True.
If the timeout is reached generate an exception"""
self._reload_connection()
elapsed_time = 0
while elapsed_time < self.server_timeout:
if self.conn.get_all_instances(vm_id):
reservation = self.conn.get_all_instances(vm_id)[0]
if hasattr(reservation, "instances"):
instance = reservation.instances[0]
if instance.state == status:
return True
time.sleep(5)
elapsed_time += 5
# if we exceeded the timeout
else:
raise vimconn.VimConnException(
"Timeout waiting for instance " + vm_id + " to get " + status,
http_code=vimconn.HTTP_Request_Timeout,
)
def refresh_vms_status(self, vm_list):
"""Get the status of the virtual machines and their interfaces/ports
Params: the list of VM identifiers
Returns a dictionary with:
vm_id: #VIM id of this Virtual Machine
status: #Mandatory. Text with one of:
# DELETED (not found at vim)
# VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
# OTHER (Vim reported other status not understood)
# ERROR (VIM indicates an ERROR status)
# ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
# BUILD (on building process), ERROR
# ACTIVE:NoMgmtIP (Active but any of its interface has an IP address
#
error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
interfaces: list with interface info. Each item a dictionary with:
vim_interface_id - The ID of the ENI.
vim_net_id - The ID of the VPC subnet.
mac_address - The MAC address of the interface.
ip_address - The IP address of the interface within the subnet.
"""
self.logger.debug("Getting VM instance information from VIM")
try:
self._reload_connection()
elapsed_time = 0
while elapsed_time < self.server_timeout:
reservation = self.conn.get_all_instances(vm_list)[0]
if reservation:
break
time.sleep(5)
elapsed_time += 5
# if we exceeded the timeout
else:
raise vimconn.VimConnException(
vm_list + "could not be gathered, refresh vm status failed",
http_code=vimconn.HTTP_Request_Timeout,
)
instances = {}
instance_dict = {}
for instance in reservation.instances:
if hasattr(instance, "id"):
try:
if instance.state in ("pending"):
instance_dict["status"] = "BUILD"
elif instance.state in ("available", "running", "up"):
instance_dict["status"] = "ACTIVE"
else:
instance_dict["status"] = "ERROR"
instance_dict["error_msg"] = ""
instance_dict["interfaces"] = []
for interface in instance.interfaces:
interface_dict = {
"vim_interface_id": interface.id,
"vim_net_id": interface.subnet_id,
"mac_address": interface.mac_address,
}
if (
hasattr(interface, "publicIp")
and interface.publicIp is not None
):
interface_dict["ip_address"] = (
interface.publicIp
+ ";"
+ interface.private_ip_address
)
else:
interface_dict[
"ip_address"
] = interface.private_ip_address
instance_dict["interfaces"].append(interface_dict)
except Exception as e:
self.logger.error(
"Exception getting vm status: %s", str(e), exc_info=True
)
instance_dict["status"] = "DELETED"
instance_dict["error_msg"] = str(e)
finally:
instance_dictionary = vars(instance)
cleared_instance_dict = {
key: instance_dictionary[key]
for key in instance_dictionary
if not (isinstance(instance_dictionary[key], object))
}
instance_dict["vim_info"] = cleared_instance_dict
instances[instance.id] = instance_dict
return instances
except Exception as e:
self.logger.error("Exception getting vm status: %s", str(e), exc_info=True)
self.format_vimconn_exception(e)
def action_vminstance(self, vm_id, action_dict, created_items={}):
"""Send and action over a VM instance from VIM
Returns the vm_id if the action was successfully sent to the VIM"""
self.logger.debug("Action over VM '%s': %s", vm_id, str(action_dict))
try:
self._reload_connection()
if "start" in action_dict:
self.conn.start_instances(vm_id)
elif "stop" in action_dict or "stop" in action_dict:
self.conn.stop_instances(vm_id)
elif "terminate" in action_dict:
self.conn.terminate_instances(vm_id)
elif "reboot" in action_dict:
self.conn.reboot_instances(vm_id)
return None
except Exception as e:
self.format_vimconn_exception(e)
def migrate_instance(self, vm_id, compute_host=None):
"""
Migrate a vdu
param:
vm_id: ID of an instance
compute_host: Host to migrate the vdu to
"""
# TODO: Add support for migration
raise vimconn.VimConnNotImplemented("Not implemented")
def resize_instance(self, vm_id, flavor_id=None):
"""
resize a vdu
param:
vm_id: ID of an instance
flavor_id: flavor to resize the vdu
"""
# TODO: Add support for resize
raise vimconn.VimConnNotImplemented("Not implemented")