vmmgr/usr/lib/python3.6/lxcmgr/lxcmgr.py

120 lines
5.3 KiB
Python

# -*- 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)
layers = layers.split(',')
mount_rootfs(container, layers, 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[0], ':'.join(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