119 lines
5.3 KiB
Python
119 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)
|
|
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
|