Source code for cohydra.node.lxd

"""LXD containers in the simulation."""

import logging

from nsenter import Namespace
import pylxd

from ..context import defer
from ..command_executor import LXDCommandExecutor
from .base import Node

logger = logging.getLogger(__name__)

[docs]def log_to_file(container, log_path, stdout=False, stderr=False): """Log the container's output. This opens a stream to the docker container's log output and writes it into a file. Parameters --------- log_path : str The file path to the log file. stdout : bool Whether stdout should be logged. stderr : bool Whether stderr should be logged. """ log = logging.getLogger(container.name) log.debug('Write log to %s', log_path) with open(log_path, 'wb', 0) as log_file: for line in container.logs(stdout=stdout, stderr=stderr, follow=True, stream=True): log.log(logging.INFO if stdout else logging.ERROR, '%s', line.decode().strip()) log_file.write(line) log.debug('Done logging')
[docs]class LXDNode(Node): """A LXDNode represents a LXD container. Parameters ---------- name : str The name of the node (and container). image : str The name of the image to use (=the **alias**). image_server : str The server to pull the image off if not found locally. custom_configuration : dict Additional configuration key-value-pairs to pass to LXD. """ def __init__(self, name, image=None, image_server='https://images.linuxcontainers.org', custom_configuration=None): super().__init__(name) #: The image's name being used. self.image = image #: The container instance. self.container = None #: Custom configuration values. self.custom_configuration = custom_configuration #: The executor for running commands in the container. #: This is useful for a scripted :class:`.Workflow`. self.command_executor = None #: The server to fetch the image from. #: Before fetching from the server, local images will be checked. self.image_server = image_server
[docs] def wants_ip_stack(self): return True
[docs] def prepare(self, simulation): """This runs a setup on network interfaces and starts the container.""" logger.info('Preparing node %s', self.name) self.create_container() self.start_container(simulation.log_directory, simulation.hosts) self.setup_host_interfaces()
[docs] def create_container(self): """Create the LXC container.""" logger.info('Creating LXC container for: %s', self.name) client = pylxd.Client() config = { 'name': self.name, 'source': { 'type': 'image', 'alias': self.image, } } if isinstance(self.custom_configuration, dict): config.update(self.custom_configuration) # Check whether image with alias exists locally. try: client.images.get_by_alias(self.image) except pylxd.exceptions.NotFound: # Not found, so use the server. logger.debug('Image "%s" not found locally, pulling from %s', self.image, self.image_server) config['source'].update({ 'protocol': 'simplestreams', 'server': self.image_server }) # Tag for removal with cleanup. self.container = client.containers.create(config, wait=True) self.container.config.update({ 'user.created-by': 'ns-3' }) self.container.save(wait=True)
[docs] def start_container(self, log_directory, hosts=None): """Start the LXC container. All docker containers are labeled with "ns-3" as the creator. Parameters ---------- log_directory : str The path to the directory to put log files in. hosts : dict A dictionary with hostnames as keys and IP addresses (a list) as value. """ logger.info('Starting LXC container: %s', self.name) defer(f'stop and delete LXD container {self.name}', self.delete_container) self.container.start(wait=True) # Add extra_hosts to hosts file. hosts_file_content = self.container.files.get('/etc/hosts').decode() extra_hosts = '\n'.join(f'{address}\t{name}' for name, addresses in hosts.items() for address in addresses) self.container.files.put('/etc/hosts', f'{hosts_file_content}\n{extra_hosts}\n'.encode()) self.command_executor = LXDCommandExecutor(self.name, self.container)
[docs] def delete_container(self): """Delete the container.""" if self.container is not None: logger.info('Stopping LXD container: %s', self.container.name) self.container.stop(timeout=-1, wait=True) self.container.delete(wait=True) self.container = None self.command_executor = None
[docs] def setup_host_interfaces(self): """Setup the interfaces (bridge, tap, VETH pair) on the host and connect them to the container.""" for name, interface in self.interfaces.items(): logger.debug('Setting up interface %s on %s.', name, self.name) interface.setup_bridge() interface.connect_tap_to_bridge() self.container.devices.update({ name: { 'name': name, 'type': 'nic', 'nictype': 'bridged', 'parent': interface.bridge_name, 'hwaddr': interface.mac_address, 'host_name': interface.veth_name, } }) self.container.save(wait=True) container_state = pylxd.Client().api.containers[self.name].state.get().json() pid = container_state['metadata']['pid'] # Get container's namespace and setup the interface in the container with Namespace(pid, 'net'): interface.setup_veth_container_end(name)