def run_playbook(playbook_location, verbose, vault_pass=None): """ Runs a playbook in a change root. :param playbook_location: The location of the playbook file. :param verbose: True, if a more verbose output should be used. :param vault_pass: If the playbook references an encrypted Ansible vault, this is the password to decrypt it. """ # Some distributions (e.g. Arch) have fewer bin paths in the PATH variable. # This causes plain ubuntu/debian-chroots to fail. Since this is what # Ansible does, so we extend the path accordingly. env = os.environ.copy() env["PATH"] += ':/usr/sbin:/sbin:/bin' inventory_file_path = os.path.join(playbook_location, 'hosts') playbook_file_path = os.path.join(playbook_location, 'main.yml') vars_reference = '@' + os.path.join(playbook_location, 'slingring_vars.yml') user_vars_reference = '@' + os.path.join(playbook_location, 'slingring_user_vars.yml') secret_user_vars_reference = '@' + os.path.join(playbook_location, 'slingring_user_secrets.yml') cmd = ['sudo', 'ansible-playbook', '--inventory', inventory_file_path, playbook_file_path, '--extra-vars', vars_reference, '--extra-vars', user_vars_reference] with tempfile.TemporaryDirectory() as tempdir: if vault_pass: cmd.append('--extra-vars') cmd.append(secret_user_vars_reference) cmd.append('--vault-password-file') random_string = key.create_random_password(8) pipe_path = os.path.join(tempdir, random_string) cmd.append(pipe_path) PipeThread(pipe_path, vault_pass).start() run_command(cmd, 'ansible_phase', verbose, env)
def execute_in_schroot(name, command, sudo, phase, verbose, env=None): """ Executes the given command string in the given schroot. :param name: The schroot name. :param command: The command as a single string. :param sudo: True, if the command shall be executed as root. :param phase: a message key which describes the current phase. This is used if something fails. :param verbose: True, if a more verbose output is desired. :param env: A dictionary containing environment variables which should be present within the schroot for the time of execution (e.g. { 'VARIABLE': 'content' }) """ effective_command = [] if sudo: effective_command.append('sudo') effective_command.append('schroot') effective_command.append('-c') effective_command.append(name) effective_command.append('--directory') effective_command.append('/') effective_command.append('--') effective_command.append('/bin/bash') effective_command.append('-c') environment_exports = str() if env: for variable_name, variable_value in env.items(): environment_exports += "export {}={};".format( variable_name, variable_value) effective_command.append(environment_exports + command) run_command(effective_command, phase, verbose)
def _write_config_file_as_root(config_parser, path, phase, verbose): with tempfile.TemporaryDirectory() as tempdir: file_name = os.path.join(tempdir, 'temp.conf') with open(file_name, 'w') as temp_file: config_parser.write(temp_file, space_around_delimiters=False) command = ['sudo', 'cp', file_name, path] run_command(command, phase, verbose)
def _copy_initializers_to_chroot(source_directory, target_directory, verbose): copied_initializer_scripts = [] for file in os.listdir(source_directory): file_path = os.path.join(source_directory, file) run_command(['sudo', 'cp', file_path, target_directory], 'copy-initializers-phase', verbose) copied_initializer_scripts.append(file) return sorted(copied_initializer_scripts)
def umount(mount_point, phase_key, verbose): """ Unmounts a mount point. :param mount_point: The mount point (e.g. /mnt/disk1) :param phase_key: a message key which describes the current phase. This is used if something fails. :param verbose: True, if a more verbose output is desired. """ umount_cmd = ['sudo', 'umount', mount_point] run_command(umount_cmd, phase_key, verbose)
def contains_active_mount_point(mount_point): """ Checks if the given mount point contains an active mount. E.g. if /mnt/ is given and something is mounted in /mnt/foo/bar, this will return True :param mount_point: The directory to check. :return: True, if something is mounted beneath the given mount_point. """ output = run_command('mount', 'mount-check', False).stdout.decode('utf-8') return mount_point in output
def run_ansible(universe_name, chroot_path, user_vars, user_secrets, slingring_vars, verbose): """ Runs the universe playbook on the given chroot. :param universe_name: The universe name. :param chroot_path: The path to the chroot. :param user_vars: A dictionary containing user vars (key: name, value: value) :param user_secrets: A dictionary containing user secrets (key: name, value: value) :param slingring_vars: A dictionary containing slingring vars (key: name, value: value) :param verbose: True, if a more verbose output is desired. """ print(_('config-files')) with open(playbook_hosts_file_path(universe_name), 'w') as hosts_file: hosts_file.write('"{}" {}'.format(chroot_path, " ansible_connection=chroot")) # create ansible variable files playbook_directory = playbook_directory_path(universe_name) run_command(['mkdir', '-p', playbook_directory], 'create-cache-dir-phase', verbose) ansible.write_vars_file(playbook_user_vars_path(universe_name), user_vars, 'user_vars') ansible.write_vars_file(playbook_slingring_vars_path(universe_name), slingring_vars, 'slingring') if user_secrets: password = key.create_random_password(20) ansible.write_vars_vault_file( playbook_user_secrets_path(universe_name), user_secrets, password, 'user_secrets', 'write-user-secret-phase', verbose) else: password = None # mount chroot print(_('mount-chroot')) try: mount(chroot_path, verbose) # run ansible print(_('ansible')) ansible.run_playbook(playbook_directory, verbose, password) print(_('umount-chroot')) finally: umount(chroot_path, verbose)
def change_schroot_name(current_name, new_name, current_path, new_path, phase, verbose): """ Changes the name of the schroot config to the given value. Also renames the config file. :param current_name: The current schroot name. :param new_name: The new schroot name. :param current_path: The current path to the schroot configuration. :param new_path: The new path to the schroot configuration. :param phase: a message key which describes the current phase. This is used if something fails. :param verbose: True, if a more verbose output is desired. """ config = configparser.ConfigParser() config.read(current_path) config.add_section(new_name) config[new_name] = config[current_name] config[new_name]['description'] = new_name + ' Slingring Universe' config.remove_section(current_name) run_command(['sudo', 'rm', current_path], phase, verbose) _write_config_file_as_root(config, new_path, phase, verbose)
def mount(source, mount_point, phase_key, verbose, bind=False, fstype=None): """ Mounts a given source onto an mount point. :param source: The source (e.g. /dev/sda1) :param mount_point: The mount point (e.g. /mnt/disk1) :param phase_key: a message key which describes the current phase. This is used if something fails. :param verbose: True, if a more verbose output is desired. :param bind: If bind option shall be used. :param fstype: The filesystem type. """ mount_cmd = ['sudo', 'mount'] if bind: mount_cmd.append('-o') mount_cmd.append('bind') if fstype: mount_cmd.append('-t') mount_cmd.append(fstype) mount_cmd.append(source) mount_cmd.append(mount_point) run_command(mount_cmd, phase_key, verbose)
def bootstrap(chroot_path, seed_dictionary, verbose, temp_dir=None): """ Bootstraps a chroot into the given directory. If the parent base directory does not exist, it will be created. :param chroot_path: The path of the created chroot. :param seed_dictionary: The universe description as a dictionary. :param verbose: True, if a more verbose output is desired. :param temp_dir: """ run_command(['sudo', 'mkdir', '-p', chroot_path], 'create-base-directory-phase', verbose) image_variant = seed_dictionary[ 'variant'] if 'variant' in seed_dictionary else None debootstrap_dir = TemporaryDirectory(dir=temp_dir) with debootstrap_dir as debootstrap_dir_path: base_image_path = os.path.join(debootstrap_dir_path, 'base_image') mnt.mount('none', debootstrap_dir_path, 'tmpfs-mount-phase', verbose, fstype='tmpfs') # create a dedicated directory inside the tmpfs, so we can mv it later run_command(['sudo', 'mkdir', base_image_path], 'create-base-temp-directory-phase', verbose) debootstrap.debootstrap(base_image_path, seed_dictionary['arch'], image_variant, seed_dictionary['suite'], seed_dictionary['mirror'], 'debootstrap-phase', verbose) run_command(['sudo', 'mv', base_image_path, '-T', chroot_path], 'mv-debootstrap-dir-phase', verbose) mnt.umount(debootstrap_dir_path, 'tmpfs-umount-phase', verbose)
def debootstrap(path, architecture, variant, suite, mirror, phase, verbose): """ Creates a Debian-based chroot in the given location. Find more details about the possible parameters in man debootstrap(8) :param path: The target path (e.g. /seu/chrootname). Will be created if it does not exist. :param architecture: The architecture (e.g. amd64, i386) :param variant: the bootstrap variant (e.g. minbase) :param suite: the suite name (e.g. xenial for ubuntu 16.04 lts) :param mirror: the image mirror (e.g. http://de.archive.ubuntu.com/ubuntu) :param phase: a message key which describes the current phase. This is used if something fails. :param verbose: True, if a more verbose output is desired. """ if variant is not None: variant_cmd = '--variant=' + variant else: variant_cmd = '' arch_cmd = '--arch=' + architecture cmd = ['sudo', 'debootstrap', variant_cmd, arch_cmd, suite, path, mirror] run_command(cmd, phase, verbose)
def remove_universe(universe_name, verbose): """ Removes a universe from the local machine. :param universe_name: The name of the universe which should be removed. :param verbose: True, for more verbose output. """ print(_('remove-intro').format(universe_name)) installation_configuration_path = installation_file_path(universe_name) if not os.path.exists(installation_configuration_path): print(_('universe-does-not-exist')) exit(1) installation_path = configuration.read_configuration( installation_configuration_path)['location'] schroot_path = schroot_config_file_path(universe_name) if mount.contains_active_mount_point(session_mount_point(universe_name)) \ or mount.contains_active_mount_point(installation_path): print(_('still-mounted-error')) exit(1) if os.path.exists(installation_path): run_command(['sudo', 'rm', '-rf', installation_path], 'deletion-phase', verbose) print(_('installation-path-removed')) else: print(_('installation-path-does-not-exist')) if os.path.exists(schroot_path): run_command(['sudo', 'rm', schroot_path], 'deletion-phase', verbose) print(_('schroot-config-removed')) else: print(_('schroot-config-does-not-exist')) local_universe_path = local_universe_dir(universe_name) if os.path.exists(local_universe_path): run_command(['rm', '-rf', local_universe_path], 'deletion-phase', verbose) print(_('local-cache-removed')) else: print(_('local-cache-does-not-exist'))
def upgrade_universe(seed_path, universe_name, verbose): """ Replaces the local seed of an existing universe with a newer version and runs the Ansible playbook on the given universe's chroot. :param seed_path: The path to the universe seed directory as string. :param universe_name: The name of the universe :param verbose: True, for more verbose output. """ old_universe_name = universe_name local_installation_path = local_universe_dir(old_universe_name) if not os.path.exists(local_installation_path): print(_('universe-does-not-exist')) exit(1) # read current information old_installation_configuration_path = installation_file_path( old_universe_name) universe_path = configuration.read_configuration( old_installation_configuration_path)['location'] seed_universe_file_path = universe_file_path(old_universe_name) old_seed_dictionary = configuration.read_seed_file(seed_universe_file_path) # read new information source_seed_directory = get_seed_directory_from_argument(seed_path) source_seed_universe_path = source_universe_file_path( source_seed_directory) new_seed_dictionary = configuration.read_seed_file( source_seed_universe_path) new_universe_name = new_seed_dictionary['name'] if not yes_no_prompt( _('upgrade-warning').format(new_seed_dictionary['version'], old_seed_dictionary['version'])): exit(1) else: print() # validate _validate_seed_dictionaries(old_seed_dictionary, new_seed_dictionary) if old_universe_name != new_universe_name: _validate_paths_for_collision(new_universe_name) # replace old seed with new seed run_command(['rm', '-rf', local_installation_path], 'remove-local-seed-phase', verbose) copy_seed_to_local_home(source_seed_directory, new_universe_name) new_installation_configuration_path = installation_file_path( new_universe_name) configuration.write_installation_configuration( new_installation_configuration_path, universe_path) # take care of the schroot config in case of a universe rename if old_universe_name != new_universe_name: change_schroot_name(old_universe_name, new_universe_name, schroot_config_file_path(old_universe_name), schroot_config_file_path(new_universe_name), 'rename-schroot-phase', verbose) # run the new Ansible playbook on the universe as if we just updated update_universe(new_universe_name, verbose)