# -*- 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)