#!/usr/bin/python # -*- coding: utf-8 -*- # (c) 2014, Kevin Carter # (c) 2014, Hugh Saunders # # This file is part of Ansible # # Ansible 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 3 of the License, or # (at your option) any later version. # # Ansible 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 Ansible. If not, see . import ast import os import shutil import tempfile import time DOCUMENTATION = """ --- module: lxc version_added: "1.6.2" short_description: Manage LXC Containers description: - Management of LXC containers options: name: description: - Name of a container. required: false return_code: description: - Allow for return Codes other than 0 when executing commands. This is a comma separated list of acceptable return codes. default: 0 backingstore: description: - Options 'dir', 'lvm', 'loop', 'btrfs', 'best'. The default is 'none'. required: false template: description: - Name of the template to use within an LXC create. required: false default: ubuntu template_options: description: - Template options when building the container. required: false config: description: - Path to the LXC configuration file. required: false default: /etc/lxc/default.conf bdev: description: - Backend device for use with an LXC container. required: false lvname: description: - Backend store for lvm. required: false vgname: description: - If Backend store is lvm, specify the name of the volume group. required: false thinpool: description: - Use LVM thin pool called TP (Default lxc). required: false fstype: description: - Create fstype TYPE (Default ext3). required: false fssize: description: - Filesystem Size (Default 1G, default unit 1M). required: false dir: description: - Place rootfs directory under DIR. required: false zfsroot: description: - Create zfs under given zfsroot (Default tank/lxc). required: false container_command: description: - Run a command in a container. Only used in the "attach" operation. required: false return_facts: description: - Return stdout after an attach command. Only used in the "attach" operation. required: false default: false lxcpath: description: - Place container under PATH required: false snapshot: description: - The new container's rootfs should be a LVM or btrfs snapshot of the original. Only used in "clone" operation. required: false keepname: description: - Do not change the hostname of the container (in the root filesystem). Only used in "clone" operation. required: false newpath: description: - The lxcpath for the new container. Only used in "clone" operation. required: false orig: description: - The name of the original container to clone. Only used in "clone" operation. required: false new: description: - The name of the new container to create. Only used in "clone" operation. required: false state: choices: - running - stopped description: - Start a container right after it's created. required: false default: 'running' options: description: - Dictionary of options to use in a containers configuration. Only used in "config" operation. When dropping additional configuration options the values are strings IE "key=value", see example section for more details. required: false command: choices: - list - create - destroy - info - attach - start - stop - restart - config - createtar - clone description: - Type of command to run, see Examples. required: true author: Kevin Carter, Hugh Saunders requirements: ['lxc >= 1.0'] """ EXAMPLES = """ # Create a new LXC container. - lxc: name=test-container template=ubuntu config=/etc/lxc/lxc-rpc.conf command=create state=running # Create tar archive from Container this is a bzip2 compressed archive - lxc: name="{{ container_name }}" command=createtar tarpath="/tmp/{{ container_name }}" # Run a command within a built and started container. - lxc: name=test-container container_command="git clone https://github.com/cloudnull/lxc_defiant" command=attach # List all containers and return a dict of all found information - lxc: command=list # Get information on a given container. - lxc: name=test-container command=info # Stop a container. - lxc: name=test-container command=stop # Start a container. - lxc: name=test-container command=start # Restart a container. - lxc: name=test-container command=restart # Update the configuration for a container. # Uses a list of "key=value" pairs. container_config_options: - 'cpuset.cpus="0,3"' - 'lxc.cgroup.devices.allow="a rmw"' - lxc: name=test-container command=config options="{{ container_config_options }}" # Clone a container. - lxc: orig=test-container new=test-container-new command=clone state=running # Destroy a container. - lxc: name=test-container command=destroy """ COMMAND_MAP = { 'list': { 'command': 'container_list', 'variables': [ 'lxcpath' ], }, 'create': { 'command': 'container_create', 'variables': [ 'name', 'config', 'template', 'bdev', 'template', 'lxcpath', 'lvname', 'vgname', 'thinpool', 'fstype', 'fssize', 'dir', 'zfsroot', 'template_options', 'state' ] }, 'destroy': { 'command': 'container_destroy', 'variables': [ 'name', 'lxcpath' ], }, 'clone': { 'command': 'container_clone', 'variables': [ 'keepname', 'snapshot', 'fssize', 'lxcpath', 'newpath', 'backingstore', 'orig', 'new', 'state' ] }, 'info': { 'command': 'container_info', 'variables': [ 'name', 'lxcpath' ], }, 'attach': { 'command': 'container_attach', 'variables': [ 'name', 'lxcpath', 'container_command', 'return_facts' ], }, 'start': { 'command': 'container_start', 'variables': [ 'name', 'lxcpath' ], }, 'stop': { 'command': 'container_stop', 'variables': [ 'name', 'lxcpath' ], }, 'restart': { 'command': 'container_restart', 'variables': [ 'name', 'lxcpath' ], }, 'config': { 'command': 'container_config', 'variables': [ 'name', 'lxcpath', 'options', 'state' ], }, 'createtar': { 'command': 'container_create_tar', 'variables': [ 'name', 'lxcpath', 'tarpath' ], } } # This is used to attach to a running container and execute commands from # within the container on the host. This will provide local access to a # container without using SSH. The template will attempt to work within the # home directory of the user that was attached to the conatiner and source # that users environment variables by default. ATTACH_TEMPLATE = """ %(command)s < greater than [ %s ] on volume group' ' [ %s ]' % (snapshot_size_gb, free_space, vg) ) self.failure( error='Not enough space to create snapshot', rc=2, msg=message ) # Create LVM Snapshot build_command = [ self.module.get_bin_path('lvcreate', True), "-n", snapshot_name, "-s", os.path.join(vg, source_lv), "-L%sg" % snapshot_size_gb ] rc, stdout, err = self._run_command(build_command) if rc not in self.rc: msg = ( 'Failed to Create LVM snapshot %(vg)s/%(source_lv)s' ' --> %(snapshot_name)s' % {'vg': vg, 'source_lv': source_lv, 'snapshot_name': snapshot_name} ) self.failure(err, rc, msg) def _lvm_lv_remove(self, name): vg = self._get_lxc_vg() # Create LVM Snapshot build_command = [ self.module.get_bin_path('lvremove', True), "-f", "%(vg)s/%(name)s" % dict(vg=vg, name=name), ] rc, stdout, err = self._run_command(build_command) if rc not in self.rc: msg = ("Failed to remove LVM LV %(vg)s/%(name)s " % {'vg': vg, 'name': name}) self.failure(err, rc, msg) def _lvm_lv_mount(self, lv_name, mount_point): # mount an lv vg = self._get_lxc_vg() build_command = [ self.module.get_bin_path('mount', True), "/dev/%(vg)s/%(lv_name)s" % dict(vg=vg, lv_name=lv_name), mount_point, ] rc, stdout, err = self._run_command(build_command) if rc not in self.rc: msg = ("failed to mountlvm lv %(vg)s/%(lv_name)s to %(mp)s" % {'vg': vg, 'lv_name': lv_name, 'mp': mount_point}) self.failure(err, rc, msg) def _unmount(self, mount_point): # Unmount a file system build_command = [ self.module.get_bin_path('umount', True), mount_point, ] rc, stdout, err = self._run_command(build_command) if rc not in self.rc: msg = ("failed to unmount %(mp)s" % {'mp': mount_point}) self.failure(err, rc, msg) def _create_tar(self, source_dir, archive_name): """Create an archive of a given ``source_dir`` to ``output_path``. :param source_dir: ``str`` Path to the directory to be archived. :param archive_name: ``str`` Name of the archive file. """ # remove trailing / if present. output_path = archive_name.rstrip(os.sep) if not output_path.endswith('tar.bz2'): output_path = '%s.tar.bz2' % output_path source_path = os.path.expanduser(source_dir) build_command = [ self.module.get_bin_path('tar', True), '--directory=%s' % source_path, '-cjf', output_path, '.' ] rc, stdout, err = self._run_command( build_command=build_command, unsafe_shell=True ) if rc not in self.rc: msg = "failed to create tar archive [ %s ]" % build_command self.failure(err, rc, msg) return output_path @staticmethod def _roundup(num): """Return a rounded floating point number. :param num: ``float`` Number to round up. """ num, part = str(num).split('.') num = int(num) if int(part) != 0: num += 1 return num def _container_create_tar(self, variables): """Create a tar archive from an LXC container. The process is as follows: * Freeze the container (pause processes) * Create temporary dir * Copy container config to tmpdir/ * Unfreeze the container * If LVM backed: * Create LVM snapshot of LV backing the container * Mount the snapshot to tmpdir/rootfs * Create tar of tmpdir * Clean up :param variables: ``list`` List of all variables that are available to use within the LXC Command """ required_vars = ['name', 'tarpath'] variables_dict = self._get_vars(variables, required=required_vars) name = variables_dict.pop('name') lxc_config_path = self._load_lxcpath(variables_dict) config_file, options = self._load_config(lxc_config_path, name) lxc_rootfs = [i for i in options if i.startswith('lxc.rootfs')] if lxc_rootfs: root_path = [i.strip() for i in lxc_rootfs[0].split('=')][1] else: message = ( 'Check the config file for container [ %s ] @ [ %s ]' % (name, config_file) ) return self.failure( error='No rootfs entry found in config.', rc=2, msg=message ) # Create a temp dir temp_dir = tempfile.mkdtemp() # Set the name of the working dir, temp + container_name work_dir = os.path.join(temp_dir, name) # Set the path to the container data container_path = os.path.join(lxc_config_path, name) # Get current container info container_data = self._get_container_info( name=name, variables_dict=variables_dict ) # set current state state = container_data.get('state') # Ensure the original container is stopped or frozen if state not in ['stopped', 'frozen']: # Freeze Container self._ensure_state( state='frozen', name=name, variables_dict=variables_dict ) # Prepare tmp dir build_command = [ self.module.get_bin_path('rsync', True), '-aHAX', container_path, temp_dir ] rc, stdout, err = self._run_command(build_command, unsafe_shell=True) if rc not in self.rc: self.failure(err, rc, msg='failed to perform backup') mount_point = os.path.join(work_dir, 'rootfs') if not os.path.exists(mount_point): os.makedirs(mount_point) # Restore original state of container self._ensure_state( state=state, name=name, variables_dict=variables_dict ) # Test if the containers rootfs is a block device block_backed = root_path.startswith(os.path.join(os.sep, 'dev')) snapshot_name = '%s_rpc_ansible_snapshot' % name if block_backed: if snapshot_name not in self._lvm_lv_list(): # Take snapshot size, measurement = self._get_lv_size(name=name) self._lvm_snapshot_create( source_lv=name, snapshot_name=snapshot_name, snapshot_size_gb=self._roundup(num=size) ) # Mount snapshot self._lvm_lv_mount( lv_name=snapshot_name, mount_point=mount_point ) try: # Create Tar archive_file = self._create_tar( source_dir=work_dir, archive_name=variables_dict['tarpath'] ) except Exception as exp: self.failure(error=exp, rc=2, msg='Failed to create the archive') else: # Set the state as changed and set a new fact self.state_change = True archive_fact = { name: { 'archive': archive_file } } return self._lxc_facts(facts=archive_fact) finally: if block_backed: # unmount snapshot self._unmount(mount_point) # Remove snapshot self._lvm_lv_remove(snapshot_name) # Remove tmpdir shutil.rmtree(os.path.dirname(work_dir)) def main(): """Ansible Main module.""" module = AnsibleModule( argument_spec=dict( name=dict( type='str' ), return_code=dict( type='str', default='0' ), template=dict( type='str', default='ubuntu' ), backingstore=dict( type='str' ), template_options=dict( type='str' ), config=dict( type='str', default='/etc/lxc/default.conf' ), bdev=dict( type='str' ), lvname=dict( type='str' ), vgname=dict( type='str' ), thinpool=dict( type='str' ), fstype=dict( type='str' ), fssize=dict( type='str' ), dir=dict( type='str' ), zfsroot=dict( type='str' ), lxcpath=dict( type='str' ), keepname=dict( choices=BOOLEANS, default='false' ), snapshot=dict( choices=BOOLEANS, default='false' ), newpath=dict( type='str' ), orig=dict( type='str' ), new=dict( type='str' ), state=dict( choices=[ 'running', 'stopped' ], default='running' ), command=dict( required=True, choices=COMMAND_MAP.keys() ), container_command=dict( type='str' ), options=dict( type='str' ), return_facts=dict( choices=BOOLEANS, default=False ), tarpath=dict( type='str' ) ), supports_check_mode=False, ) return_code = module.params.get('return_code', '').split(',') module.params['return_code'] = return_code lm = LxcManagement(module=module) lm.command_router() # import module bits from ansible.module_utils.basic import * main()