Added overlayfs backend type to the lxc_container module

This commit adds the overlayfs type to the lxc_container module. In
Adding the overlayfs type the commit adds the ability to clone a
container. While cloning is not locked down to only the overlayfs
container backend it is of particular interest when using the overlayfs
backend as it provides for amazingly fast snapshots.

Changes to the resource types and documentation have been added on how
the new backend type can be used along with the clone operation.

This PR addresses a question asked on the original merged pull request
for overlayfs support which came from @fghaas on PR
"https://github.com/ansible/ansible-modules-extras/pull/123".

The overlayfs archive function is a first class function and will
allow for the containers to be backed-up using all methods which
brings support up to that of all other storage backends.
This commit is contained in:
Kevin Carter 2015-03-19 08:45:28 -05:00
parent cb848fcd9e
commit 2f188600a8

View file

@ -38,6 +38,7 @@ options:
- lvm - lvm
- loop - loop
- btrfs - btrfs
- overlayfs
description: description:
- Backend storage type for the container. - Backend storage type for the container.
required: false required: false
@ -112,6 +113,20 @@ options:
- Set the log level for a container where *container_log* was set. - Set the log level for a container where *container_log* was set.
required: false required: false
default: INFO default: INFO
clone_name:
description:
- Name of the new cloned server. This is only used when state is
clone.
required: false
clone_snapshot:
choices:
- true
- false
description:
- Create a snapshot a container when cloning. This is not supported
by all container storage backends. Enabling this may fail if the
backing store does not support snapshots.
default: false
archive: archive:
choices: choices:
- true - true
@ -141,8 +156,12 @@ options:
- restarted - restarted
- absent - absent
- frozen - frozen
- clone
description: description:
- Start a container right after it's created. - Define the state of a container. If you use clone the container
will be stopped while the clone operation is happening and upon
completion of the clone the original container state will be
restored.
required: false required: false
default: started default: started
container_config: container_config:
@ -295,6 +314,47 @@ EXAMPLES = """
archive: true archive: true
archive_path: /opt/archives archive_path: /opt/archives
- name: Create an overlayfs container
lxc_container:
name: test-container-overlayfs
container_log: true
template: ubuntu
state: started
backing_store: overlayfs
template_options: --release trusty
- name: Clone a container
lxc_container:
name: test-container-overlayfs
clone_name: test-container-clone
state: clone
- name: Clone a container using snapshot.
lxc_container:
name: test-container-overlayfs
clone_name: test-container-overlayfs-clone
backing_store: overlayfs
clone_snapshot: true
state: clone
- name: Create a new container and clone it
lxc_container:
name: test-container-new-overlayfs
clone_name: test-container-new-overlayfs-clone
backing_store: overlayfs
clone_snapshot: true
state: clone
- name: Create a new container, clone it, and archive
lxc_container:
name: test-container-new-overlayfs
clone_name: test-container-new-overlayfs-clone
backing_store: overlayfs
clone_snapshot: true
state: clone
archive: true
archive_compression: gzip
- name: Destroy a container. - name: Destroy a container.
lxc_container: lxc_container:
name: "{{ item }}" name: "{{ item }}"
@ -305,6 +365,9 @@ EXAMPLES = """
- test-container-frozen - test-container-frozen
- test-container-lvm - test-container-lvm
- test-container-config - test-container-config
- test-container-overlayfs
- test-container-clone
- test-container-overlayfs-clone
""" """
@ -351,6 +414,15 @@ LXC_COMMAND_MAP = {
'directory': '--dir', 'directory': '--dir',
'zfs_root': '--zfsroot' 'zfs_root': '--zfsroot'
} }
},
'clone': {
'variables': {
'backing_store': '--backingstore',
'lxc_path': '--lxcpath',
'fs_size': '--fssize',
'name': '--orig',
'clone_name': '--new'
}
} }
} }
@ -369,6 +441,9 @@ LXC_BACKING_STORE = {
], ],
'loop': [ 'loop': [
'lv_name', 'vg_name', 'thinpool', 'zfs_root' 'lv_name', 'vg_name', 'thinpool', 'zfs_root'
],
'overlayfs': [
'lv_name', 'vg_name', 'fs_type', 'fs_size', 'thinpool', 'zfs_root'
] ]
} }
@ -388,7 +463,8 @@ LXC_ANSIBLE_STATES = {
'stopped': '_stopped', 'stopped': '_stopped',
'restarted': '_restarted', 'restarted': '_restarted',
'absent': '_destroyed', 'absent': '_destroyed',
'frozen': '_frozen' 'frozen': '_frozen',
'clone': '_clone'
} }
@ -502,15 +578,15 @@ class LxcContainerManagement(object):
return num return num
@staticmethod @staticmethod
def _container_exists(name): def _container_exists(container_name):
"""Check if a container exists. """Check if a container exists.
:param name: Name of the container. :param container_name: Name of the container.
:type: ``str`` :type: ``str``
:returns: True or False if the container is found. :returns: True or False if the container is found.
:rtype: ``bol`` :rtype: ``bol``
""" """
if [i for i in lxc.list_containers() if i == name]: if [i for i in lxc.list_containers() if i == container_name]:
return True return True
else: else:
return False return False
@ -543,6 +619,7 @@ class LxcContainerManagement(object):
""" """
# Remove incompatible storage backend options. # Remove incompatible storage backend options.
variables = variables.copy()
for v in LXC_BACKING_STORE[self.module.params['backing_store']]: for v in LXC_BACKING_STORE[self.module.params['backing_store']]:
variables.pop(v, None) variables.pop(v, None)
@ -655,6 +732,83 @@ class LxcContainerManagement(object):
self._container_startup() self._container_startup()
self.container.freeze() self.container.freeze()
def _clone(self, count=0):
"""Clone a new LXC container from an existing container.
This method will clone an existing container to a new container using
the `clone_name` variable as the new container name. The method will
create a container if the container `name` does not exist.
Note that cloning a container will ensure that the original container
is "stopped" before the clone can be done. Because this operation can
require a state change the method will return the original container
to its prior state upon completion of the clone.
Once the clone is complete the new container will be left in a stopped
state.
"""
self.check_count(count=count, method='clone')
if self._container_exists(container_name=self.container_name):
# Ensure that the state of the original container is stopped
container_state = self._get_state()
if container_state != 'stopped':
self.state_change = True
self.container.stop()
build_command = [
self.module.get_bin_path('lxc-clone', True),
]
build_command = self._add_variables(
variables_dict=self._get_vars(
variables=LXC_COMMAND_MAP['clone']['variables']
),
build_command=build_command
)
# Load logging for the instance when creating it.
if self.module.params.get('clone_snapshot') in BOOLEANS_TRUE:
build_command.append('--snapshot')
rc, return_data, err = self._run_command(build_command)
if rc != 0:
message = "Failed executing lxc-clone."
self.failure(
err=err, rc=rc, msg=message, command=' '.join(
build_command
)
)
else:
self.state_change = True
# Restore the original state of the origin container if it was
# not in a stopped state.
if container_state == 'running':
self.container.start()
elif container_state == 'frozen':
self.container.start()
self.container.freeze()
# Change the container name context to the new cloned container
# This enforces that the state of the new cloned container will be
# "stopped".
self.state = 'stopped'
self.container_name = self.module.params['clone_name']
self.container = self.get_container_bind()
# Return data
self._execute_command()
# Perform any configuration updates
self._config()
# Check if the container needs to have an archive created.
self._check_archive()
else:
self._create()
count += 1
self._clone(count)
def _create(self): def _create(self):
"""Create a new LXC container. """Create a new LXC container.
@ -709,9 +863,9 @@ class LxcContainerManagement(object):
rc, return_data, err = self._run_command(build_command) rc, return_data, err = self._run_command(build_command)
if rc != 0: if rc != 0:
msg = "Failed executing lxc-create." message = "Failed executing lxc-create."
self.failure( self.failure(
err=err, rc=rc, msg=msg, command=' '.join(build_command) err=err, rc=rc, msg=message, command=' '.join(build_command)
) )
else: else:
self.state_change = True self.state_change = True
@ -751,7 +905,7 @@ class LxcContainerManagement(object):
:rtype: ``str`` :rtype: ``str``
""" """
if self._container_exists(name=self.container_name): if self._container_exists(container_name=self.container_name):
return str(self.container.state).lower() return str(self.container.state).lower()
else: else:
return str('absent') return str('absent')
@ -816,7 +970,7 @@ class LxcContainerManagement(object):
""" """
for _ in xrange(timeout): for _ in xrange(timeout):
if not self._container_exists(name=self.container_name): if not self._container_exists(container_name=self.container_name):
break break
# Check if the container needs to have an archive created. # Check if the container needs to have an archive created.
@ -852,7 +1006,7 @@ class LxcContainerManagement(object):
""" """
self.check_count(count=count, method='frozen') self.check_count(count=count, method='frozen')
if self._container_exists(name=self.container_name): if self._container_exists(container_name=self.container_name):
self._execute_command() self._execute_command()
# Perform any configuration updates # Perform any configuration updates
@ -886,7 +1040,7 @@ class LxcContainerManagement(object):
""" """
self.check_count(count=count, method='restart') self.check_count(count=count, method='restart')
if self._container_exists(name=self.container_name): if self._container_exists(container_name=self.container_name):
self._execute_command() self._execute_command()
# Perform any configuration updates # Perform any configuration updates
@ -913,7 +1067,7 @@ class LxcContainerManagement(object):
""" """
self.check_count(count=count, method='stop') self.check_count(count=count, method='stop')
if self._container_exists(name=self.container_name): if self._container_exists(container_name=self.container_name):
self._execute_command() self._execute_command()
# Perform any configuration updates # Perform any configuration updates
@ -940,7 +1094,7 @@ class LxcContainerManagement(object):
""" """
self.check_count(count=count, method='start') self.check_count(count=count, method='start')
if self._container_exists(name=self.container_name): if self._container_exists(container_name=self.container_name):
container_state = self._get_state() container_state = self._get_state()
if container_state == 'running': if container_state == 'running':
pass pass
@ -1007,18 +1161,18 @@ class LxcContainerManagement(object):
all_lvms = [i.split() for i in stdout.splitlines()][1:] 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] return [lv_entry[0] for lv_entry in all_lvms if lv_entry[1] == vg]
def _get_vg_free_pe(self, name): def _get_vg_free_pe(self, vg_name):
"""Return the available size of a given VG. """Return the available size of a given VG.
:param name: Name of volume. :param vg_name: Name of volume.
:type name: ``str`` :type vg_name: ``str``
:returns: size and measurement of an LV :returns: size and measurement of an LV
:type: ``tuple`` :type: ``tuple``
""" """
build_command = [ build_command = [
'vgdisplay', 'vgdisplay',
name, vg_name,
'--units', '--units',
'g' 'g'
] ]
@ -1027,7 +1181,7 @@ class LxcContainerManagement(object):
self.failure( self.failure(
err=err, err=err,
rc=rc, rc=rc,
msg='failed to read vg %s' % name, msg='failed to read vg %s' % vg_name,
command=' '.join(build_command) command=' '.join(build_command)
) )
@ -1036,17 +1190,17 @@ class LxcContainerManagement(object):
_free_pe = free_pe[0].split() _free_pe = free_pe[0].split()
return float(_free_pe[-2]), _free_pe[-1] return float(_free_pe[-2]), _free_pe[-1]
def _get_lv_size(self, name): def _get_lv_size(self, lv_name):
"""Return the available size of a given LV. """Return the available size of a given LV.
:param name: Name of volume. :param lv_name: Name of volume.
:type name: ``str`` :type lv_name: ``str``
:returns: size and measurement of an LV :returns: size and measurement of an LV
:type: ``tuple`` :type: ``tuple``
""" """
vg = self._get_lxc_vg() vg = self._get_lxc_vg()
lv = os.path.join(vg, name) lv = os.path.join(vg, lv_name)
build_command = [ build_command = [
'lvdisplay', 'lvdisplay',
lv, lv,
@ -1080,7 +1234,7 @@ class LxcContainerManagement(object):
""" """
vg = self._get_lxc_vg() vg = self._get_lxc_vg()
free_space, messurement = self._get_vg_free_pe(name=vg) free_space, messurement = self._get_vg_free_pe(vg_name=vg)
if free_space < float(snapshot_size_gb): if free_space < float(snapshot_size_gb):
message = ( message = (
@ -1183,25 +1337,25 @@ class LxcContainerManagement(object):
return archive_name return archive_name
def _lvm_lv_remove(self, name): def _lvm_lv_remove(self, lv_name):
"""Remove an LV. """Remove an LV.
:param name: The name of the logical volume :param lv_name: The name of the logical volume
:type name: ``str`` :type lv_name: ``str``
""" """
vg = self._get_lxc_vg() vg = self._get_lxc_vg()
build_command = [ build_command = [
self.module.get_bin_path('lvremove', True), self.module.get_bin_path('lvremove', True),
"-f", "-f",
"%s/%s" % (vg, name), "%s/%s" % (vg, lv_name),
] ]
rc, stdout, err = self._run_command(build_command) rc, stdout, err = self._run_command(build_command)
if rc != 0: if rc != 0:
self.failure( self.failure(
err=err, err=err,
rc=rc, rc=rc,
msg='Failed to remove LVM LV %s/%s' % (vg, name), msg='Failed to remove LVM LV %s/%s' % (vg, lv_name),
command=' '.join(build_command) command=' '.join(build_command)
) )
@ -1213,21 +1367,34 @@ class LxcContainerManagement(object):
:param temp_dir: path to the temporary local working directory :param temp_dir: path to the temporary local working directory
:type temp_dir: ``str`` :type temp_dir: ``str``
""" """
# This loop is created to support overlayfs archives. This should
# squash all of the layers into a single archive.
fs_paths = container_path.split(':')
if 'overlayfs' in fs_paths:
fs_paths.pop(fs_paths.index('overlayfs'))
build_command = [ for fs_path in fs_paths:
self.module.get_bin_path('rsync', True), # Set the path to the container data
'-aHAX', fs_path = os.path.dirname(fs_path)
container_path,
temp_dir # Run the sync command
] build_command = [
rc, stdout, err = self._run_command(build_command, unsafe_shell=True) self.module.get_bin_path('rsync', True),
if rc != 0: '-aHAX',
self.failure( fs_path,
err=err, temp_dir
rc=rc, ]
msg='failed to perform archive', rc, stdout, err = self._run_command(
command=' '.join(build_command) build_command,
unsafe_shell=True
) )
if rc != 0:
self.failure(
err=err,
rc=rc,
msg='failed to perform archive',
command=' '.join(build_command)
)
def _unmount(self, mount_point): def _unmount(self, mount_point):
"""Unmount a file system. """Unmount a file system.
@ -1249,6 +1416,33 @@ class LxcContainerManagement(object):
command=' '.join(build_command) command=' '.join(build_command)
) )
def _overlayfs_mount(self, lowerdir, upperdir, mount_point):
"""mount an lv.
:param lowerdir: name/path of the lower directory
:type lowerdir: ``str``
:param upperdir: name/path of the upper directory
:type upperdir: ``str``
:param mount_point: path on the file system that is mounted.
:type mount_point: ``str``
"""
build_command = [
self.module.get_bin_path('mount', True),
'-t overlayfs',
'-o lowerdir=%s,upperdir=%s' % (lowerdir, upperdir),
'overlayfs',
mount_point,
]
rc, stdout, err = self._run_command(build_command)
if rc != 0:
self.failure(
err=err,
rc=rc,
msg='failed to mount overlayfs:%s:%s to %s -- Command: %s'
% (lowerdir, upperdir, mount_point, build_command)
)
def _container_create_tar(self): def _container_create_tar(self):
"""Create a tar archive from an LXC container. """Create a tar archive from an LXC container.
@ -1275,13 +1469,15 @@ class LxcContainerManagement(object):
# Test if the containers rootfs is a block device # Test if the containers rootfs is a block device
block_backed = lxc_rootfs.startswith(os.path.join(os.sep, 'dev')) block_backed = lxc_rootfs.startswith(os.path.join(os.sep, 'dev'))
# Test if the container is using overlayfs
overlayfs_backed = lxc_rootfs.startswith('overlayfs')
mount_point = os.path.join(work_dir, 'rootfs') mount_point = os.path.join(work_dir, 'rootfs')
# Set the snapshot name if needed # Set the snapshot name if needed
snapshot_name = '%s_lxc_snapshot' % self.container_name snapshot_name = '%s_lxc_snapshot' % self.container_name
# Set the path to the container data
container_path = os.path.dirname(lxc_rootfs)
container_state = self._get_state() container_state = self._get_state()
try: try:
# Ensure the original container is stopped or frozen # Ensure the original container is stopped or frozen
@ -1292,7 +1488,7 @@ class LxcContainerManagement(object):
self.container.stop() self.container.stop()
# Sync the container data from the container_path to work_dir # Sync the container data from the container_path to work_dir
self._rsync_data(container_path, temp_dir) self._rsync_data(lxc_rootfs, temp_dir)
if block_backed: if block_backed:
if snapshot_name not in self._lvm_lv_list(): if snapshot_name not in self._lvm_lv_list():
@ -1301,7 +1497,7 @@ class LxcContainerManagement(object):
# Take snapshot # Take snapshot
size, measurement = self._get_lv_size( size, measurement = self._get_lv_size(
name=self.container_name lv_name=self.container_name
) )
self._lvm_snapshot_create( self._lvm_snapshot_create(
source_lv=self.container_name, source_lv=self.container_name,
@ -1322,6 +1518,25 @@ class LxcContainerManagement(object):
' up old snapshot of containers before continuing.' ' up old snapshot of containers before continuing.'
% snapshot_name % snapshot_name
) )
elif overlayfs_backed:
lowerdir, upperdir = lxc_rootfs.split(':')[1:]
self._overlayfs_mount(
lowerdir=lowerdir,
upperdir=upperdir,
mount_point=mount_point
)
# Set the state as changed and set a new fact
self.state_change = True
return self._create_tar(source_dir=work_dir)
finally:
if block_backed or overlayfs_backed:
# unmount snapshot
self._unmount(mount_point)
if block_backed:
# Remove snapshot
self._lvm_lv_remove(snapshot_name)
# Restore original state of container # Restore original state of container
if container_state == 'running': if container_state == 'running':
@ -1330,17 +1545,6 @@ class LxcContainerManagement(object):
else: else:
self.container.start() self.container.start()
# Set the state as changed and set a new fact
self.state_change = True
return self._create_tar(source_dir=work_dir)
finally:
if block_backed:
# unmount snapshot
self._unmount(mount_point)
# Remove snapshot
self._lvm_lv_remove(snapshot_name)
# Remove tmpdir # Remove tmpdir
shutil.rmtree(temp_dir) shutil.rmtree(temp_dir)
@ -1450,6 +1654,14 @@ def main():
choices=[n for i in LXC_LOGGING_LEVELS.values() for n in i], choices=[n for i in LXC_LOGGING_LEVELS.values() for n in i],
default='INFO' default='INFO'
), ),
clone_name=dict(
type='str',
required=False
),
clone_snapshot=dict(
choices=BOOLEANS,
default='false'
),
archive=dict( archive=dict(
choices=BOOLEANS, choices=BOOLEANS,
default='false' default='false'
@ -1477,4 +1689,3 @@ def main():
# import module bits # import module bits
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
main() main()