# -*- coding: utf-8 -*- import fcntl import os import shutil import subprocess from . import flock from .paths import HOSTS_FILE, HOSTS_LOCK, LXC_LOGS, LXC_ROOT, LXC_STORAGE_DIR from .templates import LXC_CONTAINER def prepare_container(container, layers): # Remove ephemeral layer data clean_ephemeral_layer(container) # Prepare and mount overlayfs. This needs to be done before handing over control to LXC as we use unprivileged containers # which don't have the capability to mount overlays - https://www.spinics.net/lists/linux-fsdevel/msg105877.html rootfs = os.path.join(LXC_ROOT, container, 'rootfs') # Unmount rootfs in case it remained mounted for whatever reason unmount_rootfs(rootfs) mount_rootfs(container, layers.split(','), rootfs) def mount_rootfs(container, layers, mountpoint): if len(layers) == 1: # We have only single layer, no overlay needed subprocess.run(['mount', '--bind', layers[0], mountpoint]) else: olwork = os.path.join(LXC_ROOT, container, 'olwork') subprocess.run(['mount', '-t', 'overlay', '-o', 'upperdir={},lowerdir={},workdir={}'.format(layers[-1], ':'.join(reversed(layers[:-1])), olwork), 'none', mountpoint]) def unmount_rootfs(mountpoint): if os.path.exists(mountpoint): subprocess.run(['umount', '--quiet', mountpoint]) def clean_ephemeral_layer(container): # Cleans containers ephemeral layer. Called in lxc.hook.post-stop and lxc.hook.pre-start in case of unclean shutdown # This is done early in the container start process, so the inode of the ephemeral directory must remain unchanged ephemeral = os.path.join(LXC_ROOT, container, 'ephemeral') for item in os.scandir(ephemeral): shutil.rmtree(item.path) if item.is_dir() else os.unlink(item.path) def cleanup_container(container): # Unmount rootfs rootfs = os.path.join(LXC_ROOT, container, 'rootfs') unmount_rootfs(rootfs) # Remove ephemeral layer data clean_ephemeral_layer(container) def create_container(container, image): # Create directories after container installation rootfs = os.path.join(LXC_ROOT, container, 'rootfs') olwork = os.path.join(LXC_ROOT, container, 'olwork') ephemeral = os.path.join(LXC_ROOT, container, 'ephemeral') os.makedirs(rootfs, 0o755, True) os.makedirs(olwork, 0o755, True) os.makedirs(ephemeral, 0o755, True) os.chown(ephemeral, 100000, 100000) # Create container configuration file layers = ','.join([os.path.join(LXC_STORAGE_DIR, layer) for layer in image['layers']]) # Add ephemeral layer if the container is not created as part of build process if 'build' not in image: layers = '{},{}'.format(layers, ephemeral) mounts = '\n{}'.format('\n'.join(['lxc.mount.entry = {} {} none bind,create={} 0 0'.format(m[1], m[2].lstrip('/'), m[0].lower()) for m in image['mounts']])) if 'mounts' in image else '' env = '\n{}'.format('\n'.join(['lxc.environment = {}={}'.format(e[0], e[1]) for e in image['env']])) if 'env' in image else '' uid = image['uid'] if 'uid' in image else '0' gid = image['gid'] if 'gid' in image else '0' cmd = image['cmd'] if 'cmd' in image else '/bin/sh' cwd = image['cwd'] if 'cwd' in image else '/root' halt = image['halt'] if 'halt' in image else 'SIGINT' # Lease the first unused IP to the container ipv4 = update_hosts_lease(container, True) # Create the config file with open(os.path.join(LXC_ROOT, container, 'config'), 'w') as f: f.write(LXC_CONTAINER.format(name=container, ipv4=ipv4, layers=layers, mounts=mounts, env=env, uid=uid, gid=gid, cmd=cmd, cwd=cwd, halt=halt)) def destroy_container(container): # Remove container configuration and directories rootfs = os.path.join(LXC_ROOT, container, 'rootfs') unmount_rootfs(rootfs) try: shutil.rmtree(os.path.join(LXC_ROOT, container)) except FileNotFoundError: pass try: os.unlink(os.path.join(LXC_LOGS, '{}.log'.format(container))) except FileNotFoundError: pass # Release the IP address update_hosts_lease(container, False) @flock.flock_ex(HOSTS_LOCK) def update_hosts_lease(container, is_request): # This is a poor man's DHCP server which uses /etc/hosts as lease database # Leases the first unused IP from range 172.17.0.0/16 # Uses file lock as interprocess mutex ip = None # Load all existing records with open(HOSTS_FILE, 'r') as f: leases = [l.strip().split(' ', 1) for l in f] # If this call is a request for lease, find if there isn't already existing lease for the container if is_request: already_leased = [l[0] for l in leases if l[1] == container] if already_leased: return already_leased[0] # Otherwise assign the first unassigned IP used_ips = [l[0] for l in leases] for i in range(2, 65278): # Reserve last /24 subnet for VPN ip = '172.17.{}.{}'. format(i // 256, i % 256) if ip not in used_ips: leases.append([ip, container]) break # Otherwise it is a release in which case we just delete the record else: leases = [l for l in leases if l[1] != container] # Write the contents back to the file with open(HOSTS_FILE, 'w') as f: for lease in leases: f.write('{} {}\n'.format(lease[0], lease[1])) return ip