X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=rwcal%2Fplugins%2Fvala%2Frwcal_cloudsim%2Frwcal_cloudsim.py;fp=rwcal%2Fplugins%2Fvala%2Frwcal_cloudsim%2Frwcal_cloudsim.py;h=6da8a2e154f960e59857decb0c178817fa28a323;hb=6f07e6f33f751ab4ffe624f6037f887b243bece2;hp=0000000000000000000000000000000000000000;hpb=72a563886272088feb7cb52e4aafbe6d2c580ff9;p=osm%2FSO.git diff --git a/rwcal/plugins/vala/rwcal_cloudsim/rwcal_cloudsim.py b/rwcal/plugins/vala/rwcal_cloudsim/rwcal_cloudsim.py new file mode 100644 index 00000000..6da8a2e1 --- /dev/null +++ b/rwcal/plugins/vala/rwcal_cloudsim/rwcal_cloudsim.py @@ -0,0 +1,1430 @@ + +# +# Copyright 2016 RIFT.IO Inc +# +# 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. +# + +import collections +import hashlib +import itertools +import logging +import os +import time +import uuid + +import ipaddress + +from gi import require_version +require_version('RwCal', '1.0') + +from gi.repository import ( + GObject, + RwCal, + RwTypes, + RwcalYang, + ) + +import rw_status +import rift.cal.rwcal_status as rwcal_status +import rwlogger + +import rift.rwcal.cloudsim.lxc as lxc +import rift.rwcal.cloudsim.lvm as lvm +import rift.rwcal.cloudsim.net as net +import rift.rwcal.cloudsim.exceptions as exceptions + +logger = logging.getLogger('rwcal.cloudsim') + +rwstatus_exception_map = { IndexError: RwTypes.RwStatus.NOTFOUND, + KeyError: RwTypes.RwStatus.NOTFOUND, + NotImplementedError: RwTypes.RwStatus.NOT_IMPLEMENTED,} + +rwstatus = rw_status.rwstatus_from_exc_map(rwstatus_exception_map) +rwcalstatus = rwcal_status.rwcalstatus_from_exc_map(rwstatus_exception_map) + + +class UnknownAccountError(Exception): + pass + + +class MissingFileError(Exception): + pass + + +class ImageLocationError(Exception): + pass + + +class CreateNetworkError(Exception): + pass + + +rwstatus = rw_status.rwstatus_from_exc_map({ + IndexError: RwTypes.RwStatus.NOTFOUND, + KeyError: RwTypes.RwStatus.NOTFOUND, + UnknownAccountError: RwTypes.RwStatus.NOTFOUND, + MissingFileError: RwTypes.RwStatus.NOTFOUND, + }) + + +class Resources(object): + def __init__(self): + self.images = dict() + + +def rwcal_copy_object(obj): + dup = obj.__class__() + dup.copy_from(obj) + return dup + + +MGMT_NETWORK_NAME = "virbr0" +MGMT_NETWORK_INTERFACE_IP = ipaddress.IPv4Interface("192.168.122.1/24") + + +class IPPoolError(Exception): + pass + + +class NetworkIPPool(object): + def __init__(self, subnet): + self._network = ipaddress.IPv4Network(subnet) + self._ip_gen = self._network.hosts() + self._allocated_ips = [] + self._unallocated_ips = [] + + def allocate_ip(self): + try: + ip = str(next(self._ip_gen)) + except StopIteration: + try: + ip = self._unallocated_ips.pop() + except IndexError: + raise IPPoolError("All ip addresses exhausted") + + self._allocated_ips.append(ip) + return ip + + def deallocate_ip(self, ip): + if ip not in self._allocated_ips: + raise ValueError("Did not find IP %s in allocate ip pool") + + self._allocated_ips.remove(ip) + self._unallocated_ips.append(ip) + + +class CalManager(object): + def __init__(self): + self._vms = {} + self._ports = {} + self._images = {} + self._networks = {} + self.flavors = {} + + self._port_to_vm = {} + self._vm_to_image = {} + self._port_to_network = {} + self._network_to_ip_pool = {} + + self._vm_to_ports = collections.defaultdict(list) + self._image_to_vms = collections.defaultdict(list) + self._network_to_ports = collections.defaultdict(list) + + self._vm_id_gen = itertools.count(1) + self._network_id_gen = itertools.count(1) + self._image_id_gen = itertools.count(1) + + def add_image(self, image): + image_id = str(next(self._image_id_gen)) + self._images[image_id] = image + + return image_id + + def remove_image(self, image_id): + for vm_id in self.get_image_vms(image_id): + self.remove_vm(vm_id) + + del self._images[image_id] + del self._image_to_vms[image_id] + + def get_image(self, image_id): + if image_id not in self._images: + msg = "Unable to find image {}" + raise exceptions.RWErrorNotFound(msg.format(image_id)) + + return self._images[image_id] + + def get_image_list(self): + return list(self._images.values()) + + def get_image_vms(self, image_id): + if image_id not in self._images: + msg = "Unable to find image {}" + raise exceptions.RWErrorNotFound(msg.format(image_id)) + + return self._image_to_vms[image_id] + + def add_port(self, network_id, vm_id, port): + if network_id not in self._networks: + msg = "Unable to find network {}" + raise exceptions.RWErrorNotFound(msg.format(network_id)) + + if vm_id not in self._vms: + msg = "Unable to find vm {}" + raise exceptions.RWErrorNotFound(msg.format(vm_id)) + + port_id = str(uuid.uuid4()) + self._ports[port_id] = port + + self._vm_to_ports[vm_id].append(port_id) + self._network_to_ports[network_id].append(port_id) + + self._port_to_vm[port_id] = vm_id + self._port_to_network[port_id] = network_id + + return port_id + + def remove_port(self, port_id): + if port_id not in self._ports: + msg = "Unable to find port {}" + raise exceptions.RWErrorNotFound(msg.format(port_id)) + + network_id = self._port_to_network[port_id] + vm_id = self._port_to_vm[port_id] + + self._vm_to_ports[vm_id].remove(port_id) + self._network_to_ports[network_id].remove(port_id) + + del self._ports[port_id] + del self._port_to_vm[port_id] + del self._port_to_network[port_id] + + def get_port(self, port_id): + return self._ports[port_id] + + def get_port_list(self): + return list(self._ports.values()) + + def add_network(self, network): + network_id = str(next(self._network_id_gen)) + self._networks[network_id] = network + + return network_id + + def remove_network(self, network_id): + for port_id in self.get_network_ports(network_id): + self.remove_port(port_id) + + del self._networks[network_id] + + def get_network(self, network_id): + return self._networks[network_id] + + def add_network_ip_pool(self, network_id, ip_pool): + self._network_to_ip_pool[network_id] = ip_pool + + def get_network_ip_pool(self, network_id): + return self._network_to_ip_pool[network_id] + + def remove_network_ip_pool(self, network_id): + del self._network_to_ip_pool[network_id] + + def get_network_list(self): + return list(self._networks.values()) + + def get_network_ports(self, network_id): + return self._network_to_ports[network_id] + + def add_vm(self, image_id, vm): + if image_id not in self._images: + msg = "Unable to find image {}" + raise exceptions.RWErrorNotFound(msg.format(image_id)) + + vm_id = str(next(self._vm_id_gen)) + self._vms[vm_id] = vm + + self._vm_to_image[vm_id] = image_id + self._image_to_vms[image_id].append(vm_id) + + return vm_id + + def remove_vm(self, vm_id): + for port_id in self.get_vm_ports(vm_id): + self.remove_port(port_id) + + image_id = self._vm_to_image[vm_id] + + self._image_to_vms[image_id].remove(vm_id) + + del self._vms[vm_id] + del self._vm_to_image[vm_id] + + def get_vm(self, vm_id): + return self._vms[vm_id] + + def get_vm_list(self): + return list(self._vms.values()) + + def get_vm_ports(self, vm_id): + return self._vm_to_ports[vm_id] + + +class LxcManager(object): + def __init__(self): + self._containers = {} + self._ports = {} + self._bridges = {} + + self._port_to_container = {} + self._port_to_bridge = {} + + self._container_to_ports = collections.defaultdict(list) + self._bridge_to_ports = collections.defaultdict(list) + + # Create the management network + self.mgmt_network = RwcalYang.NetworkInfoItem() + self.mgmt_network.network_name = MGMT_NETWORK_NAME + + network = MGMT_NETWORK_INTERFACE_IP.network + self.mgmt_network.subnet = str(network) + + # Create/Start the default virtd network for NAT-based + # connectivity inside containers (http://wiki.libvirt.org/page/Networking) + if "default" not in net.virsh_list_network_names(): + logger.debug("default virtd network not found. Creating.") + net.virsh_define_default() + + # The default virsh profile create a virbr0 interface + # with a 192.168.122.1 ip address. Also sets up iptables + # for NAT access. + net.virsh_start("default") + + # Create the IP pool + mgmt_network_hosts = network.hosts() + + # Remove the management interface ip from the pool + self._mgmt_ip_pool = list(mgmt_network_hosts) + self._mgmt_ip_pool.remove(MGMT_NETWORK_INTERFACE_IP.ip) + + def acquire_mgmt_ip(self): + """Returns an IP address from the available pool""" + # TODO these ips will need to be recycled at some point + return str(self._mgmt_ip_pool.pop()) + + def add_port(self, bridge_id, container_id, port): + if bridge_id not in self._bridges: + msg = "Unable to find bridge {}" + raise exceptions.RWErrorNotFound(msg.format(bridge_id)) + + if container_id not in self._containers: + msg = "Unable to find container {}" + raise exceptions.RWErrorNotFound(msg.format(container_id)) + + port_id = str(uuid.uuid4()) + self._ports[port_id] = port + + self._container_to_ports[container_id].append(port_id) + self._bridge_to_ports[bridge_id].append(port_id) + + self._port_to_container[port_id] = container_id + self._port_to_bridge[port_id] = bridge_id + + return port_id + + def remove_port(self, port_id): + if port_id not in self._ports: + msg = "Unable to find port {}" + raise exceptions.RWErrorNotFound(msg.format(port_id)) + + bridge_id = self._port_to_bridge[port_id] + container_id = self._port_to_container[port_id] + + self._container_to_ports[container_id].remove(port_id) + self._bridge_to_ports[bridge_id].remove(port_id) + + del self._ports[port_id] + del self._port_to_bridge[port_id] + del self._port_to_container[port_id] + + def get_port(self, port_id): + return self._ports[port_id] + + def add_bridge(self, bridge): + bridge_id = str(uuid.uuid4()) + self._bridges[bridge_id] = bridge + + return bridge_id + + def remove_bridge(self, bridge_id): + for port_id in self._bridge_to_ports[bridge_id]: + self.remove_port(port_id) + + del self._bridges[bridge_id] + + def get_bridge(self, bridge_id): + return self._bridges[bridge_id] + + def get_bridge_ports(self, bridge_id): + port_ids = self._bridge_to_ports[bridge_id] + return [self.get_port(port_id) for port_id in port_ids] + + def add_container(self, container): + container_id = str(uuid.uuid4()) + self._containers[container_id] = container + + return container_id + + def remove_container(self, container_id): + for port_id in self.get_container_ports(container_id): + self.remove_port(port_id) + + del self._containers[container_id] + + def get_container(self, container_id): + return self._containers[container_id] + + def get_container_ports(self, container_id): + return self._container_to_ports[container_id] + + + +class Datastore(object): + """ + This class is used to store data that is shared among different instance of + the Container class. + """ + def __init__(self): + self.lxc_manager = LxcManager() + self.cal_manager = CalManager() + self.cal_to_lxc = {'image': {}, 'port': {}, 'network': {}, 'vm': {}} + self.last_index = 0 + + +class CloudSimPlugin(GObject.Object, RwCal.Cloud): + # HACK this is a work-around for sharing/persisting container information. + # This will only work for instances of CloudSimPlugin that are within the + # same process. Thus, it works in collapsed mode, but will not work in + # expanded mode. At the point where it is necessary to persist this + # information in expanded mode, we will need to find a better solution. + datastore = None + + def __init__(self): + GObject.Object.__init__(self) + if CloudSimPlugin.datastore is None: + CloudSimPlugin.datastore = Datastore() + + @property + def lxc(self): + return CloudSimPlugin.datastore.lxc_manager + + @property + def cal(self): + return CloudSimPlugin.datastore.cal_manager + + @property + def volume_group(self): + return lvm.get("rift") + + @property + def cal_to_lxc(self): + return CloudSimPlugin.datastore.cal_to_lxc + + def next_snapshot_name(self): + """Generates a new snapshot name for a container""" + CloudSimPlugin.datastore.last_index += 1 + return 'rws{}'.format(CloudSimPlugin.datastore.last_index) + + @rwstatus + def do_init(self, rwlog_ctx): + if not any(isinstance(h, rwlogger.RwLogger) for h in logger.handlers): + logger.addHandler( + rwlogger.RwLogger( + category="rw-cal-log", + subcategory="cloudsim", + log_hdl=rwlog_ctx, + ) + ) + + @rwstatus(ret_on_failure=[None]) + def do_validate_cloud_creds(self, account): + """ + Validates the cloud account credentials for the specified account. + If creds are not valid, returns an error code & reason string + Arguments: + account - a cloud account to validate + + Returns: + Validation Code and Details String + """ + status = RwcalYang.CloudConnectionStatus( + status="success", + details="" + ) + + return status + + @rwstatus(ret_on_failure=[None]) + def do_get_management_network(self, account): + """Returns the management network + + Arguments: + account - a cloud account + + Returns: + a NetworkInfo object + + """ + return self.lxc.mgmt_network + + @rwstatus + def do_create_tenant(self, account, name): + """ + Create a new tenant. + + @param name - name to assign to the tenant. + """ + raise NotImplementedError() + + @rwstatus + def do_delete_tenant(self, account, tenant_id): + """ + delete a tenant. + + @param tenant_id - id of tenant to be deleted. + """ + raise NotImplementedError() + + @rwstatus(ret_on_failure=[[]]) + def do_get_tenant_list(self, account): + """ + List tenants. + + """ + raise NotImplementedError() + + @rwstatus + def do_create_role(self, account, name): + """ + Create a new role. + + @param name - name to assign to the role. + """ + raise NotImplementedError() + + @rwstatus + def do_delete_role(self, account, role_id): + """ + delete a role. + + @param role_id - id of role to be deleted. + """ + raise NotImplementedError() + + @rwstatus(ret_on_failure=[[]]) + def do_get_role_list(self, account): + """ + List roles. + + """ + raise NotImplementedError() + + @rwstatus(ret_on_failure=[None]) + def do_create_image(self, account, image): + """Create a new image + + Creates a new container based upon the template and tarfile specified. + Only one image is currently supported for a given instance of the CAL. + + Arguments: + account - a cloud account + image - an ImageInfo object + + Raises: + An RWErrorDuplicate is raised if create_image is called and there + is already an image. + + Returns: + The UUID of the new image + + """ + def file_md5(path, block_size=2 ** 20): + """ + Block size directly depends on the block size of your filesystem + to avoid performances issues. + """ + md5 = hashlib.md5() + with open(path, 'rb') as f: + for chunk in iter(lambda: f.read(block_size), b''): + md5.update(chunk) + + return md5.hexdigest() + + current_images = self.cal.get_image_list() + lxc_name = "rwm{}".format(len(current_images)) + + if not image.has_field("disk_format"): + logger.warning("Image disk format not provided assuming qcow2") + image.disk_format = "qcow2" + + if image.disk_format not in ["qcow2"]: + msg = "Only qcow2 currently supported for container CAL" + raise exceptions.RWErrorNotSupported(msg) + + logger.debug('Calculating IMAGE checksum...') + image.checksum = file_md5(image.location) + logger.debug("Calculated image checksum: %s", image.checksum) + image.state = 'active' + + container = lxc.create_container( + name=lxc_name, + template_path=os.path.join( + os.environ['RIFT_INSTALL'], + "etc/lxc-fedora-rift.lxctemplate", + ), + volume="rift", + rootfs_qcow2file=image.location, + ) + + + # Add the images to the managers + cal_image_id = self.cal.add_image(image) + lxc_image_id = self.lxc.add_container(container) + + # Create the CAL to LXC mapping + self.cal_to_lxc["image"][cal_image_id] = lxc_image_id + + image.id = cal_image_id + + return image.id + + @rwstatus + def do_delete_image(self, account, image_id): + """Deletes an image + + This function will remove the record of the image from the CAL and + destroy the associated container. + + Arguments: + account - a cloud account + image_id - the UUID of the image to delete + + Raises: + An RWErrorNotEmpty exception is raised if there are VMs based on + this image (the VMs need to be deleted first). An RWErrorNotFound + is raised if the image_id does not match any of the known images. + + """ + container_id = self.cal_to_lxc["image"][image_id] + container = self.lxc.get_container(container_id) + + # Stop the image and destroy it (NB: it should not be necessary to stop + # the container, but just in case) + container.stop() + container.destroy() + + self.cal.remove_image(image_id) + self.lxc.remove_container(container_id) + + @rwstatus(ret_on_failure=[None]) + def do_get_image(self, account, image_id): + """Returns the specified image + + Arguments: + account - a cloud account + image_id - the UUID of the image to retrieve + + Raises: + An RWErrorNotFound exception is raised if the image_id does not + match any of the known images. + + Returns: + An image object + + """ + return self.cal.get_image(image_id) + + @rwstatus(ret_on_failure=[[]]) + def do_get_image_list(self, account): + """Returns a list of images""" + resources = RwcalYang.VimResources() + for image in self.cal.get_image_list(): + resources.imageinfo_list.append(rwcal_copy_object(image)) + + return resources + + @rwstatus + def do_create_vm(self, account, vm): + """Create a VM + + Arguments: + vm - the VM info used to define the desire VM + + Raises: + An RWErrorFailure is raised if there is not + + Returns: + a string containing the unique id of the created VM + + """ + # Retrieve the container that will be used as the base of the snapshot + container_id = self.cal_to_lxc["image"][vm.image_id] + container = self.lxc.get_container(container_id) + + # Create a container snapshot + snapshot = container.snapshot(self.next_snapshot_name()) + snapshot.hostname = vm.vm_name + + # Register the vm and container + snapshot_id = self.lxc.add_container(snapshot) + vm.vm_id = self.cal.add_vm(vm.image_id, vm) + + self.cal_to_lxc["vm"][vm.vm_id] = snapshot_id + + return vm.vm_id + + @rwstatus + def do_start_vm(self, account, vm_id): + """Starts the specified VM + + Arguments: + vm_id - the id of the vm to start + + Raises: + An RWErrorNotFound is raised if the specified vm id is not known to + this driver. + + """ + if vm_id not in self.cal_to_lxc["vm"]: + msg = "Unable to find the specified VM ({})" + raise exceptions.RWErrorNotFound(msg.format(vm_id)) + + container_id = self.cal_to_lxc["vm"][vm_id] + + snapshot = self.lxc.get_container(container_id) + port_ids = self.lxc.get_container_ports(container_id) + + config = lxc.ContainerConfig(snapshot.name) + + for port_id in port_ids: + port = self.lxc.get_port(port_id) + config.add_network_config(port) + + vm = self.cal.get_vm(vm_id) + + # Set the management IP on the vm if not yet set + if not vm.has_field("management_ip"): + mgmt_ip = self.lxc.acquire_mgmt_ip() + vm.management_ip = mgmt_ip + + # Add the management interface + config.add_network_config( + lxc.NetworkConfig( + type="veth", + link=self.lxc.mgmt_network.network_name, + name="eth0", + ipv4=vm.management_ip, + ipv4_gateway='auto', + ) + ) + + # Add rift root as a mount point + config.add_mount_point_config( + lxc.MountConfig( + local=os.environ["RIFT_ROOT"], + remote=os.environ["RIFT_ROOT"][1:], + read_only=False, + ) + ) + + userdata=None + if vm.cloud_init.has_field("userdata"): + userdata = vm.cloud_init.userdata + + snapshot.configure(config, userdata=userdata) + # For some reason, the cloud-init fails or runs only partially when + # you start the container immediately after writing the config files. + # A sleep of 1 sec seems to magically fix the issue!! + time.sleep(1) + snapshot.start() + + @rwstatus + def do_stop_vm(self, account, vm_id): + """Stops the specified VM + + Arguments: + vm_id - the id of the vm to stop + + Raises: + An RWErrorNotFound is raised if the specified vm id is not known to + this driver. + + """ + if vm_id not in self.cal_to_lxc["vm"]: + msg = "Unable to find the specified VM ({})" + raise exceptions.RWErrorNotFound(msg.format(vm_id)) + + # Stop the container + container_id = self.cal_to_lxc["vm"][vm_id] + snapshot = self.lxc.get_container(container_id) + snapshot.stop() + + @rwstatus + def do_delete_vm(self, account, vm_id): + """Deletes the specified VM + + Arguments: + vm_id - the id of the vm to delete + + Raises: + An RWErrorNotFound is raised if the specified vm id is not known to + this driver. + + """ + if vm_id not in self.cal_to_lxc["vm"]: + msg = "Unable to find the specified VM ({})" + raise exceptions.RWErrorNotFound(msg.format(vm_id)) + + container_id = self.cal_to_lxc["vm"][vm_id] + + snapshot = self.lxc.get_container(container_id) + snapshot.stop() + snapshot.destroy() + + self.cal.remove_vm(vm_id) + self.lxc.remove_container(container_id) + + # TODO: Recycle management ip + + @rwstatus + def do_reboot_vm(self, account, vm_id): + """ + reboot a virtual machine. + + @param vm_id - Instance id of VM to be deleted. + """ + self.do_stop_vm(account, vm_id, no_rwstatus=True) + self.do_start_vm(account, vm_id, no_rwstatus=True) + + @rwstatus + def do_get_vm(self, account, vm_id): + """Returns the specified VM + + Arguments: + vm_id - the id of the vm to return + + Raises: + An RWErrorNotFound is raised if the specified vm id is not known to + this driver. + + Returns: + a VMInfoItem object + + """ + if vm_id not in self.cal_to_lxc["vm"]: + msg = "Unable to find the specified VM ({})" + raise exceptions.RWErrorNotFound(msg.format(vm_id)) + + return self.cal.get_vm(vm_id) + + @rwstatus(ret_on_failure=[[]]) + def do_get_vm_list(self, account): + """Returns the a list of the VMs known to the driver + + Returns: + a list of VMInfoItem objects + + """ + resources = RwcalYang.VimResources() + for vm in self.cal.get_vm_list(): + resources.vminfo_list.append(rwcal_copy_object(vm)) + + return resources + + @rwstatus + def do_create_flavor(self, account, flavor): + """ + create new flavor. + + @param flavor - Flavor object + """ + flavor_id = str(uuid.uuid4()) + flavor.id = flavor_id + self.cal.flavors[flavor_id] = flavor + logger.debug('Created flavor: {}'.format(flavor_id)) + return flavor_id + + @rwstatus + def do_delete_flavor(self, account, flavor_id): + """ + Delete flavor. + + @param flavor_id - Flavor id to be deleted. + """ + logger.debug('Deleted flavor: {}'.format(flavor_id)) + self.cal.flavors.pop(flavor_id) + + @rwstatus(ret_on_failure=[None]) + def do_get_flavor(self, account, flavor_id): + """ + Return the specified flavor + + @param flavor_id - the id of the flavor to return + """ + flavor = self.cal.flavors[flavor_id] + logger.debug('Returning flavor-info for : {}'.format(flavor_id)) + return flavor + + @rwstatus(ret_on_failure=[[]]) + def do_get_flavor_list(self, account): + """ + Return a list of flavors + """ + vim_resources = RwcalYang.VimResources() + for flavor in self.cal.flavors.values(): + f = RwcalYang.FlavorInfoItem() + f.copy_from(flavor) + vim_resources.flavorinfo_list.append(f) + logger.debug("Returning list of flavor-info of size: %d", len(vim_resources.flavorinfo_list)) + return vim_resources + + @rwstatus + def do_add_host(self, account, host): + raise NotImplementedError() + + @rwstatus + def do_remove_host(self, account, host_id): + raise NotImplementedError() + + @rwstatus(ret_on_failure=[None]) + def do_get_host(self, account, host_id): + raise NotImplementedError() + + @rwstatus(ret_on_failure=[[]]) + def do_get_host_list(self, account): + raise NotImplementedError() + + @rwstatus + def do_create_port(self, account, port): + """Create a port between a network and a virtual machine + + Arguments: + account - a cloud account + port - a description of port to create + + Raises: + Raises an RWErrorNotFound exception if either the network or the VM + associated with the port cannot be found. + + Returns: + the ID of the newly created port. + + """ + if port.network_id not in self.cal_to_lxc["network"]: + msg = 'Unable to find the specified network ({})' + raise exceptions.RWErrorNotFound(msg.format(port.network_id)) + + if port.vm_id not in self.cal_to_lxc["vm"]: + msg = "Unable to find the specified VM ({})" + raise exceptions.RWErrorNotFound(msg.format(port.vm_id)) + + if port.has_field("ip_address"): + raise exceptions.RWErrorFailure("IP address of the port must not be specific") + + network = self.cal.get_network(port.network_id) + ip_pool = self.cal.get_network_ip_pool(port.network_id) + port.ip_address = ip_pool.allocate_ip() + + net_config = lxc.NetworkConfig( + type='veth', + link=network.network_name[:15], + name="veth" + str(uuid.uuid4())[:10], + ipv4=port.ip_address, + ) + + lxc_network_id = self.cal_to_lxc["network"][port.network_id] + lxc_vm_id = self.cal_to_lxc["vm"][port.vm_id] + + cal_port_id = self.cal.add_port(port.network_id, port.vm_id, port) + lxc_port_id = self.lxc.add_port(lxc_network_id, lxc_vm_id, net_config) + + self.cal_to_lxc["port"][cal_port_id] = lxc_port_id + port.port_id = cal_port_id + + return port.port_id + + @rwstatus + def do_delete_port(self, account, port_id): + """Delete the specified port + + Arguments: + account - a cloud account + port_id - the ID of the port to delete + + Raises: + A RWErrorNotFound exception is raised if the specified port cannot + be found. + + """ + if port_id not in self.cal_to_lxc["port"]: + msg = "Unable to find the specified port ({})" + raise exceptions.RWErrorNotFound(msg.format(port_id)) + + lxc_port_id = self.cal_to_lxc["port"][port_id] + + # Release the port's ip address back into the network pool + port = self.cal.get_port(port_id) + ip_pool = self.cal.get_network_ip_pool(port.network_id) + ip_pool.deallocate_ip(port.ip_address) + + self.cal.remove_port(port_id) + self.lxc.remove_port(lxc_port_id) + + del self.cal_to_lxc["port"][port_id] + + @rwstatus(ret_on_failure=[None]) + def do_get_port(self, account, port_id): + """Return the specified port + + Arguments: + account - a cloud account + port_id - the ID of the port to return + + Raises: + A RWErrorNotFound exception is raised if the specified port cannot + be found. + + Returns: + The specified port. + + """ + if port_id not in self.cal_to_lxc["port"]: + msg = "Unable to find the specified port ({})" + raise exceptions.RWErrorNotFound(msg.format(port_id)) + + return self.cal.get_port(port_id) + + @rwstatus(ret_on_failure=[[]]) + def do_get_port_list(self, account): + """Returns a list of ports""" + resources = RwcalYang.VimResources() + for port in self.datastore.cal_manager.get_port_list(): + resources.portinfo_list.append(rwcal_copy_object(port)) + + return resources + + @rwstatus + def do_create_network(self, account, network): + """Create a network + + Arguments: + account - a cloud account + network - a description of the network to create + + Returns: + The ID of the newly created network + + """ + + # Create the network + try: + # Setup a pool of mgmt IPv4 addresses + if net.bridge_exists(network.network_name): + logger.warning("Bridge %s already exists. Removing.", network.network_name) + net.bridge_down(network.network_name) + net.bridge_remove(network.network_name) + + # Ensure that the subnet field was filled out and is valid + if not network.has_field("subnet"): + raise CreateNetworkError("subnet not provided in create network request") + + try: + ipaddress.IPv4Network(network.subnet) + except ValueError as e: + raise CreateNetworkError("Could not convert subnet into a " + "IPv4Network: %s" % str(network.subnet)) + + ip_pool = NetworkIPPool(network.subnet) + + # Create the management bridge with interface information + net.create(network.network_name) + + except Exception as e: + logger.warning(str(e)) + + # Register the network + cal_network_id = self.cal.add_network(network) + lxc_network_id = self.lxc.add_bridge(network) + self.cal.add_network_ip_pool(cal_network_id, ip_pool) + + self.cal_to_lxc["network"][cal_network_id] = lxc_network_id + + # Set the ID of the network object + network.network_id = cal_network_id + + return network.network_id + + @rwstatus + def do_delete_network(self, account, network_id): + """ + Arguments: + account - a cloud account + network_id - the UUID of the network to delete + + Raises: + An RWErrorNotFound is raised if the specified network cannot be + found. + + """ + if network_id not in self.cal_to_lxc["network"]: + msg = "Unable to find the specified network ({})" + raise exceptions.RWErrorNotFound(msg.format(network_id)) + + # Get the associated bridge ID + bridge_id = self.cal_to_lxc["network"][network_id] + + # Delete the network + network = self.cal.get_network(network_id) + net.delete(network.network_name) + + # Remove the network records + self.lxc.remove_bridge(bridge_id) + self.cal.remove_network(network_id) + del self.cal_to_lxc["network"][network_id] + + @rwstatus(ret_on_failure=[None]) + def do_get_network(self, account, network_id): + """Returns the specified network + + Arguments: + account - a cloud account + network_id - the UUID of the network to delete + + Raises: + An RWErrorNotFound is raised if the specified network cannot be + found. + + Returns: + The specified network + + """ + return self.cal.get_network(network_id) + + @rwstatus(ret_on_failure=[[]]) + def do_get_network_list(self, account): + """Returns a list of network objects""" + resources = RwcalYang.VimResources() + for network in self.cal.get_network_list(): + resources.networkinfo_list.append(rwcal_copy_object(network)) + + return resources + + @rwcalstatus(ret_on_failure=[""]) + def do_create_virtual_link(self, account, link_params): + """Create a new virtual link + + Arguments: + account - a cloud account + link_params - information that defines the type of VDU to create + + Returns: + The vdu_id + """ + network = RwcalYang.NetworkInfoItem() + network.network_name = link_params.name + network.subnet = link_params.subnet + + if link_params.has_field("provider_network"): + logger.warning("Container CAL does not implement provider network") + + rs, net_id = self.do_create_network(account, network) + if rs != RwTypes.RwStatus.SUCCESS: + raise exceptions.RWErrorFailure(rs) + + return net_id + + @rwstatus + def do_delete_virtual_link(self, account, link_id): + """Delete a virtual link + + Arguments: + account - a cloud account + link_id - id for the virtual-link to be deleted + + Returns: + None + """ + + network_ports = self.cal.get_network_ports(link_id) + for port_id in network_ports: + self.do_delete_port(account, port_id, no_rwstatus=True) + + self.do_delete_network(account, link_id, no_rwstatus=True) + + @staticmethod + def fill_connection_point_info(c_point, port_info): + """Create a GI object for RwcalYang.VDUInfoParams_ConnectionPoints() + + Converts Port information dictionary object returned by container cal + driver into Protobuf Gi Object + + Arguments: + port_info - Port information from container cal + Returns: + Protobuf Gi object for RwcalYang.VDUInfoParams_ConnectionPoints + """ + c_point.name = port_info.port_name + c_point.connection_point_id = port_info.port_id + c_point.ip_address = port_info.ip_address + c_point.state = 'active' + c_point.virtual_link_id = port_info.network_id + c_point.vdu_id = port_info.vm_id + + @staticmethod + def create_virtual_link_info(network_info, port_list): + """Create a GI object for VirtualLinkInfoParams + + Converts Network and Port information dictionary object + returned by container manager into Protobuf Gi Object + + Arguments: + network_info - Network information from container cal + port_list - A list of port information from container cal + subnet: Subnet information from openstack + Returns: + Protobuf Gi object for VirtualLinkInfoParams + """ + link = RwcalYang.VirtualLinkInfoParams() + link.name = network_info.network_name + link.state = 'active' + link.virtual_link_id = network_info.network_id + for port in port_list: + c_point = link.connection_points.add() + CloudSimPlugin.fill_connection_point_info(c_point, port) + + link.subnet = network_info.subnet + + return link + + @rwstatus(ret_on_failure=[None]) + def do_get_virtual_link(self, account, link_id): + """Get information about virtual link. + + Arguments: + account - a cloud account + link_id - id for the virtual-link + + Returns: + Object of type RwcalYang.VirtualLinkInfoParams + """ + + network = self.do_get_network(account, link_id, no_rwstatus=True) + port_ids = self.cal.get_network_ports(network.network_id) + ports = [self.cal.get_port(p_id) for p_id in port_ids] + + virtual_link = CloudSimPlugin.create_virtual_link_info( + network, ports + ) + + return virtual_link + + @rwstatus(ret_on_failure=[None]) + def do_get_virtual_link_list(self, account): + """Get information about all the virtual links + + Arguments: + account - a cloud account + + Returns: + A list of objects of type RwcalYang.VirtualLinkInfoParams + """ + networks = self.do_get_network_list(account, no_rwstatus=True) + vnf_resources = RwcalYang.VNFResources() + for network in networks.networkinfo_list: + virtual_link = self.do_get_virtual_link(account, network.network_id, no_rwstatus=True) + vnf_resources.virtual_link_info_list.append(virtual_link) + + return vnf_resources + + def _create_connection_point(self, account, c_point, vdu_id): + """ + Create a connection point + Arguments: + account - a cloud account + c_point - connection_points + """ + port = RwcalYang.PortInfoItem() + port.port_name = c_point.name + port.network_id = c_point.virtual_link_id + port.port_type = 'normal' ### Find Port type from network_profile under cloud account + port.vm_id = vdu_id + port_id = self.do_create_port(account, port, no_rwstatus=True) + return port_id + + @rwcalstatus(ret_on_failure=[""]) + def do_create_vdu(self, account, vdu_init): + """Create a new virtual deployment unit + + Arguments: + account - a cloud account + vdu_init - information about VDU to create (RwcalYang.VDUInitParams) + + Returns: + The vdu_id + """ + ### Create VM + vm = RwcalYang.VMInfoItem() + vm.vm_name = vdu_init.name + vm.image_id = vdu_init.image_id + if vdu_init.vdu_init.has_field('userdata'): + vm.cloud_init.userdata = vdu_init.vdu_init.userdata + vm.user_tags.node_id = vdu_init.node_id + + vm_id = self.do_create_vm(account, vm, no_rwstatus=True) + + ### Now create required number of ports aka connection points + port_list = [] + for c_point in vdu_init.connection_points: + virtual_link_id = c_point.virtual_link_id + + # Attempt to fetch the network to verify that the network + # already exists. + self.do_get_network(account, virtual_link_id, no_rwstatus=True) + + port_id = self._create_connection_point(account, c_point, vm_id) + port_list.append(port_id) + + # Finally start the vm + self.do_start_vm(account, vm_id, no_rwstatus=True) + + return vm_id + + @rwstatus + def do_modify_vdu(self, account, vdu_modify): + """Modify Properties of existing virtual deployment unit + + Arguments: + account - a cloud account + vdu_modify - Information about VDU Modification (RwcalYang.VDUModifyParams) + """ + ### First create required number of ports aka connection points + port_list = [] + network_list = [] + if not vdu_modify.has_field("vdu_id"): + raise ValueError("vdu_id must not be empty") + + for c_point in vdu_modify.connection_points_add: + if not c_point.has_field("virtual_link_id"): + raise ValueError("virtual link id not provided") + + network_list.append(c_point.virtual_link_id) + port_id = self._create_connection_point(account, c_point, vdu_modify.vdu_id) + port_list.append(port_id) + + ### Delete the requested connection_points + for c_point in vdu_modify.connection_points_remove: + self.do_delete_port(account, c_point.connection_point_id, no_rwstatus=True) + + self.do_reboot_vm(account, vdu_modify.vdu_id) + + @rwstatus + def do_delete_vdu(self, account, vdu_id): + """Delete a virtual deployment unit + + Arguments: + account - a cloud account + vdu_id - id for the vdu to be deleted + + Returns: + None + """ + ### Get list of port on VM and delete them. + port_id_list = self.cal.get_vm_ports(vdu_id) + ports = [self.cal.get_port(p_id) for p_id in port_id_list] + for port in ports: + self.do_delete_port(account, port.port_id, no_rwstatus=True) + self.do_delete_vm(account, vdu_id, no_rwstatus=True) + + @staticmethod + def fill_vdu_info(vm_info, port_list): + """create a gi object for vduinfoparams + + converts vm information dictionary object returned by openstack + driver into protobuf gi object + + arguments: + vm_info - vm information from openstack + mgmt_network - management network + port_list - a list of port information from container cal + returns: + protobuf gi object for vduinfoparams + """ + vdu = RwcalYang.VDUInfoParams() + vdu.name = vm_info.vm_name + vdu.vdu_id = vm_info.vm_id + vdu.management_ip = vm_info.management_ip + vdu.public_ip = vm_info.management_ip + vdu.node_id = vm_info.user_tags.node_id + vdu.image_id = vm_info.image_id + vdu.state = 'active' + + # fill the port information + for port in port_list: + c_point = vdu.connection_points.add() + CloudSimPlugin.fill_connection_point_info(c_point, port) + + vdu.vm_flavor.vcpu_count = 1 + vdu.vm_flavor.memory_mb = 8 * 1024 # 8GB + vdu.vm_flavor.storage_gb = 10 + + return vdu + + @rwstatus(ret_on_failure=[None]) + def do_get_vdu(self, account, vdu_id): + """Get information about a virtual deployment unit. + + Arguments: + account - a cloud account + vdu_id - id for the vdu + + Returns: + Object of type RwcalYang.VDUInfoParams + """ + port_id_list = self.cal.get_vm_ports(vdu_id) + ports = [self.cal.get_port(p_id) for p_id in port_id_list] + vm_info = self.do_get_vm(account, vdu_id, no_rwstatus=True) + vdu_info = CloudSimPlugin.fill_vdu_info(vm_info, ports) + + return vdu_info + + @rwstatus(ret_on_failure=[None]) + def do_get_vdu_list(self, account): + """Get information about all the virtual deployment units + + Arguments: + account - a cloud account + + Returns: + A list of objects of type RwcalYang.VDUInfoParams + """ + + vnf_resources = RwcalYang.VNFResources() + + vm_resources = self.do_get_vm_list(account, no_rwstatus=True) + for vm in vm_resources.vminfo_list: + port_list = self.cal.get_vm_ports(vm.vm_id) + port_list = [self.cal.get_port(port_id) for port_id in port_list] + vdu = CloudSimPlugin.fill_vdu_info(vm, port_list) + vnf_resources.vdu_info_list.append(vdu) + + return vnf_resources