214 lines
7.2 KiB
Python
214 lines
7.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
LXC_ROOT = '/var/lib/lxc'
|
|
CONFIG_TEMPLATE = '''# Image name
|
|
lxc.uts.name = {name}
|
|
|
|
# Network
|
|
lxc.net.0.type = veth
|
|
lxc.net.0.link = lxcbr0
|
|
lxc.net.0.flags = up
|
|
|
|
# Volumes
|
|
lxc.rootfs.path = {rootfs}
|
|
|
|
# Mounts
|
|
lxc.mount.entry = shm dev/shm tmpfs rw,nodev,noexec,nosuid,relatime,mode=1777,create=dir 0 0
|
|
lxc.mount.entry = /etc/hosts etc/hosts none bind,create=file 0 0
|
|
lxc.mount.entry = /etc/resolv.conf etc/resolv.conf none bind,create=file 0 0
|
|
{mounts}
|
|
|
|
# Init
|
|
lxc.init.uid = {uid}
|
|
lxc.init.gid = {gid}
|
|
lxc.init.cwd = {cwd}
|
|
|
|
# Environment
|
|
lxc.environment = PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
{env}
|
|
|
|
# Halt
|
|
lxc.signal.halt = {halt}
|
|
|
|
# Log
|
|
lxc.console.size = 1MB
|
|
lxc.console.logfile = /var/log/lxc/{name}.log
|
|
|
|
# Other
|
|
lxc.arch = x86_64
|
|
lxc.cap.drop = sys_admin
|
|
lxc.hook.pre-start = /usr/bin/vmmgr prepare-container
|
|
lxc.hook.start-host = /usr/bin/vmmgr register-container
|
|
lxc.hook.post-stop = /usr/bin/vmmgr unregister-container
|
|
lxc.include = /usr/share/lxc/config/common.conf
|
|
'''
|
|
|
|
class LXCBuilder:
|
|
def __init__(self, image):
|
|
self.image = image
|
|
self.script = []
|
|
self.script_eof = None
|
|
self.already_built = False
|
|
|
|
def build(self):
|
|
with open(self.image.lxcfile, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if self.script_eof:
|
|
if line == self.script_eof:
|
|
self.script_eof = None
|
|
self.run_script(self.script)
|
|
else:
|
|
self.script.append(line)
|
|
elif line:
|
|
self.process_line(*line.split(None, 1))
|
|
|
|
def process_line(self, directive, args):
|
|
if 'RUN' == directive:
|
|
self.script = []
|
|
self.script_eof = args
|
|
elif 'IMAGE' == directive:
|
|
self.set_name(*args.split())
|
|
elif 'META' == directive:
|
|
self.add_meta(*args.split(None, 1))
|
|
elif 'LAYER' == directive:
|
|
self.add_layer(*args.split())
|
|
elif 'FIXLAYER' == directive:
|
|
self.fix_layer(args.split())
|
|
elif 'COPY' == directive:
|
|
srcdst = args.split()
|
|
self.copy_files(srcdst[0], srcdst[1] if len(srcdst) == 2 else '')
|
|
elif 'MOUNT' == directive:
|
|
self.add_mount(args.split())
|
|
elif 'ENV' == directive:
|
|
self.add_env(*args.split(None, 1))
|
|
elif 'USER' == directive:
|
|
self.set_user(*args.split())
|
|
elif 'CMD' == directive:
|
|
self.set_cmd(args)
|
|
elif 'WORKDIR' == directive:
|
|
self.set_cwd(args)
|
|
elif 'HALT' == directive:
|
|
self.set_halt(args)
|
|
|
|
def get_layer_path(self, layer):
|
|
return os.path.join(LXC_ROOT, 'storage', layer)
|
|
|
|
def rebuild_config(self):
|
|
if not self.image.upper_layer:
|
|
return
|
|
upper_layer = self.get_layer_path(self.image.upper_layer)
|
|
if not self.image.layers:
|
|
rootfs = upper_layer
|
|
else:
|
|
# Multiple lower overlayfs layers are ordered from right to left (lower2:lower1:rootfs:upper)
|
|
layers = [self.get_layer_path(layer) for layer in self.image.layers]
|
|
rootfs = 'overlay:{}:{}'.format(':'.join(layers[::-1]), upper_layer)
|
|
mounts = '\n'.join(['lxc.mount.entry = {} {} none bind,create={} 0 0'.format(m[1], m[2], m[0].lower()) for m in self.image.mounts])
|
|
env = '\n'.join(['lxc.environment = {}={}'.format(e[0], e[1]) for e in self.image.env])
|
|
cwd = self.image.cwd if self.image.cwd else '/'
|
|
halt = self.image.halt if self.image.halt else 'SIGINT'
|
|
with open(os.path.join(LXC_ROOT, self.image.upper_layer, 'config'), 'w') as f:
|
|
f.write(CONFIG_TEMPLATE.format(name=self.image.upper_layer, rootfs=rootfs, mounts=mounts, env=env, uid=self.image.uid, gid=self.image.gid, cwd=cwd, halt=halt))
|
|
|
|
def run_script(self, script):
|
|
if self.already_built:
|
|
return
|
|
sh = os.path.join(self.get_layer_path(self.image.upper_layer), 'run.sh')
|
|
with open(sh, 'w') as f:
|
|
f.write('#!/bin/sh\nset -ev\n\n{}\n'.format('\n'.join(script)))
|
|
os.chmod(sh, 0o700)
|
|
subprocess.run(['lxc-execute', '-n', self.image.upper_layer, '--', '/bin/sh', '-lc', '/run.sh'], check=True)
|
|
os.unlink(sh)
|
|
|
|
def set_name(self, name, version):
|
|
self.image.name = name
|
|
self.image.version = version
|
|
self.image.upper_layer = '{}_{}'.format(self.image.name, self.image.version)
|
|
layer_path = self.get_layer_path(self.image.upper_layer)
|
|
if os.path.exists(layer_path):
|
|
self.already_built = True
|
|
print('Layer {} already exists, skipping build tasks'.format(self.image.upper_layer))
|
|
else:
|
|
os.makedirs(layer_path, 0o755, True)
|
|
os.makedirs(os.path.join(LXC_ROOT, self.image.upper_layer), 0o755, True)
|
|
self.rebuild_config()
|
|
|
|
def add_meta(self, key, value):
|
|
self.image.meta[key] = value
|
|
|
|
def add_layer(self, name, version):
|
|
self.image.layers.append('{}_{}'.format(name, version))
|
|
self.rebuild_config()
|
|
|
|
def fix_layer(self, cmd):
|
|
if self.already_built:
|
|
return
|
|
layers = [self.get_layer_path(layer) for layer in self.image.layers]
|
|
layers.append(self.get_layer_path(self.image.upper_layer))
|
|
subprocess.run([cmd]+layers, check=True)
|
|
|
|
def copy_files(self, src, dst):
|
|
if self.already_built:
|
|
return
|
|
dst = os.path.join(self.get_layer_path(self.image.upper_layer), dst)
|
|
if src.startswith('http://') or src.startswith('https://'):
|
|
unpack_http_archive(src, dst)
|
|
else:
|
|
src = os.path.join(self.image.build_dir, src)
|
|
copy_tree(src, dst)
|
|
|
|
def add_mount(self, args):
|
|
self.image.mounts.append(args)
|
|
if not self.already_built:
|
|
self.rebuild_config()
|
|
|
|
def add_env(self, args):
|
|
self.image.env.append(args)
|
|
if not self.already_built:
|
|
self.rebuild_config()
|
|
|
|
def set_user(self, uid, gid):
|
|
self.image.uid = uid
|
|
self.image.gid = gid
|
|
if not self.already_built:
|
|
self.rebuild_config()
|
|
|
|
def set_cmd(self, cmd):
|
|
self.image.cmd = cmd
|
|
|
|
def set_cwd(self, cwd):
|
|
self.image.cwd = cwd
|
|
if not self.already_built:
|
|
self.rebuild_config()
|
|
|
|
def set_halt(self, halt):
|
|
self.image.halt = halt
|
|
if not self.already_built:
|
|
self.rebuild_config()
|
|
|
|
def unpack_http_archive(src, dst):
|
|
xf = 'xzf'
|
|
if src.endswith('.bz2'):
|
|
xf = 'xjf'
|
|
elif src.endswith('.xz'):
|
|
xf = 'xJf'
|
|
with subprocess.Popen(['wget', src, '-O', '-'], stdout=subprocess.PIPE) as wget:
|
|
with subprocess.Popen(['tar', xf, '-', '-C', dst], stdin=wget.stdout) as tar:
|
|
wget.stdout.close()
|
|
tar.wait()
|
|
|
|
def copy_tree(src, dst):
|
|
if not os.path.isdir(src):
|
|
shutil.copy2(src, dst)
|
|
else:
|
|
os.makedirs(dst, exist_ok=True)
|
|
for name in os.listdir(src):
|
|
copy_tree(os.path.join(src, name), os.path.join(dst, name))
|
|
shutil.copystat(src, dst)
|