From 3e918c703c0bda1db2d91ee55d1c21a137402461 Mon Sep 17 00:00:00 2001 From: Fabio Erculiani Date: Sun, 8 Aug 2010 23:41:57 +0200 Subject: [PATCH] [molecule.specs.plugins] introduce iso_to_image plugin. This plugin allows to build filesystem images (user can define the filesystem type) off an ISO image. --- ...on-amd64-spinbase-amazon-ami-template.spec | 13 + ...ayon-x86-spinbase-amazon-ami-template.spec | 13 + .../spinbase-amazon-ami-image-template.common | 105 +++ molecule/specs/plugins/image_plugin.py | 627 ++++++++++++++++++ 4 files changed, 758 insertions(+) create mode 100644 examples/specs/sabayon/sabayon-amd64-spinbase-amazon-ami-template.spec create mode 100644 examples/specs/sabayon/sabayon-x86-spinbase-amazon-ami-template.spec create mode 100644 examples/specs/sabayon/spinbase-amazon-ami-image-template.common create mode 100644 molecule/specs/plugins/image_plugin.py diff --git a/examples/specs/sabayon/sabayon-amd64-spinbase-amazon-ami-template.spec b/examples/specs/sabayon/sabayon-amd64-spinbase-amazon-ami-template.spec new file mode 100644 index 0000000..4c1d462 --- /dev/null +++ b/examples/specs/sabayon/sabayon-amd64-spinbase-amazon-ami-template.spec @@ -0,0 +1,13 @@ +# Use abs path, otherwise daily builds automagic won't work +# For further documentation, see the file above: +%import /sabayon/molecules/spinbase-amazon-ami-image-template.common + +# pre chroot command, example, for 32bit chroots on 64bit system, you always +# have to append "linux32" this is useful for inner_chroot_script +# prechroot: + +# Path to source ISO file (MANDATORY) +source_iso: /sabayon/iso/Sabayon_Linux_SpinBase_DAILY_amd64.iso + +release_version: 5.3 +image_name: Sabayon_Linux_SpinBase_5.3_amd64_ami.img diff --git a/examples/specs/sabayon/sabayon-x86-spinbase-amazon-ami-template.spec b/examples/specs/sabayon/sabayon-x86-spinbase-amazon-ami-template.spec new file mode 100644 index 0000000..5ddc406 --- /dev/null +++ b/examples/specs/sabayon/sabayon-x86-spinbase-amazon-ami-template.spec @@ -0,0 +1,13 @@ +# Use abs path, otherwise daily builds automagic won't work +# For further documentation, see the file above: +%import /sabayon/molecules/spinbase-amazon-ami-image-template.common + +# pre chroot command, example, for 32bit chroots on 64bit system, you always +# have to append "linux32" this is useful for inner_chroot_script +prechroot: linux32 + +# Path to source ISO file (MANDATORY) +source_iso: /sabayon/iso/Sabayon_Linux_SpinBase_DAILY_x86.iso + +release_version: 5.3 +image_name: Sabayon_Linux_SpinBase_5.3_x86_ami.img diff --git a/examples/specs/sabayon/spinbase-amazon-ami-image-template.common b/examples/specs/sabayon/spinbase-amazon-ami-image-template.common new file mode 100644 index 0000000..66def01 --- /dev/null +++ b/examples/specs/sabayon/spinbase-amazon-ami-image-template.common @@ -0,0 +1,105 @@ +# Define an alternative execution strategy, in this case, the value must be +execution_strategy: iso_to_image + +# Error script command, executed when something went wrong and molecule has +# to terminate the execution +# error_script: /path/to/script/to/be/executed/outside/after + +# Outer chroot script command, to be executed outside destination chroot before +# before entering it (and before inner_chroot_script) +# outer_chroot_script: /path/to/script/to/be/executed/outside + +# Inner chroot script command, to be executed inside destination chroot before +# packing it +# inner_chroot_script: /sabayon/scripts/openvz_inner_chroot_script.sh + +# Inner chroot script command, to be executed inside destination chroot after +# packages installation and removal +inner_chroot_script_after: /sabayon/scripts/image_generic_inner_chroot_script_after.sh + +# Outer chroot script command, to be executed outside destination chroot before +# before entering it (and AFTER inner_chroot_script) +# outer_chroot_script_after: /path/to/script/to/be/executed/outside/after + +# Pre-image building script. Hook called before image file creation +# Variables exported: +# TMP_IMAGE_PATH = path pointing to the temporary destination image file +# LOOP_DEVICE = loop device (/dev/loopN) currently in use +# pre_image_script: /sabayon/scripts/pre_image_script.sh + +# Post-image building script. Hook called after image file creation and move +# into destination directory. +# Variables exported: +# IMAGE_PATH = path pointing to the destination image file +# IMAGE_CHECKSUM_PATH = path pointing to the destination image file checksum (md5) +# post_image_script: /sabayon/scripts/post_image_script.sh + +# Destination directory for the image path (MANDATORY) +destination_image_directory: /sabayon/images + +# Specify an alternative image file name (image file name will be automatically +# produced otherwise) +# image_name: + +# Specify the image file size in Megabytes. This is mandatory. +# To avoid runtime failure, make sure the image is large enough to fit your +# chroot data. +# Example: 5000 (means: ~5GB) +# Example: 15000 (means: ~15GB) +image_mb: 5000 + +# Either set this to "yes" or "no" if you want your image to be filled with +# random data instead of zeroes. urandom is used, fallback is: random. +# Default is: no +# image_randomize: + +# Specify an image filesystem formatter that takes a single argument , which is +# the image device (by design, a loop device is passed to this executable). +# Default is: mkfs.ext3 +# image_formatter: + +# Specify an image file mount command. Two arguments are passed: (1) a loop +# device (/dev/loopN), (2) a temporary destination directory. +# Default is: mount -o loop,rw +# image_mounter: + +# Specify am image file unmount command. One arguments is passed: mount point. +# Default is: umount +# image_umounter: + +# Alternative ISO file mount command (default is: mount -o loop -t iso9660) +# iso_mounter: + +# Alternative ISO umounter command (default is: umount) +# iso_umounter: + +# Alternative squashfs file mount command (default is: mount -o loop -t squashfs) +# squash_mounter: + +# Alternative ISO squashfs umount command (default is: umount) +# squash_umounter: + +# List of packages that would be removed from chrooted system (comma separated) +# packages_to_remove: + +# Custom shell call to packages removal (default is: equo remove) +# custom_packages_remove_cmd: + +# List of packages that would be added from chrooted system (comma separated) +# packages_to_add: + +# Custom shell call to packages add (default is: equo install) +# custom_packages_add_cmd: + +# Custom command for updating repositories (default is: equo update) +# repositories_update_cmd: + +# Determine whether repositories update should be run (if packages_to_add is set) +# (default is: no), values are: yes, no. +# execute_repositories_update: no + +# Directories to remove completely (comma separated) +# paths_to_remove: + +# Directories to empty (comma separated) +# paths_to_empty: diff --git a/molecule/specs/plugins/image_plugin.py b/molecule/specs/plugins/image_plugin.py new file mode 100644 index 0000000..8efdb2d --- /dev/null +++ b/molecule/specs/plugins/image_plugin.py @@ -0,0 +1,627 @@ +# -*- coding: utf-8 -*- +# Molecule Disc Image builder for Sabayon Linux +# Copyright (C) 2009 Fabio Erculiani +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +import array +import os +import shutil +import tempfile +import gc +import errno + +from molecule.i18n import _ +from molecule.output import blue, darkred +from molecule.specs.skel import GenericExecutionStep, GenericSpec +from molecule.specs.plugins.builtin_plugin import BuiltinHandlerMixin +from molecule.specs.plugins.remaster_plugin import IsoUnpackHandler as \ + RemasterIsoUnpackHandler, ChrootHandler as RemasterChrootHandler +import molecule.utils + + +class ImageHandler(GenericExecutionStep, BuiltinHandlerMixin): + + LOSETUP_EXEC = "/sbin/losetup" + MB_IN_BYTES = 1024000 + DEFAULT_IMAGE_FORMATTER = ["/sbin/mkfs.ext3"] + DEFAULT_IMAGE_MOUNTER = ["/bin/mount", "-o", "loop,rw"] + DEFAULT_IMAGE_UMOUNTER = ["/bin/umount"] + + def __init__(self, *args, **kwargs): + GenericExecutionStep.__init__(self, *args, **kwargs) + + # init variables + self.loop_device = None + self._tmp_loop_device_fd = None + self.tmp_loop_device_file = None + self.image_mb = 0 + self.randomize = False + self.image_mounted = False + self.tmp_image_mount = None + + def setup(self): + + self.image_mb = self.metadata['image_mb'] + if self.metadata.get('image_randomize') == "yes": + self.randomize = True + + sts, loop_device = molecule.utils.exec_cmd_get_status_output( + [ImageHandler.LOSETUP_EXEC, "-f"]) + if sts != 0: + # ouch + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("setup hook failed"), _("cannot setup loop device"), + ) + ) + return sts + try: + self._tmp_loop_device_fd, self.tmp_loop_device_file = \ + tempfile.mkstemp(prefix = "molecule", dir = "/var/tmp") + except (OSError, IOError,) as err: + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("setup hook failed"), _("cannot create temporary file"), + ) + ) + return 1 + + # bind loop device + args = [ImageHandler.LOSETUP_EXEC, loop_device, + self.tmp_loop_device_file] + rc = molecule.utils.exec_cmd(args) + if rc != 0: + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("setup hook failed"), _("cannot bind loop device"), + ) + ) + return 1 + self.loop_device = loop_device + + self.tmp_image_mount = molecule.utils.mkdtemp() + + # setup metadata for next phases + self.metadata['ImageHandler_loop_device_file'] = \ + self.tmp_loop_device_file + self.metadata['ImageHandler_tmp_image_mount'] = self.tmp_image_mount + self.metadata['ImageHandler_loop_device'] = self.loop_device + self.metadata['ImageHandler_kill_loop_device'] = self._kill_loop_device + + return 0 + + def pre_run(self): + self._output.output("[%s|%s] %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("executing pre_run"), + ) + ) + + # run pre image script + exec_script = self.metadata.get('pre_image_script') + if exec_script: + env = os.environ.copy() + env['TMP_IMAGE_PATH'] = self.tmp_loop_device_file + env['LOOP_DEVICE'] = self.loop_device + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("spawning"), exec_script, + ) + ) + rc = molecule.utils.exec_cmd(exec_script, env = env) + if rc != 0: + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("pre image hook failed"), rc, + ) + ) + return rc + + return 0 + + def _fill_image_file(self): + """ + Fill image file (using _tmp_loop_device_fd) with either zeroes or + random data of image_mb size. + + @raises IOError: if space is not enough + @raises OSError: well, sorry + """ + if self.randomize: + self._output.output("[%s|%s] %s => %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("generating random base image file"), + self.tmp_loop_device_file, + ) + ) + else: + self._output.output("[%s|%s] %s => %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("generating zeroed base image file"), + self.tmp_loop_device_file, + ) + ) + + image_mb = self.image_mb + loop_f = os.fdopen(self._tmp_loop_device_fd, "wb") + self._tmp_loop_device_fd = None + arr = None + mb_bytes = ImageHandler.MB_IN_BYTES + while image_mb > 0: + image_mb -= 1 + if self.randomize: + arr = array.array('c', molecule.utils.get_random_str(mb_bytes)) + else: + arr = array.array('c', chr(0)*mb_bytes) + arr.tofile(loop_f) + + del arr + gc.collect() + # file self._tmp_loop_device_fd is closed here. + # no more writes needed + loop_f.flush() + loop_f.close() + + # last but not least, tell the loop device that the file size changed + args = [ImageHandler.LOSETUP_EXEC, "-c", self.loop_device] + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("spawning"), args, + ) + ) + rc = molecule.utils.exec_cmd(args) + if rc != 0: + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("image file resize failed"), rc, + ) + ) + return rc + + def run(self): + self._output.output("[%s|%s] %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("run hook called"), + ) + ) + + # fill image file + try: + rc = self._fill_image_file() + if rc != 0: + return rc + except (IOError, OSError) as err: + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("image file generation failed"), err, + ) + ) + return 1 + + # format image file + image_formatter = self.metadata.get('image_formatter', + ImageHandler.DEFAULT_IMAGE_FORMATTER) + formatter_args = image_formatter + [self.loop_device] + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("spawning"), formatter_args, + ) + ) + rc = molecule.utils.exec_cmd(formatter_args) + if rc != 0: + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("image formatter hook failed"), rc, + ) + ) + return rc + + # mount image file + mounter = self.metadata.get('image_mounter', + ImageHandler.DEFAULT_IMAGE_MOUNTER) + mount_args = mounter + [self.loop_device, self.tmp_image_mount] + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("spawning"), mount_args, + ) + ) + rc = molecule.utils.exec_cmd(mount_args) + if rc != 0: + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("image mount failed"), rc, + ) + ) + return rc + self.image_mounted = True + + return 0 + + def post_run(self): + """ Nothing to do """ + return 0 + + def _kill_loop_device(self, preserve_loop_device_file = False): + + if self.image_mounted: + umounter = self.metadata.get('image_umounter', + ImageHandler.DEFAULT_IMAGE_UMOUNTER) + args = umounter + [self.tmp_image_mount] + rc = molecule.utils.exec_cmd(args) + if rc != 0: + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("unable to umount loop device"), self.loop_device, + ) + ) + else: + self.image_mounted = False + + if self.tmp_image_mount is not None: + try: + os.rmdir(self.tmp_image_mount) + except OSError: + pass + + # kill loop device + if self.loop_device is not None: + rc = molecule.utils.exec_cmd([ImageHandler.LOSETUP_EXEC, "-d", + self.loop_device]) + if rc != 0: + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("unable to kill loop device"), self.loop_device, + ) + ) + else: + self.loop_device = None + + if (self.tmp_loop_device_file is not None) and \ + (not preserve_loop_device_file): + try: + os.remove(self.tmp_loop_device_file) + self.tmp_loop_device_file = None + except OSError as err: + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("unable to remove temp. loop device file"), + err, + ) + ) + + def kill(self, success = True): + + # kill tmp files + if self._tmp_loop_device_fd is not None: + try: + os.close(self._tmp_loop_device_fd) + except IOError as err: + self._output.output("[%s|%s] %s: %s" % ( + blue("ImageHandler"), darkred(self.spec_name), + _("unable to close temp fd"), err, + ) + ) + self._tmp_loop_device_fd = None + + if not success: + self._kill_loop_device() + self._run_error_script(None, None, None) + + + return 0 + + +class IsoUnpackHandler(RemasterIsoUnpackHandler): + + def setup(self): + + unpack_prefix = molecule.utils.mkdtemp(suffix = "chroot") + + self.metadata['chroot_tmp_dir'] = unpack_prefix + self.metadata['chroot_unpack_path'] = \ + self.metadata['ImageHandler_tmp_image_mount'] + self.dest_root = os.path.join(unpack_prefix, "cdroot") + + # setup upcoming new chroot path + self.chroot_dir = self.metadata['chroot_unpack_path'] + # explicitly set this to None + self.metadata['cdroot_path'] = None + + return 0 + + def kill(self, success = True): + # ImageHandler sets this + if not success: + self.metadata['ImageHandler_kill_loop_device']() + RemasterIsoUnpackHandler.kill(self, success = success) + + # we don't need the whole dir + tmp_dir = self.metadata['chroot_tmp_dir'] + if os.path.isdir(tmp_dir): + try: + shutil.rmtree(tmp_dir, True) + except (shutil.Error, OSError,): + self._output.output("[%s|%s] %s: %s" % ( + blue("IsoUnpackHandler"), darkred(self.spec_name), + _("unable to remove temp. dir"), tmp_dir, + ) + ) + + return 0 + + +class ChrootHandler(RemasterChrootHandler): + + def setup(self): + # to make superclass working + self.source_dir = self.metadata['chroot_unpack_path'] + self.dest_dir = self.source_dir + return 0 + + def kill(self, success = True): + # ImageHandler sets this + if not success: + self.metadata['ImageHandler_kill_loop_device']() + RemasterChrootHandler.kill(self, success = success) + return 0 + + +class FinalImageHandler(GenericExecutionStep, BuiltinHandlerMixin): + + MD5_EXT = ".md5" + IMAGE_EXT = ".img" + + def __init__(self, *args, **kwargs): + GenericExecutionStep.__init__(self, *args, **kwargs) + + def setup(self): + # ImageHandler sets this + self.tmp_loop_device_file = \ + self.metadata['ImageHandler_loop_device_file'] + # umount all, but don't remove our loop device file + self.metadata['ImageHandler_kill_loop_device']( + preserve_loop_device_file = True) + + image_name = self.metadata.get('image_name', + os.path.basename(self.metadata['source_iso']) + \ + FinalImageHandler.IMAGE_EXT) + self.dest_path = os.path.join( + self.metadata['destination_image_directory'], image_name) + + dest_path_dir = os.path.dirname(self.dest_path) + if (not os.path.lexists(dest_path_dir)) and \ + (not os.path.isdir(dest_path_dir)): + os.makedirs(dest_path_dir, 0o755) + + return 0 + + def pre_run(self): + """ Nothing to do """ + return 0 + + def run(self): + self._output.output("[%s|%s] %s: %s %s" % ( + blue("FinalImageHandler"), darkred(self.spec_name), + _("run hook called"), _("moving image file to destination"), + self.dest_path, + ) + ) + try: + os.rename(self.tmp_loop_device_file, self.dest_path) + except OSError as err: + if err.errno != errno.EXDEV: + raise + # cannot move atomically, fallback to shutil.move + try: + shutil.move(self.tmp_loop_device_file, self.dest_path) + except shutil.Error: + raise + + self._output.output("[%s|%s] %s: %s" % ( + blue("FinalImageHandler"), darkred(self.spec_name), + _("built image"), self.dest_path, + ) + ) + if os.path.isfile(self.dest_path) and os.access(self.dest_path, os.R_OK): + self._output.output("[%s|%s] %s: %s" % ( + blue("FinalImageHandler"), darkred(self.spec_name), + _("generating md5 for"), self.dest_path, + ) + ) + digest = molecule.utils.md5sum(self.dest_path) + md5file = self.dest_path + FinalImageHandler.MD5_EXT + with open(md5file, "w") as f: + f.write("%s %s\n" % (digest, + os.path.basename(self.dest_path),)) + f.flush() + + def post_run(self): + # run post tar script + exec_script = self.metadata.get('post_image_script') + if exec_script: + env = os.environ.copy() + env['IMAGE_PATH'] = self.dest_path + env['IMAGE_CHECKSUM_PATH'] = self.dest_path + \ + FinalImageHandler.MD5_EXT + self._output.output("[%s|%s] %s: %s" % ( + blue("FinalImageHandler"), darkred(self.spec_name), + _("spawning"), exec_script, + ) + ) + rc = molecule.utils.exec_cmd(exec_script, env = env) + if rc != 0: + self._output.output("[%s|%s] %s: %s" % ( + blue("FinalImageHandler"), darkred(self.spec_name), + _("post image hook failed"), rc, + ) + ) + return rc + return 0 + + def kill(self, success = True): + """ Nothing to do """ + return 0 + +class IsoToImageSpec(GenericSpec): + + PLUGIN_API_VERSION = 0 + + @staticmethod + def execution_strategy(): + return "iso_to_image" + + def vital_parameters(self): + return [ + "source_iso", + "destination_image_directory", + "image_mb", + ] + + def parser_data_path(self): + return { + 'prechroot': { + 'cb': self.valid_exec_first_list_item, + 've': self.ve_string_splitter, + }, + 'release_string': { + 'cb': self.ne_string, # validation callback + 've': self.ve_string_stripper, # value extractor + }, + 'release_version': { + 'cb': self.ne_string, + 've': self.ve_string_stripper, + }, + 'release_desc': { + 'cb': self.ne_string, + 've': self.ve_string_stripper, + }, + 'release_file': { + 'cb': self.ne_string, + 've': self.ve_string_stripper, + }, + 'source_iso': { + 'cb': self.valid_path_string, + 've': self.ve_string_stripper, + }, + 'image_name': { + 'cb': self.ne_string, + 've': self.ve_string_stripper, + }, + 'image_formatter': { + 'cb': self.ne_list, + 've': self.ve_string_splitter, + }, + 'image_mounter': { + 'cb': self.ne_list, + 've': self.ve_string_splitter, + }, + 'image_umounter': { + 'cb': self.ne_list, + 've': self.ve_string_splitter, + }, + 'image_mb': { + 'cb': self.valid_integer, + 've': self.ve_integer_converter, + }, + 'image_randomize': { + 'cb': self.valid_ascii, + 've': self.ve_string_stripper, + }, + 'error_script': { + 'cb': self.valid_exec_first_list_item, + 've': self.ve_string_splitter, + }, + 'outer_chroot_script': { + 'cb': self.valid_exec_first_list_item, + 've': self.ve_string_splitter, + }, + 'inner_chroot_script': { + 'cb': self.valid_path_string_first_list_item, + 've': self.ve_string_splitter, + }, + 'inner_chroot_script_after': { + 'cb': self.valid_path_string_first_list_item, + 've': self.ve_string_splitter, + }, + 'outer_chroot_script_after': { + 'cb': self.valid_exec_first_list_item, + 've': self.ve_string_splitter, + }, + 'destination_image_directory': { + 'cb': self.valid_dir, + 've': self.ve_string_stripper, + }, + 'pre_image_script': { + 'cb': self.valid_exec_first_list_item, + 've': self.ve_string_splitter, + }, + 'post_image_script': { + 'cb': self.valid_exec_first_list_item, + 've': self.ve_string_splitter, + }, + 'iso_mounter': { + 'cb': self.ne_list, + 've': self.ve_string_splitter, + }, + 'iso_umounter': { + 'cb': self.ne_list, + 've': self.ve_string_splitter, + }, + 'squash_mounter': { + 'cb': self.ne_list, + 've': self.ve_string_splitter, + }, + 'squash_umounter': { + 'cb': self.ne_list, + 've': self.ve_string_splitter, + }, + 'custom_packages_remove_cmd': { + 'cb': self.valid_ascii, + 've': self.ve_string_stripper, + }, + 'custom_packages_add_cmd': { + 'cb': self.valid_ascii, + 've': self.ve_string_stripper, + }, + 'packages_to_remove': { + 'cb': self.ne_list, + 've': self.valid_comma_sep_list, + }, + 'packages_to_add': { + 'cb': self.ne_list, + 've': self.valid_comma_sep_list, + }, + 'repositories_update_cmd': { + 'cb': self.ne_list, + 've': self.ve_string_splitter, + }, + 'execute_repositories_update': { + 'cb': self.valid_ascii, + 've': self.ve_string_stripper, + }, + 'paths_to_remove': { + 'cb': self.ne_list, + 've': self.valid_path_list, + }, + 'paths_to_empty': { + 'cb': self.ne_list, + 've': self.valid_path_list, + }, + } + + def get_execution_steps(self): + return [ImageHandler, IsoUnpackHandler, ChrootHandler, + FinalImageHandler]