def create_lxc_storage_pool(name=settings.LXC_STORAGE_POOL_NAME, driver=settings.LXC_STORAGE_POOL_DRIVER): """ Creates a new LXC storage pool with the passed driver and name :param str name: The name of the storage pool to create (default from settings) :param str driver: The driver to use (default from settings) """ # Sanity check, we don't want to create duplicate pools if check_if_lxc_storage_pool_exists(name): raise RuntimeError( "LXC storage pool {} already exists, cannot create duplicate". format(name)) # Make it logger.info("Creating LXC storage pool {} with driver {}".format( name, driver)) client = get_lxd_client() try: client.storage_pools.create({ "name": name, "driver": driver, "config": { "size": settings.LXC_STORAGE_POOL_SIZE } }) logger.info( "Storage pool {} with driver {} successfully created".format( name, driver)) except LXDAPIException as e: logger.critical( "Got API error while creating storage pool {}. Error: {}".format( name, e)) raise RuntimeError( "Received API error while creating storage pool {}".format(name))
def check_if_lxc_machine_exists(machine): """ Checks if an LXC machine exists :param str machine: The machine/container to check for :return: bool: True if it exists, false otherwise """ return get_lxd_client().containers.exists(machine)
def check_if_lxc_storage_pool_exists(name=settings.LXC_STORAGE_POOL_NAME): """ Check for the existence of the LXC storage pool defined in the settings :param str name: The name of the storage pool to check for (default from the settings) :return: bool: True if the pool exists, False otherwise """ logger.debug( "Checking for the existence of LXC {} storage pool".format(name)) return get_lxd_client().storage_pools.exists( settings.LXC_STORAGE_POOL_NAME)
def write_file_to_lxc_container(container, file_path, data): """ Writes file data to a path on the LXC container :param container: str: The name of the container :param file_path: str: The guest path to write the file to :param data: The data to write """ try: machine = get_lxd_client().containers.get(container) machine.files.put(file_path, data) except NotFound: logger.error("Tried to write data to path {} on LXC container {}, but the container does not exist".format(file_path, container))
def get_lxc_machine_status(name): """ Gets the LXC machine state and returns a list :param name: str: The name of the machine :return: list: [name, state, provider] """ # TODO: Let's not return a list here, simply the status client = get_lxd_client() try: status = client.containers.get(name).status except NotFound: status = "NA" return [name, status, "LXC"]
def create_lxc_base_image_container(config): """ Creates the LXC base image container :param dict config: The config generated by get_config() :raises RuntimeError: if any issues are encountered during creation """ # Check if the base image machine already exists and destroy it if check_if_lxc_machine_exists(settings.LXC_BASE_IMAGE_MACHINE_NAME): logger.warning("LXC base image machine already exists") request_confirmation( message="Recreating it will destroy any local changes", prompt="Recreate the LXC base image machine? ") destroy_lxc_machine(settings.LXC_BASE_IMAGE_MACHINE_NAME, wait=True) # Now create the base image machine client = get_lxd_client() machine_config = { "name": settings.LXC_BASE_IMAGE_MACHINE_NAME, "architecture": "x86_64", "profiles": [settings.LXC_VNET_PROFILE], "ephemeral": False, "config": {}, "devices": { "eth0": { "name": "eth0", "parent": "lxdbr0", "type": "nic", "nictype": "bridged", "host_name": "{}-eth0".format(settings.LXC_BASE_IMAGE_MACHINE_NAME), } }, "source": { "type": "image", "protocol": str(config["providers"]["lxc"]["base_image"]["protocol"]), "server": config["providers"]["lxc"]["base_image"]["server"], "alias": str(config["providers"]["lxc"]["base_image"]["os"]), }, } logger.info("Creating LXC base image container") client.containers.create(machine_config, wait=True)
def check_if_lxc_image_exists(image, by_alias=True): """ Check if an LXC image exists :param str image: The image fingerprint or alias :param bool by_alias: Search by image alias instead of fingerprint :return: bool: True if the image exists false otherwise """ logger.debug("Checking for the existence of LXC image {}".format(image)) client = get_lxd_client() if by_alias: try: client.images.get_by_alias(image) return True except NotFound: return False # Search by fingerprint return client.images.exists(image)
def destroy_lxc_image(image, by_alias=True): """ Destroy a LXC image :param str image: The fingerprint or alias of the image to destroy :param bool by_alias: Search by alias instead of fingerprint """ # Check if it even exists if not check_if_lxc_image_exists(image, by_alias=by_alias): logger.warning( "Tried to destroy LXC image {}, but it is already gone".format( image)) return # Delete it logger.info("Deleting LXC image {}".format(image)) client = get_lxd_client() image = client.images.get_by_alias( image) if by_alias else client.images.get(image) image.delete()
def create_lxc_image_from_container(container, alias=None, description=None): """ Create a LXC image from a container :param str container: The container to create the image from :param str alias: Creates an alias for the image :param str description: A description for the image alias """ # Stop it first change_lxc_machine_status(container, status="stop") # Create the image logger.info("Creating image from LXC container {}".format(container)) client = get_lxd_client() container = client.containers.get(container) img = container.publish(wait=True) logger.info("Image {} created successfully".format(img.fingerprint)) # Create the alias if requested if alias: logger.info("Adding alias {} to newly created image".format(alias)) img.add_alias(alias, description)
def destroy_lxc_machine(machine, wait=False): """ Deletes an LXC machine :param str machine: The name of the machine to delete :param bool wait: Wait for the deletion to be complete before returning :return: """ client = get_lxd_client() try: container = client.containers.get(machine) except NotFound: logger.warning( "Tried to delete LXC machine {}, but it does not exist. Maybe it was already deleted?" .format(machine)) return # Check if the container is still running if container.status.lower() == "running": logger.info("Stopping LXC container {}".format(machine)) container.stop(wait=True) logger.info("Deleting LXC container {}".format(machine)) container.delete(wait=wait)
def change_lxc_machine_status(machine, status="stop"): """ Start a LXC machine :param str machine: The name of the machine to change the status of :param str status: The status to change the LXC machine to """ client = get_lxd_client() try: machine = client.containers.get(machine) except NotFound: logger.error( "Tried to change machine status of LXC container {}, but it doesn't exist!" .format(machine)) return # Change the status if status == "stop": machine.stop() elif status == "start": try: # On start we wait, as we might catch invalid configs machine.start(wait=True) except LXDAPIException as e: logger.error( "Unable to start LXC container {}, got error: {}".format( machine.name, e)) return # Take a short nap after issuing the start/stop command, so we might pass the first status check sleep(1) try: required_state = "Stopped" if status == "stop" else "Running" wait_for_lxc_machine_status(machine, required_state) logger.debug("LXC container {} is {}".format(machine.name, machine.state().status)) except TimeoutError: logger.error( "Unable to change LXC status container {}, got timeout after issuing {} command" .format(machine.name, status))
def delete_lxc_storage_pool(name): """ Deletes a LXC storage pool Pool must be empty before deletion :param str name: The name of the pool to delete """ # Check if the pool even exists if not check_if_lxc_storage_pool_exists(name): logger.warning( "Tried to delete LXC storage pool {}, but it didn't exist, skipping..." .format(name)) return client = get_lxd_client() # Try to delete it try: logger.info("Deleting LXC storage pool {}".format(name)) client.storage_pools.get(name).delete() except LXDAPIException as e: logger.critical( "Got API error while deleting storage pool {}. Error: {}".format( name, e)) raise RuntimeError( "Received API error while deleting storage pool {}".format(name))
def create_lxc_machines_from_base_image(config, containers): """ Create LXC machines from the base image specified in the settings :param dict config: The config generated by get_config() :param list containers: A list of machines to create """ containers_to_create = [] containers_already_created = False # Get all the LXC machines to create for container in containers: # Check if the requested machine name is present in the config if container not in config["machines"]: logger.error( "Tried to get provider for container {}, but the container was not found in the config, skipping" .format(container)) # Quick check if the machine already exists elif check_if_lxc_machine_exists(container): logger.error( "A LXC container with the name {} already exists, skipping". format(container)) containers_already_created = True # Check if LXC is the provider elif settings.MACHINE_TYPE_PROVIDER_MAPPING[ config["machines"][container]["type"]].lower() == "lxc": logger.debug( "Selecting LXC machine {} for creation".format(container)) containers_to_create.append(container) else: logger.debug( "Machine {} is not provided by LXC, skipping LXC container creation" .format(container)) # Create it client = get_lxd_client() for container in containers_to_create: logger.debug( "Generating LXC config for container {}".format(container)) # Interface config # First add eth0 (default), which does nothing device_config = {"eth0": {"type": "none"}} # Then for each interface in the config add the configuration for that interface to the interfaces_config dict for inet_name, inet_config in config["machines"][container][ "interfaces"].items(): device_config[inet_name] = { "name": inet_name, # The name of the interface inside the instance "host_name": "{}-{}".format( container, inet_name), # The name of the interface inside the host "parent": "{}{}".format( settings.VNET_BRIDGE_NAME, inet_config["bridge"]), # The name of the host device "type": "nic", "nictype": "bridged", "hwaddr": inet_config["mac"], } container_config = { "name": container, "source": { "alias": settings.LXC_BASE_IMAGE_ALIAS, "type": "image" }, "ephemeral": False, "config": { "user.network-config": "disabled" }, "devices": device_config, "profiles": [settings.LXC_VNET_PROFILE], } logger.info("Creating LXC container {}".format(container)) # TODO: Make this nicer by not waiting here but doing the configuration after we've created all containers client.containers.create(container_config, wait=True) place_lxc_interface_configuration_on_container(config, container) # Check with the user if it is okay to overwrite config files if containers_already_created: request_confirmation( message= "Some containers already existed, the next operation will overwrite network, " "host and user config files on those containers", )
def configure_lxc_base_machine(config): """ Configure the LXC base machine to get a fully functional VNet base machine which we can make an image from :param dict config: The config generated by get_config() :raises RuntimeError: If the base machine is started without networking/dns """ logger.info( "Configuring LXC base machine {}, this might take a while".format( settings.LXC_BASE_IMAGE_MACHINE_NAME)) client = get_lxd_client() machine = client.containers.get(settings.LXC_BASE_IMAGE_MACHINE_NAME) def execute_and_log(command, **kwargs): result = machine.execute(shlex.split(command), **kwargs) logger.debug(result) return result # Check for DNS logger.debug("Checking for DNS connectivity") dns = False for _ in range(0, settings.LXC_MAX_STATUS_WAIT_ATTEMPTS): if execute_and_log("host -t A google.com")[0] == 0: dns = True break # No DNS connectivity (yet), try again sleep(2) if not dns: # Shutdown base if DNS check fails logger.debug("Stopping base machine") machine.stop() raise RuntimeError( "Base machine started without working DNS, unable to continue") # Set the FRR routing source and key execute_and_log( "bash -c 'curl -s https://deb.frrouting.org/frr/keys.asc | apt-key add'" ) execute_and_log( "bash -c 'echo deb https://deb.frrouting.org/frr $(lsb_release -s -c) {} | tee -a /etc/apt/sources.list.d/frr.list'" .format(settings.FRR_RELEASE)) # Update and install packages execute_and_log("apt-get update") execute_and_log( "apt-get upgrade -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'", environment={"DEBIAN_FRONTEND": "noninteractive"}, ) execute_and_log( "apt-get install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' {}" .format(" ".join(config["providers"]["lxc"]["guest_packages"])), environment={"DEBIAN_FRONTEND": "noninteractive"}, ) # Disable radvd by default execute_and_log("systemctl disable radvd") # Disable cloud init messing with our networking execute_and_log( "bash -c 'echo network: {config: disabled} > /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg'" ) # Set the default VTYSH_PAGER execute_and_log("bash -c 'export VTYSH_PAGER=more >> ~/.bashrc'") # Make all files in the FRR dir owned by the frr user execute_and_log( "bash -c 'echo -e \"#!/bin/bash\nchown -R frr:frr /etc/frr\nsystemctl restart frr\" > /etc/rc.local; chmod +x /etc/rc.local'" ) # All done, stop the container machine.stop(wait=True) logger.debug("LXC base machine {} successfully configured".format( settings.LXC_BASE_IMAGE_MACHINE_NAME))