openstack-ansible/rpc_deployment/library/lxc
David Wittman 7032f4bb1e Idempotency for LXC cloning
Previously, clone operations where the new container already exists were
reporting as failed. This commit modifies that behavior so that if the
new container exists, it returns the facts for that container instead of
failing.

To facilitate these changes, a _container_exists method was added which
returns True or False based on whether or not the container name is in
the output from self._list().

I also made a few fixes in the documentation by removing trailing spaces
and fixing a typo in the state on line 222.
2014-09-29 12:31:10 -05:00

1605 lines
52 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2014, Kevin Carter <kevin.carter@rackspace.com>
# (c) 2014, Hugh Saunders <hugh.saunders@rackspace.co.uk>
#
# 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 <http://www.gnu.org/licenses/>.
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 <<EOL
pushd \$(grep \$(whoami) /etc/passwd | awk -F':' '{print \$6}')
if [[ -f ".bashrc" ]];then
source .bashrc
fi
%(container_command)s
popd
EOL
"""
class LxcManagement(object):
"""Manage LXC containers with ansible."""
def __init__(self, module):
"""Management of LXC containers via Ansible.
:param module: ``object`` Processed Ansible Module.
"""
self.module = module
self.rc = [int(i) for i in self.module.params.get('return_code')]
self.state_change = False
self.lxc_vg = None
def failure(self, error, rc, msg):
"""Return a Failure when running an Ansible command.
:param error: ``str`` Error that occurred.
:param rc: ``int`` Return code while executing an Ansible command.
:param msg: ``str`` Message to report.
"""
self.module.fail_json(msg=msg, rc=rc, err=error)
@staticmethod
def _lxc_facts(facts):
"""Return a dict for our Ansible facts.
:param facts: ``dict`` Dict with data to return
"""
return {'lxc_facts': facts}
def _ensure_state(self, state, name, variables_dict,
tries=0, max_tries=60, sleep_time=.5):
"""Ensure that the container is within a defined state.
:param state: ``str`` The desired state of the container.
:param name: ``str`` Name of the container.
:param variables_dict: ``dict`` Preparsed optional variables used from
a seed command.
:param tries: ``int`` The number of attempts before quiting.
"""
container_data = self._get_container_info(
name=name, variables_dict=variables_dict
)
current_state = container_data['state']
state_map = {
'running': {
'frozen': self._freezer,
'stopped': self._stopper
},
'stopped': {
'running': self._starter,
'frozen': self._starter
},
'frozen': {
'running': self._unfreezer,
'stopped': self._stopper
}
}
if state != current_state:
if tries <= max_tries:
state_map[current_state][state](
name=name, variables_dict=variables_dict
)
self.state_change = True
time.sleep(sleep_time)
self._ensure_state(
state=state, name=name,
variables_dict=variables_dict, tries=tries + 1
)
else:
message = (
'State failed to change for container [ %s ] --'
' [ %s ] != [ %s ]' % (name, state, current_state)
)
self.failure(
error='Failed to change the state of the container.',
rc=2,
msg=message
)
def command_router(self):
"""Run the command as its provided to the module."""
command_name = self.module.params['command']
if command_name in COMMAND_MAP:
action_command = COMMAND_MAP[command_name]
action_name = action_command['command']
if hasattr(self, '_%s' % action_name):
action = getattr(self, '_%s' % action_name)
facts = action(variables=action_command['variables'])
if facts is None:
self.module.exit_json(changed=self.state_change)
else:
self.module.exit_json(
changed=self.state_change,
ansible_facts=facts
)
else:
self.failure(
error='Command not in LxcManagement class',
rc=2,
msg='Method [ %s ] was not found.' % action_name
)
else:
self.failure(
error='No Command Found',
rc=2,
msg='Command [ %s ] was not found.' % command_name
)
def _get_vars(self, variables, required=None):
"""Return a dict of all variables as found within the module.
:param variables: ``list`` List of all variables to find.
:param required: ``list`` Name of variables that are required.
"""
return_dict = {}
for variable in variables:
_var = self.module.params.get(variable)
if _var not in [None, '', False]:
return_dict[variable] = self.module.params[variable]
else:
if isinstance(required, list):
for var_name in required:
if var_name not in return_dict:
self.failure(
error='Missing [ %s ] from Task' % var_name,
rc=000,
msg='known variables %s - available params %s'
% (variables, self.module.params)
)
return return_dict
def _run_command(self, build_command, unsafe_shell=False, timeout=600):
"""Return information from running an Ansible Command.
This will squash the build command list into a string and then
execute the command via Ansible. The output is returned to the method.
This output is returned as `return_code`, `stdout`, `stderr`.
Prior to running the command the method will look to see if the LXC
lockfile is present. If the lockfile "/var/lock/subsys/lxc" the method
will wait upto 10 minutes for it to be gone; polling every 5 seconds.
:param build_command: ``list`` Used for the command and all options.
"""
lockfile = '/var/lock/subsys/lxc'
command = self._construct_command(build_list=build_command)
for _ in xrange(timeout):
if os.path.exists(lockfile):
time.sleep(1)
else:
return self.module.run_command(
command, use_unsafe_shell=unsafe_shell
)
else:
message = (
'The LXC subsystem is locked and after 5 minutes it never'
' became unlocked. Lockfile [ %s ]' % lockfile
)
self.failure(
error='LXC subsystem locked',
rc=0,
msg=message
)
def _get_container_info(self, name, cstate=None, variables_dict=None):
"""Return a dict of information pertaining to a known container.
:param name: ``str`` name of the container.
:param cstate: ``dict`` dict to build within while gathering
information. If `None` an empty dict will be
created.
:param variables_dict: ``dict`` Preparsed optional variables used from
a seed command.
"""
if cstate is None:
cstate = {}
build_command = [
self.module.get_bin_path('lxc-info', True),
'--name %s' % name
]
if isinstance(variables_dict, dict):
self._add_variables(
variables_dict=variables_dict,
build_command=build_command,
allowed_variables=COMMAND_MAP['info']['variables']
)
rc, return_data, err = self._run_command(build_command)
if rc not in self.rc:
msg = 'Failed to get container Information'
self.failure(err, rc, msg)
ip_count = 0
for state in return_data.splitlines():
key, value = state.split(':')
if not key.startswith(' '):
if key.lower() == 'ip':
cstate['ip_%s' % ip_count] = value.lower().strip()
ip_count += 1
else:
cstate[key.lower().strip()] = value.lower().strip()
else:
return cstate
def _container_exists(self, name, variables_dict):
"""Return True or False based on whether or not the container exists
:param name: ``str`` name of the container
"""
containers = self._list(variables_dict=variables_dict)
for container in containers.splitlines():
if container == name:
return True
return False
@staticmethod
def _construct_command(build_list):
"""Return a string from a command and build list.
:param build_list: ``list`` List containing a command with options
"""
return ' '.join(build_list)
@staticmethod
def _add_variables(variables_dict, build_command, allowed_variables):
"""Return a command list with all found options.
:param variables_dict: ``dict`` Preparsed optional variables used from
a seed command.
:param build_command: ``list`` Command to run.
:param allowed_variables: ``list`` Variables that are allowed for use.
"""
for key, value in variables_dict.items():
if key in allowed_variables:
if isinstance(value, bool):
build_command.append('--%s' % value)
else:
build_command.append(
'--%s %s' % (key, value)
)
else:
return build_command
def _list(self, variables_dict):
"""Return a list of containers.
:param variables_dict: ``dict`` Preparsed optional variables used from
a seed command.
"""
build_command = [self.module.get_bin_path('lxc-ls', True)]
self._add_variables(
variables_dict=variables_dict,
build_command=build_command,
allowed_variables=COMMAND_MAP['list']['variables']
)
rc, return_data, err = self._run_command(build_command)
if rc not in self.rc:
msg = "Failed executing lxc-ls."
self.failure(err, rc, msg)
else:
return return_data
def _container_list(self, variables):
"""Return a dict of all containers.
:param variables: ``list`` List of all variables used in this command
"""
variables_dict = self._get_vars(variables)
containers = self._list(variables_dict=variables_dict)
all_containers = {}
for container in containers.splitlines():
cstate = all_containers[container] = {}
self._get_container_info(name=container, cstate=cstate)
else:
return self._lxc_facts(facts=all_containers)
def _create(self, name, state, variables_dict):
"""Create a new LXC container.
:param name: ``str`` Name of the container.
:param state: ``str`` State of the container once its been built
:param variables_dict: ``dict`` Preparsed optional variables used from
a seed command.
"""
if 'template_options' in variables_dict:
template_options = '-- %s' % variables_dict.pop('template_options')
else:
template_options = None
build_command = [
self.module.get_bin_path('lxc-create', True),
'--logfile /tmp/lxc-ansible-%s-create.log' % name,
'--logpriority INFO',
'--name %s' % name
]
self._add_variables(
variables_dict=variables_dict,
build_command=build_command,
allowed_variables=COMMAND_MAP['create']['variables']
)
if template_options is not None:
build_command.append(template_options)
rc, return_data, err = self._run_command(build_command)
if rc not in self.rc:
msg = "Failed executing lxc-create."
self.failure(err, rc, msg)
else:
self._ensure_state(
state=state,
name=name,
variables_dict=variables_dict
)
container_info = self._get_container_info(
name=name, variables_dict=variables_dict
)
self.state_change = True
return container_info
def _container_create(self, variables):
"""Create an LXC container.
:param variables: ``list`` List of all variables that are available to
use within the LXC Command
"""
variables_dict = self._get_vars(variables, required=['name'])
name = variables_dict.pop('name')
state = variables_dict.pop('state', 'running')
if self._container_exists(name, variables_dict):
container_info = self._get_container_info(
name=name, variables_dict=variables_dict
)
else:
container_info = self._create(name, state, variables_dict)
return self._lxc_facts(facts={name: container_info})
def _destroy(self, name):
"""Destroy an LXC container.
:param name: ``str`` Name of the container to destroy
"""
build_command = [
self.module.get_bin_path('lxc-destroy', True),
'--logfile /tmp/lxc-ansible-%s-destroy.log' % name,
'--logpriority INFO',
'--force',
'--name %s' % name
]
rc, return_data, err = self._run_command(build_command)
if rc not in self.rc:
msg = "Failed executing lxc-destroy."
self.failure(err, rc, msg)
else:
self.state_change = True
def _container_destroy(self, variables):
"""Destroy an LXC container.
:param variables: ``list`` List of all variables that are available to
use within the LXC Command.
"""
variables_dict = self._get_vars(variables, required=['name'])
name = variables_dict.pop('name')
if self._container_exists(name, variables_dict):
self._destroy(name=name)
def _local_clone(self, orig, new, variables_dict):
"""Clone a local container.
:param orig: ``str`` Original container name
:param new: ``str`` New container name
:param variables_dict: ``dict`` Dictionary of options
"""
state = variables_dict.pop('state', 'running')
build_command = [
self.module.get_bin_path('lxc-clone', True),
'--orig %s' % orig,
'--new %s' % new,
]
# The next set of if statements are special cases because the lxc-clone
# API is a bit different than the rest of the LXC commands line clients
# TODO(cloudnull) When the CLI gets better this should be updated.
if variables_dict.pop('keepname', 'false') in BOOLEANS_TRUE:
build_command.append('-K')
if variables_dict.pop('snapshot', 'false') in BOOLEANS_TRUE:
build_command.append('-s')
if variables_dict.pop('copyhooks', 'false') in BOOLEANS_TRUE:
build_command.append('-H')
if 'fssize' in variables_dict:
build_command.append('-L %s' % variables_dict.pop('fssize'))
self._add_variables(
variables_dict=variables_dict,
build_command=build_command,
allowed_variables=COMMAND_MAP['clone']['variables']
)
rc, return_data, err = self._run_command(build_command)
if rc not in self.rc:
msg = "Failed executing lxc-clone."
self.failure(err, rc, msg)
else:
self._ensure_state(
state=state,
name=new,
variables_dict=variables_dict
)
self.state_change = True
container_info = self._get_container_info(
name=new, variables_dict=variables_dict
)
return container_info
def _container_clone(self, variables):
"""Clone an LXC container.
:param variables: ``list`` List of all variables that are available to
use within the LXC Command
"""
variables_dict = self._get_vars(variables, required=['orig', 'new'])
orig = variables_dict.pop('orig')
new = variables_dict.pop('new')
# Check to see if new container already exists
if self._container_exists(new, variables_dict):
new_container_info = self._get_container_info(
name=new, variables_dict=variables_dict
)
return self._lxc_facts(facts={new: new_container_info})
container_data = self._get_container_info(
name=orig, variables_dict=variables_dict
)
# Stop the original container
if container_data.get('state') != 'stopped':
self._stopper(name=orig, variables_dict=variables_dict)
# Clone the container
container_info = self._local_clone(orig, new, variables_dict)
# Restart the original container
self._starter(name=orig, variables_dict=variables_dict)
return self._lxc_facts(facts={new: container_info})
def _container_info(self, variables):
"""Return Ansible facts on an LXC container.
:param variables: ``list`` List of all variables that are available to
use within the LXC Command.
"""
variables_dict = self._get_vars(variables, required=['name'])
name = variables_dict.pop('name')
self._get_container_info(name=name, variables_dict=variables_dict)
container_info = self._get_container_info(
name=name, variables_dict=variables_dict
)
return self._lxc_facts(facts={name: container_info})
@staticmethod
def _get_kernel_version():
"""Return the Kernel version number as a float."""
os_data = os.uname()
kernel_info = os_data[2].split('-')
kernel_release = kernel_info[0]
return [int(i) for i in kernel_release.split('.')]
def _run_attach(self, name, inner_command, variables_dict):
"""Return the results of executing an LXC Attach command.
:param name: ``str`` Name of the container.
:param inner_command: ``str`` Command to run within a container.
:param variables_dict: ``dict`` Preparsed optional variables used from
a seed command.
"""
def kernel_fail():
self.failure(
error='Unsupportable Kernel Version found.',
rc=2,
msg='The kernel version installed [ %s ] does not support'
' LXC attach. Please upgrade to at least Kernel 3.8'
% kernel_version
)
kernel_version = self._get_kernel_version()
if kernel_version[0] < 3:
kernel_fail()
elif kernel_version[1] < 6:
kernel_fail()
build_command = [
self.module.get_bin_path('lxc-attach', True),
'--logfile /tmp/lxc-ansible-%s-attach.log' % name,
'--logpriority INFO',
'--name %s' % name,
]
self._add_variables(
variables_dict=variables_dict,
build_command=build_command,
allowed_variables=COMMAND_MAP['attach']['variables']
)
command = self._construct_command(build_list=build_command)
attach_vars = {
'command': command,
'container_command': inner_command
}
rc, return_data, err = self._run_command(
[ATTACH_TEMPLATE % attach_vars], unsafe_shell=True
)
if rc not in self.rc:
msg = "Failed executing lxc-attach."
self.failure(err, rc, msg)
else:
self.state_change = True
return return_data
def _container_attach(self, variables):
"""Attach to an LXC container and execute a command.
:param variables: ``list`` List of all variables that are available to
use within the LXC Command
"""
required_vars = ['name', 'container_command']
variables_dict = self._get_vars(variables, required=required_vars)
name = variables_dict.pop('name')
container_command = variables_dict.pop('container_command')
return_facts = variables_dict.pop('return_facts', 'false')
self._ensure_state(
state='running',
name=name,
variables_dict=variables_dict
)
attach_results = self._run_attach(
name=name,
inner_command=container_command,
variables_dict=variables_dict
)
if return_facts in BOOLEANS_TRUE:
if attach_results:
return self._lxc_facts(facts=attach_results.splitlines())
def _starter(self, name, variables_dict):
"""Start an LXC Container.
:param name: ``str`` Name of the container.
:param variables_dict: ``dict`` Preparsed optional variables used from
a seed command.
"""
build_command = [
self.module.get_bin_path('lxc-start', True),
'--logfile /tmp/lxc-ansible-%s-start.log' % name,
'--logpriority INFO',
'--daemon',
'--name %s' % name
]
self._add_variables(
variables_dict=variables_dict,
build_command=build_command,
allowed_variables=COMMAND_MAP['start']['variables']
)
rc, return_data, err = self._run_command(build_command)
if rc not in self.rc:
msg = "Failed executing lxc-start."
self.failure(err, rc, msg)
else:
self.state_change = True
def _stopper(self, name, variables_dict):
"""Stop an LXC Container.
:param name: ``str`` Name of the container.
:param variables_dict: ``dict`` Preparsed optional variables used from
a seed command.
"""
build_command = [
self.module.get_bin_path('lxc-stop', True),
'--logfile /tmp/lxc-ansible-%s-stop.log' % name,
'--logpriority INFO',
'--timeout 10',
'--name %s' % name
]
if variables_dict is not None:
self._add_variables(
variables_dict=variables_dict,
build_command=build_command,
allowed_variables=COMMAND_MAP['stop']['variables']
)
rc, return_data, err = self._run_command(build_command)
if rc not in self.rc:
msg = "Failed executing lxc-stop."
self.failure(err, rc, msg)
else:
self.state_change = True
def _container_restart(self, variables):
"""Restart an LXC container.
:param variables: ``list`` List of all variables that are available to
use within the LXC Command
"""
variables_dict = self._get_vars(variables, required=['name'])
name = variables_dict.pop('name')
self._ensure_state(name=name, state='stopped',
variables_dict=variables_dict)
self._ensure_state(name=name, state='running',
variables_dict=variables_dict)
def _freezer(self, name, variables_dict):
"""Freeze an LXC Container.
:param name: ``str`` Name of the container.
:param variables_dict: ``dict`` Preparsed optional variables used from
a seed command.
"""
build_command = [
self.module.get_bin_path('lxc-freeze', True),
'--logfile /tmp/lxc-ansible-%s-freeze.log' % name,
'--logpriority INFO',
'--name %s' % name
]
if variables_dict is not None:
self._add_variables(
variables_dict=variables_dict,
build_command=build_command,
allowed_variables=['lxcpath']
)
rc, return_data, err = self._run_command(build_command)
if rc not in self.rc:
msg = "Failed executing lxc-freeze."
self.failure(err, rc, msg)
else:
self.state_change = True
def _unfreezer(self, name, variables_dict):
"""unfreeze an LXC Container.
:param name: ``str`` Name of the container.
:param variables_dict: ``dict`` Preparsed optional variables used from
a seed command.
"""
build_command = [
self.module.get_bin_path('lxc-unfreeze', True),
'--logfile /tmp/lxc-ansible-%s-unfreeze.log' % name,
'--logpriority INFO',
'--name %s' % name
]
if variables_dict is not None:
self._add_variables(
variables_dict=variables_dict,
build_command=build_command,
allowed_variables=['lxcpath']
)
rc, return_data, err = self._run_command(build_command)
if rc not in self.rc:
msg = "Failed executing lxc-freeze."
self.failure(err, rc, msg)
else:
self.state_change = True
def _container_start(self, variables):
"""Start an LXC container.
:param variables: ``list`` List of all variables that are available to
use within the LXC Command
"""
variables_dict = self._get_vars(variables, required=['name'])
name = variables_dict.pop('name')
self._ensure_state(
name=name, state='running', variables_dict=variables_dict
)
def _container_stop(self, variables):
"""Stop an LXC container.
:param variables: ``list`` List of all variables that are available to
use within the LXC Command
"""
variables_dict = self._get_vars(variables, required=['name'])
name = variables_dict.pop('name')
self._ensure_state(name=name, state='stopped',
variables_dict=variables_dict)
@staticmethod
def _load_config(lxcpath, name):
"""Return a list of lines from an LXC config file.
:param lxcpath: ``str`` path to lxc container directory
:param name: ``str`` name of LXC container
"""
container_config_file = os.path.join(lxcpath, name, 'config')
with open(container_config_file, 'rb') as f:
return container_config_file, f.readlines()
def _load_lxcpath(self, variables_dict, pop=False):
if 'lxcpath' not in variables_dict:
build_command = [
self.module.get_bin_path('lxc-config', True),
'lxc.lxcpath'
]
rc, return_data, err = self._run_command(build_command)
if rc not in self.rc:
msg = "Failed executing lxc-config."
self.failure(err, rc, msg)
lxcpath = return_data
else:
if pop is True:
lxcpath = variables_dict.pop('lxcpath')
else:
lxcpath = variables_dict.get('lxcpath')
return lxcpath.strip()
def _container_config(self, variables):
"""Configure an LXC container.
Write new configuration values to the lxc config file. This will
stop the container if its running write the new options and then
restart the container upon completion if the state is set to running,
which is the default.
:param variables: ``list`` List of all variables that are available to
use within the LXC Command
"""
required_vars = ['name', 'options']
variables_dict = self._get_vars(variables, required=required_vars)
name = variables_dict.pop('name')
lxcpath = self._load_lxcpath(variables_dict, pop=True)
container_config_file, container_config = self._load_config(
lxcpath, name
)
# Note used ast literal_eval because AnsibleModule does not provide for
# adequate dictionary parsing.
# Issue: https://github.com/ansible/ansible/issues/7679
# TODO(cloudnull) adjust import when issue has been resolved.
options_dict = variables_dict.pop('options')
options_dict = ast.literal_eval(options_dict)
parsed_options = [i.split('=') for i in options_dict]
for key, value in parsed_options:
new_entry = '%s = %s\n' % (key, value)
for option_line in container_config:
# Look for key in config
if option_line.startswith(key):
_, _value = option_line.split('=')
config_value = ' '.join(_value.split())
line_index = container_config.index(option_line)
# If the sanitized values don't match replace them
if value != config_value:
line_index += 1
if new_entry not in container_config:
self.state_change = True
container_config.insert(line_index, new_entry)
# Break the flow as values are written or not at this point
break
else:
self.state_change = True
container_config.append(new_entry)
# If the state changed restart the container.
if self.state_change is True:
self._ensure_state(
name=name, state='stopped', variables_dict=variables_dict
)
with open(container_config_file, 'wb') as f:
f.writelines(container_config)
self._ensure_state(
name=name, state='running', variables_dict=variables_dict
)
def _get_lxc_vg(self):
"""Return the name of the Volume Group used in LXC."""
build_command = [
self.module.get_bin_path('lxc-config', True),
"lxc.bdev.lvm.vg"
]
rc, vg, err = self._run_command(build_command)
if rc not in self.rc:
msg = " Failed to read LVM VG from LXC config"
self.failure(err, rc, msg)
else:
return vg.strip()
def _lvm_lv_list(self):
"""Return a list of all lv in a current vg."""
vg = self._get_lxc_vg()
build_command = [
self.module.get_bin_path('lvs', True),
]
rc, stdout, err = self._run_command(build_command)
if rc not in self.rc:
msg = "Failed to get list of LVs"
self.failure(err, rc, msg)
all_lvms = [i.split() for i in stdout.splitlines()][1:]
return [lv_entry[0] for lv_entry in all_lvms if lv_entry[1] == vg]
def _get_vg_free_pe(self, name):
"""Return the available size of a given VG.
Returns ``size``, ``measurement``
:param name: ``str`` Name of volume group.
"""
build_command = [
'vgdisplay',
name
]
rc, stdout, err = self._run_command(build_command)
if rc not in self.rc:
self.failure(err, rc, msg='failed to read vg %s' % name)
vg_info = [i.strip() for i in stdout.splitlines()][1:]
free_pe = [i for i in vg_info if i.startswith('Free')]
_free_pe = free_pe[0].split()
return float(_free_pe[-2]), _free_pe[-1]
def _get_lv_size(self, name):
"""Return the available size of a given LV.
Returns ``size``, ``measurement``
:param name: ``str`` Name of volume.
"""
vg = self._get_lxc_vg()
lv = os.path.join(vg, name)
build_command = [
'lvdisplay',
lv
]
rc, stdout, err = self._run_command(build_command)
if rc not in self.rc:
self.failure(err, rc, msg='failed to read lv %s' % lv)
lv_info = [i.strip() for i in stdout.splitlines()][1:]
free_pe = [i for i in lv_info if i.startswith('LV Size')]
_free_pe = free_pe[0].split()
return float(_free_pe[-2]), _free_pe[-1]
def _lvm_snapshot_create(self, source_lv, snapshot_name,
snapshot_size_gb=5):
"""Create an LVM snapshot.
:param source_lv: ``str`` Name of lv to snapshot
:param snapshot_name: ``str`` Name of lv snapshot
:param snapshot_size_gb: ``int`` Size of snapshot to create
"""
vg = self._get_lxc_vg()
free_space, messurement = self._get_vg_free_pe(name=vg)
if free_space < float(snapshot_size_gb):
message = (
'Snapshot size [ %s ] is > 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()