Skip to content

Instantly share code, notes, and snippets.

@DavidWittman
Created July 9, 2014 15:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DavidWittman/097b915360949490b93e to your computer and use it in GitHub Desktop.
Save DavidWittman/097b915360949490b93e to your computer and use it in GitHub Desktop.
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2014, Kevin Carter <kevin.carter@rackspace.com>
#
# 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/>.
DOCUMENTATION = """
---
author: Kevin Carter
module: lxc
short_description: Setup and Deploy containers with LXC
description:
- This module creates, removes, starts, stops, and commands to LXC containers
version_added: "0.0.1"
requirements: []
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:
- 'backingstore' is one of 'none', 'dir', 'lvm', 'loop', 'btrfs',
or '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: M)
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'
command:
choices:
- ['list', 'create', 'destroy', 'info', 'attach', 'start', 'stop']
description:
- Type of command to run, see Examples.
required: true
notes:
- module was built for LXC 1.0 or Later
- module does not implement all LXC functions
- module will log all LXC interactions possible in the `/tmp` directory.
"""
EXAMPLES = """
# Create a new LXC container.
- lxc: name=test-container
template=ubuntu
config=/etc/lxc/lxc-rpc.conf
command=create
state=running
# 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
# Clone a container.
- lxc: orig=test-container
new=test-container-new
command=clone
state=started
# 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'
],
}
}
# 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.
:param commands: ``dict`` Routing map for available commands.
"""
self.module = module
self.rc = [int(i) for i in self.module.params.get('return_code')]
self.state_change = False
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):
"""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']
if state != current_state:
if state == 'running':
self._starter(name=name, variables_dict=variables_dict)
# If the state has changed to running. rest a second.
self.state_change = True
time.sleep(5)
self._ensure_state(
state=state, name=name, variables_dict=variables_dict
)
elif state == 'stopped':
self._stopper(name=name, variables_dict=variables_dict)
self.state_change = True
time.sleep(5)
self._ensure_state(
state=state, name=name, variables_dict=variables_dict
)
if tries <= 5:
tries += 1
self._ensure_state(
state=state,
name=name,
variables_dict=variables_dict,
tries=tries
)
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='Variable Missing',
rc=000,
msg='Missing [ %s ] from play.' % var_name
)
return return_dict
def _run_command(self, build_command, unsafe_shell=False):
"""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`.
:param build_command: ``list`` Used for the command and all options.
"""
command = self._construct_command(build_list=build_command)
return self.module.run_command(command, use_unsafe_shell=unsafe_shell)
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
@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('--%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)
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)
return_data = self._list(variables_dict=variables_dict)
return_data = return_data.split()
containers = {}
for container in return_data:
cstate = containers[container] = {}
self._get_container_info(name=container, cstate=cstate)
else:
return self._lxc_facts(facts=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')
containers = self._list(variables_dict=variables_dict)
if name not in containers:
created_container_info = self._create(name, state, variables_dict)
return self._lxc_facts(facts=created_container_info)
else:
container_info = self._get_container_info(
name=name, variables_dict=variables_dict
)
return self._lxc_facts(facts=container_info)
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')
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_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')
build_command = [
self.module.get_bin_path('lxc-clone', True),
'--logfile /tmp/lxc-ansible-%s-clone-%s.log' % (orig, new),
'--logpriority INFO',
'--copyhooks',
'--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 'keepname' in variables_dict:
del(variables_dict['keepname'])
build_command.append('-K')
if 'copyhooks' in variables_dict:
del(variables_dict['copyhooks'])
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=variables_dict.get('state'),
name=new,
variables_dict=variables_dict
)
self.state_change = True
container_info = self._get_container_info(
name=new, variables_dict=variables_dict
)
return self._lxc_facts(facts=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=container_info)
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
)
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': container_command
}
attach_command = [ATTACH_TEMPLATE % attach_vars]
rc, return_data, err = self._run_command(
attach_command, unsafe_shell=True
)
if rc not in self.rc:
msg = "Failed executing lxc-attach."
self.failure(err, rc, msg)
else:
self.state_change = True
if return_facts in BOOLEANS_TRUE:
if return_data:
return self._lxc_facts(return_data.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
self._ensure_state(
state='running',
name=name,
variables_dict=variables_dict
)
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
self._ensure_state(
state='stopped',
name=name,
variables_dict=variables_dict
)
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')
container_data = self._get_container_info(
name=name, variables_dict=variables_dict
)
if container_data.get('state') != 'running':
self._starter(name=name, 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')
container_data = self._get_container_info(
name=name, variables_dict=variables_dict
)
if container_data.get('state') != 'stopped':
self._stopper(name=name, variables_dict=variables_dict)
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=[
'list',
'create',
'destroy',
'info',
'attach',
'start',
'stop'
]
),
container_command=dict(
type='str'
),
return_facts=dict(
choices=BOOLEANS,
default=False
)
),
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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment