aboutsummaryrefslogtreecommitdiffstats
path: root/vicn/resource/lxd/lxc_container.py
diff options
context:
space:
mode:
Diffstat (limited to 'vicn/resource/lxd/lxc_container.py')
-rw-r--r--vicn/resource/lxd/lxc_container.py317
1 files changed, 317 insertions, 0 deletions
diff --git a/vicn/resource/lxd/lxc_container.py b/vicn/resource/lxd/lxc_container.py
new file mode 100644
index 00000000..afa64aba
--- /dev/null
+++ b/vicn/resource/lxd/lxc_container.py
@@ -0,0 +1,317 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Cisco and/or its affiliates.
+#
+# 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 logging
+import shlex
+import time
+
+# Suppress logging from pylxd dependency on ws4py
+# (this needs to be included before pylxd)
+from ws4py import configure_logger
+configure_logger(level=logging.ERROR)
+import pylxd
+
+from netmodel.model.type import String, Integer, Bool, Self
+from vicn.core.address_mgr import AddressManager
+from vicn.core.attribute import Attribute, Reference, Multiplicity
+from vicn.core.commands import ReturnValue
+from vicn.core.exception import ResourceNotFound
+from vicn.core.requirement import Requirement
+from vicn.core.resource_mgr import wait_resource_task
+from vicn.core.task import task, inline_task, BashTask
+from vicn.resource.linux.net_device import NetDevice
+from vicn.resource.node import Node
+from vicn.resource.vpp.scripts import APPARMOR_VPP_PROFILE
+
+log = logging.getLogger(__name__)
+
+# Default name of VICN management/monitoring interface
+DEFAULT_LXC_NETDEVICE = 'eth0'
+
+# Default remote server (pull mode only)
+DEFAULT_SOURCE_URL = 'https://cloud-images.ubuntu.com/releases/'
+
+# Default protocol used to download images (lxd or simplestreams)
+DEFAULT_SOURCE_PROTOCOL = 'simplestreams'
+
+# Commands used to interact with LXD (in addition to pylxd bindings)
+CMD_GET_PID='lxc info {container.name} | grep Pid | cut -d " " -f 2'
+
+# Type: ContainerName
+ContainerName = String(max_size = 64, ascii = True,
+ forbidden = ('/', ',', ':'))
+
+class LxcContainer(Node):
+ """
+ Resource: LxcContainer
+
+ Todo:
+ - Remove VPP dependency
+ - The bridge is not strictly needed, but we currently have no automated
+ way to determine whether we need it or not
+ - The management interface should be added by VICN, not part of the
+ resource, and its name should be determined automatically.
+ """
+
+ architecture = Attribute(String, description = 'Architecture',
+ default = 'x86_64')
+ container_name = Attribute(ContainerName,
+ description = 'Name of the container',
+ default = Reference(Self, 'name'))
+ ephemeral = Attribute(Bool, description = 'Ephemeral container flag',
+ default = False)
+ node = Attribute(Node,
+ description = 'Node on which the container is running',
+ mandatory = True,
+ requirements = [
+ # We need the hypervisor setup to be able to check for the
+ # container; more generally, all dependencies
+ Requirement('lxd_hypervisor'), # not null
+ # The bridge is not strictly needed, but we currently have
+ # no automated way to determine whether we need it or not
+ Requirement('bridge'),
+ # A DNS server is required to provide internet connectivity to
+ # the containers
+ Requirement('dns_server'),
+ ])
+ profiles = Attribute(String, multiplicity = Multiplicity.OneToMany,
+ default = ['default'])
+ image = Attribute(String, description = 'image', default = None)
+ is_image = Attribute(Bool, defaut = False)
+ pid = Attribute(Integer, description = 'PID of the container')
+
+ #--------------------------------------------------------------------------
+ # Constructor / Accessors
+ #--------------------------------------------------------------------------
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._container = None
+
+ #--------------------------------------------------------------------------
+ # Resource lifecycle
+ #--------------------------------------------------------------------------
+
+ @inline_task
+ def __initialize__(self):
+ """
+ We need to intanciate VPPHost before container creation.
+ """
+ self.node_with_kernel = Reference(self, 'node')
+
+ # We automatically add the management/monitoring interface
+ self._host_interface = NetDevice(node = self,
+ owner = self,
+ monitored = False,
+ device_name = DEFAULT_LXC_NETDEVICE)
+ self._state.manager.commit_resource(self._host_interface)
+
+ for iface in self.interfaces:
+ if iface.get_type() == "dpdkdevice":
+ self.node.vpp_host.dpdk_devices.append(iface.pci_address)
+
+ if 'vpp' in self.profiles:
+ dummy = self.node.vpp_host.uio_devices
+
+ @task
+ def __get__(self):
+ client = self.node.lxd_hypervisor.client
+ try:
+ self._container = client.containers.get(self.name)
+ except pylxd.exceptions.NotFound:
+ raise ResourceNotFound
+
+ def __create__(self):
+ """
+ Make sure vpp_host is instanciated before starting the container.
+ """
+ wait_vpp_host = wait_resource_task(self.node.vpp_host)
+ create = self._create_container()
+ start = self.__method_start__()
+ return wait_vpp_host > (create > start)
+
+ @task
+ def _create_container(self):
+ container = self._get_container_description()
+ log.debug('Container description: {}'.format(container))
+ client = self.node.lxd_hypervisor.client
+ self._container = client.containers.create(container, wait=True)
+ self._container.start(wait = True)
+
+ def _get_container_description(self):
+ # Base configuration
+ container = {
+ 'name' : self.container_name,
+ 'architecture' : self.architecture,
+ 'ephemeral' : self.ephemeral,
+ 'profiles' : ['default'],
+ 'config' : {},
+ 'devices' : {},
+ }
+
+ # DEVICES
+
+ devices = {}
+ # FIXME Container profile support is provided by setting changes into
+ # configuration (currently only vpp profile is supported)
+ for profile in self.profiles:
+ if profile == 'vpp':
+ # Set the new apparmor profile. This will be created in VPP
+ # application
+ # Mount hugetlbfs in the container.
+ container['config']['raw.lxc'] = APPARMOR_VPP_PROFILE
+ container['config']['security.privileged'] = 'true'
+
+ for device in self.node.vpp_host.uio_devices:
+ container['devices'][device] = {
+ 'path' : '/dev/{}'.format(device),
+ 'type' : 'unix-char' }
+
+ # NETWORK (not for images)
+
+ if not self.is_image:
+ container['config']['user.network_mode'] = 'link-local'
+ device = {
+ 'type' : 'nic',
+ 'name' : self.host_interface.device_name,
+ 'nictype' : 'bridged',
+ 'parent' : self.node.bridge.device_name,
+ }
+ device['hwaddr'] = AddressManager().get_mac(self)
+ prefix = 'veth-{}'.format(self.container_name)
+ device['host_name'] = AddressManager().get('device_name', self,
+ prefix = prefix, scope = prefix)
+
+ container['devices'][device['name']] = device
+
+
+ # SOURCE
+
+ image_names = [alias['name'] for alias in self.node.lxd_hypervisor.aliases]
+ image_exists = self.image is not None and self.image in image_names
+
+ if image_exists:
+ container['source'] = {
+ 'type' : 'image',
+ 'mode' : 'local',
+ 'alias' : self.image,
+ }
+ else:
+ container['source'] = {
+ 'type' : 'image',
+ 'mode' : 'pull',
+ 'server' : DEFAULT_SOURCE_URL,
+ 'protocol' : DEFAULT_SOURCE_PROTOCOL,
+ 'alias' : self.dist,
+ }
+
+ log.info('Creating container: {}'.format(container))
+ return container
+
+ @task
+ def __delete__(self):
+ log.info("Delete container {}".format(self.container_name))
+ self.node.lxd_hypervisor.client.containers.remove(self.name)
+
+ #--------------------------------------------------------------------------
+ # Attributes
+ #--------------------------------------------------------------------------
+
+ def _get_pid(self):
+ """
+ Attribute: pid (getter)
+ """
+ return BashTask(self.node, CMD_GET_PID, {'container': self},
+ parse = lambda rv: {'pid': rv.stdout.strip()})
+
+ #--------------------------------------------------------------------------
+ # Methods
+ #--------------------------------------------------------------------------
+
+ @task
+ def __method_start__(self):
+ """
+ Method: Start the container
+ """
+ self._container.start(wait = True)
+
+ @task
+ def __method_stop__(self):
+ """
+ Method: Stop the container
+ """
+ self._container.stop(wait = True)
+
+ @task
+ def __method_to_image__(self):
+ """
+ Returns:
+ Image metadata as returned by LXD REST API.
+ """
+ publish_description = {
+ "public": True,
+ "properties": {
+ "os": "Ubuntu",
+ "architecture": "x86_64",
+ "description": "Image generated from container {}".format(
+ self.container_name),
+ },
+ "source": {
+ "type": "container", # One of "container" or "snapshot"
+ "name": 'image-{}'.format(self.container_name),
+ }
+ }
+ return self.node.lxd_hypervisor.publish_image(publish_description)
+
+ #--------------------------------------------------------------------------
+ # Node API
+ #--------------------------------------------------------------------------
+
+ def execute(self, command, output = False, as_root = False):
+ """
+ Executes a command on the node
+
+ Params:
+ output (bool) : Flag determining whether the method should return
+ the output value.
+ as_root (bool) : Flag telling whether the command should be
+ executed as root.
+
+ Returns:
+ ReturnValue containing exit code, and eventually stdout and stderr.
+
+ Raises
+ Exception in case of error
+
+ The node exposes an interface allowing command execution through LXD.
+ We don't currently use an eventually available SSH connection.
+ """
+
+ ret = self._container.execute(shlex.split(command))
+
+ # NOTE: pylxd documents the return value as a tuple, while it is in
+ # fact a ContainerExecuteResult object
+ if not hasattr(ret, "exit_code"):
+ log.error("LXD return value does not have an exit code. "
+ "Try installing pylxd>=2.2.2 with pip3")
+ import sys; sys.exit(1)
+
+ args = (ret.exit_code,)
+ if output:
+ args += (ret.stdout, ret.stderr)
+ return ReturnValue(*args)