Merge pull request #328 from cloudnull/lxc-overlayfs-feature

Added overlayfs backend type to the lxc_container module
This commit is contained in:
Brian Coca 2015-05-13 14:01:23 -04:00
commit 56f466c70d

View file

@ -38,6 +38,7 @@ options:
- lvm
- loop
- btrfs
- overlayfs
description:
- Backend storage type for the container.
required: false
@ -112,6 +113,20 @@ options:
- Set the log level for a container where *container_log* was set.
required: false
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:
choices:
- true
@ -141,8 +156,12 @@ options:
- restarted
- absent
- frozen
- clone
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
default: started
container_config:
@ -298,6 +317,47 @@ EXAMPLES = """
archive: true
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.
lxc_container:
name: "{{ item }}"
@ -308,6 +368,9 @@ EXAMPLES = """
- test-container-frozen
- test-container-lvm
- test-container-config
- test-container-overlayfs
- test-container-clone
- test-container-overlayfs-clone
"""
@ -354,6 +417,15 @@ LXC_COMMAND_MAP = {
'directory': '--dir',
'zfs_root': '--zfsroot'
}
},
'clone': {
'variables': {
'backing_store': '--backingstore',
'lxc_path': '--lxcpath',
'fs_size': '--fssize',
'name': '--orig',
'clone_name': '--new'
}
}
}
@ -372,6 +444,9 @@ LXC_BACKING_STORE = {
],
'loop': [
'lv_name', 'vg_name', 'thinpool', 'zfs_root'
],
'overlayfs': [
'lv_name', 'vg_name', 'fs_type', 'fs_size', 'thinpool', 'zfs_root'
]
}
@ -391,7 +466,8 @@ LXC_ANSIBLE_STATES = {
'stopped': '_stopped',
'restarted': '_restarted',
'absent': '_destroyed',
'frozen': '_frozen'
'frozen': '_frozen',
'clone': '_clone'
}
@ -505,15 +581,15 @@ class LxcContainerManagement(object):
return num
@staticmethod
def _container_exists(name):
def _container_exists(container_name):
"""Check if a container exists.
:param name: Name of the container.
:param container_name: Name of the container.
:type: ``str``
:returns: True or False if the container is found.
: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
else:
return False
@ -546,6 +622,7 @@ class LxcContainerManagement(object):
"""
# Remove incompatible storage backend options.
variables = variables.copy()
for v in LXC_BACKING_STORE[self.module.params['backing_store']]:
variables.pop(v, None)
@ -658,6 +735,83 @@ class LxcContainerManagement(object):
self._container_startup()
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):
"""Create a new LXC container.
@ -712,9 +866,9 @@ class LxcContainerManagement(object):
rc, return_data, err = self._run_command(build_command)
if rc != 0:
msg = "Failed executing lxc-create."
message = "Failed executing lxc-create."
self.failure(
err=err, rc=rc, msg=msg, command=' '.join(build_command)
err=err, rc=rc, msg=message, command=' '.join(build_command)
)
else:
self.state_change = True
@ -754,7 +908,7 @@ class LxcContainerManagement(object):
: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()
else:
return str('absent')
@ -819,7 +973,7 @@ class LxcContainerManagement(object):
"""
for _ in xrange(timeout):
if not self._container_exists(name=self.container_name):
if not self._container_exists(container_name=self.container_name):
break
# Check if the container needs to have an archive created.
@ -855,7 +1009,7 @@ class LxcContainerManagement(object):
"""
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()
# Perform any configuration updates
@ -889,7 +1043,7 @@ class LxcContainerManagement(object):
"""
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()
# Perform any configuration updates
@ -916,7 +1070,7 @@ class LxcContainerManagement(object):
"""
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()
# Perform any configuration updates
@ -943,7 +1097,7 @@ class LxcContainerManagement(object):
"""
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()
if container_state == 'running':
pass
@ -1010,18 +1164,18 @@ class LxcContainerManagement(object):
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):
def _get_vg_free_pe(self, vg_name):
"""Return the available size of a given VG.
:param name: Name of volume.
:type name: ``str``
:param vg_name: Name of volume.
:type vg_name: ``str``
:returns: size and measurement of an LV
:type: ``tuple``
"""
build_command = [
'vgdisplay',
name,
vg_name,
'--units',
'g'
]
@ -1030,7 +1184,7 @@ class LxcContainerManagement(object):
self.failure(
err=err,
rc=rc,
msg='failed to read vg %s' % name,
msg='failed to read vg %s' % vg_name,
command=' '.join(build_command)
)
@ -1039,17 +1193,17 @@ class LxcContainerManagement(object):
_free_pe = free_pe[0].split()
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.
:param name: Name of volume.
:type name: ``str``
:param lv_name: Name of volume.
:type lv_name: ``str``
:returns: size and measurement of an LV
:type: ``tuple``
"""
vg = self._get_lxc_vg()
lv = os.path.join(vg, name)
lv = os.path.join(vg, lv_name)
build_command = [
'lvdisplay',
lv,
@ -1083,7 +1237,7 @@ class LxcContainerManagement(object):
"""
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):
message = (
@ -1186,25 +1340,25 @@ class LxcContainerManagement(object):
return archive_name
def _lvm_lv_remove(self, name):
def _lvm_lv_remove(self, lv_name):
"""Remove an LV.
:param name: The name of the logical volume
:type name: ``str``
:param lv_name: The name of the logical volume
:type lv_name: ``str``
"""
vg = self._get_lxc_vg()
build_command = [
self.module.get_bin_path('lvremove', True),
"-f",
"%s/%s" % (vg, name),
"%s/%s" % (vg, lv_name),
]
rc, stdout, err = self._run_command(build_command)
if rc != 0:
self.failure(
err=err,
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)
)
@ -1216,14 +1370,27 @@ class LxcContainerManagement(object):
:param temp_dir: path to the temporary local working directory
: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'))
for fs_path in fs_paths:
# Set the path to the container data
fs_path = os.path.dirname(fs_path)
# Run the sync command
build_command = [
self.module.get_bin_path('rsync', True),
'-aHAX',
container_path,
fs_path,
temp_dir
]
rc, stdout, err = self._run_command(build_command, unsafe_shell=True)
rc, stdout, err = self._run_command(
build_command,
unsafe_shell=True
)
if rc != 0:
self.failure(
err=err,
@ -1252,6 +1419,33 @@ class LxcContainerManagement(object):
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):
"""Create a tar archive from an LXC container.
@ -1278,13 +1472,15 @@ class LxcContainerManagement(object):
# Test if the containers rootfs is a block device
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')
# Set the snapshot name if needed
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()
try:
# Ensure the original container is stopped or frozen
@ -1295,7 +1491,7 @@ class LxcContainerManagement(object):
self.container.stop()
# 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 snapshot_name not in self._lvm_lv_list():
@ -1304,7 +1500,7 @@ class LxcContainerManagement(object):
# Take snapshot
size, measurement = self._get_lv_size(
name=self.container_name
lv_name=self.container_name
)
self._lvm_snapshot_create(
source_lv=self.container_name,
@ -1325,6 +1521,25 @@ class LxcContainerManagement(object):
' up old snapshot of containers before continuing.'
% 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
if container_state == 'running':
@ -1333,17 +1548,6 @@ class LxcContainerManagement(object):
else:
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
shutil.rmtree(temp_dir)
@ -1453,6 +1657,14 @@ def main():
choices=[n for i in LXC_LOGGING_LEVELS.values() for n in i],
default='INFO'
),
clone_name=dict(
type='str',
required=False
),
clone_snapshot=dict(
choices=BOOLEANS,
default='false'
),
archive=dict(
choices=BOOLEANS,
default='false'
@ -1480,4 +1692,3 @@ def main():
# import module bits
from ansible.module_utils.basic import *
main()