Source code for

from __future__ import absolute_import

import logging
import functools

from collections import namedtuple

import six

from lago import log_utils, utils

LOGGER = logging.getLogger(__name__)
LogTask = functools.partial(log_utils.LogTask, logger=LOGGER)
Command = namedtuple('Command', ['name', 'cmd'])

[docs]class Build(object): """ A Build object represents a build section in the init file. Each build section (which in turn belongs to a specific disk) should get his own Build object In order to add support for a new build command, a new function with the name of the command should be implemented in this class. this function should accept a list of options and arguments and return a named tuple 'Command', where '' is the name of the command and 'Command.cmd' is the a list containing the command and its args, for example: = 'virt-customize' Command.cmd = ['virt-customize', '-a', PATH_TO_DISK, SOME_CMDS...] Attributes: name (str): The name of the vm this builder belongs disk_path (str): The path to the disk that needs to be customized paths (lago.paths.Paths): The paths of the current prefix build_cmds (list of str): A list of commands that should be invoked on the disk located in disk_path """
[docs] @staticmethod def normalize_options(options): """ Turns a mapping of 'option: arg' to a list and prefix the options. arg can be a list of arguments. for example: dict = { o1: a1, o2: , o3: [a31, a32] o4: [] } will be transformed to: [ prefix_option(o1), a1, prefix_option(o2), prefix_option(o3), a31, prefix_option(o3), a32 prefix_option(o4) ] note that empty arguments are omitted Args: options (dict): A mapping between options and arguments Returns: lst: A normalized version of 'options' as mentioned above """ normalized_options = [] def _add(option, arg=None): normalized_options.append(option) arg and normalized_options.append(arg) for option, arg in six.iteritems(options): prefixed_option = Build.prefix_option(option) if isinstance(arg, list) and arg: for a in arg: _add(prefixed_option, a) else: _add(prefixed_option, arg) return normalized_options
[docs] @staticmethod def prefix_option(option): """ Depends on the option's length, prefix it with '-' or '--' Args: option (str): The option to prefix Returns: str: prefixed option """ if len(option) == 1: return '-{}'.format(option) else: return '--{}'.format(option)
[docs] @classmethod def get_instance_from_build_spec(cls, name, disk_path, build_spec, paths): """ Args: name (str): The name of the vm this builder belongs disk_path (str): The path to the disk that needs to be customized paths (lago.paths.Paths): The paths of the current prefix build_spec (dict): The build spec part, associated with the disk located at disk_path, from the init file. Returns: An instance of Build with a normalized build spec i.e ready to be invoked. """ instance = cls(name, disk_path, paths) instance.normalize_build_spec(build_spec) return instance
def __init__(self, name, disk_path, paths): = name self.disk_path = disk_path self.paths = paths self.build_cmds = []
[docs] def normalize_build_spec(self, build_spec): """ Convert a build spec into a list of Command tuples. After running this command, self.build_cmds should hold all the commands that should be run on the disk in self.disk_path. Args: build_spec (dict): The buildspec part from the init file """ for cmd in build_spec: if not cmd: continue cmd_name = list(cmd.keys())[0] cmd_options = list(cmd.values())[0] cmd_handler = self.get_cmd_handler(cmd_name) self.build_cmds.append(cmd_handler(cmd_options))
[docs] def get_cmd_handler(self, cmd): """ Return an handler for cmd. The handler and the command should have the same name. See class description for more info about handlers. Args: cmd (str): The name of the command Returns: callable: which handles cmd Raises: If an handler for cmd doesn't exist """ cmd = cmd.replace('-', '_') handler = getattr(self, cmd, None) if not handler: raise BuildException( 'Command {} is not supported as a ' 'build command'.format(cmd) ) return handler
[docs] def virt_customize(self, options): """ Handler for 'virt-customize' note: if 'ssh-inject' option was specified without a path to a key, the prefix' key will be copied to the vm. Args: options (lst of str): Options and arguments for 'virt-customize' Returns: callable: which handles cmd Raises: If an handler for cmd doesn't exist """ cmd = ['virt-customize', '-a', self.disk_path] if 'ssh-inject' in options and not options['ssh-inject']: options['ssh-inject'] = 'root:file:{}'.format( self.paths.ssh_id_rsa_pub() ) options = self.normalize_options(options) cmd.extend(options) return Command('virt-customize', cmd)
[docs] def build(self): """ Run all the commands in self.build_cmds Raises: If a command returned a non-zero code """ if not self.build_cmds: LOGGER.debug('No build commands were found, skipping build step') with LogTask('Building {} disk {}'.format(, self.disk_path)): for command in self.build_cmds: with LogTask('Running command {}'.format( LOGGER.debug(command.cmd) result = utils.run_command(command.cmd) if result: raise BuildException(result.err)
[docs]class BuildException(utils.LagoException): pass