Spotter-VM/build/usr/lib/python3.6/lxcbuild/imagebuilder.py

206 lines
7.0 KiB
Python

# -*- coding: utf-8 -*-
import os
import shutil
import subprocess
import sys
from enum import Enum
from lxcmgr import lxcmgr
from lxcmgr.paths import LXC_STORAGE_DIR
from lxcmgr.pkgmgr import PkgMgr
class ImageExistsError(Exception):
pass
class ImageNotFoundError(Exception):
pass
class BuildType(Enum):
NORMAL = 1
FORCE = 2
SCRATCH = 3
METADATA = 4
class ImageBuilder:
def __init__(self, image):
self.image = image
self.script = []
self.script_eof = None
def build(self):
# Read and process lines from lxcfile
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):
# Process directives from lxcfile
if 'RUN' == directive:
self.script = []
self.script_eof = args
elif 'IMAGE' == directive:
self.set_name(args)
elif 'FROM' == directive:
self.set_layers(args)
elif 'COPY' == directive:
srcdst = args.split()
self.copy_files(srcdst[0], srcdst[1] if len(srcdst) == 2 else '')
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)
elif 'READY' == directive:
self.set_ready(args)
def get_layer_path(self, layer):
return os.path.join(LXC_STORAGE_DIR, layer)
def run_script(self, script):
# Creates a temporary container, runs a script in its namespace, and stores the modifications as part of the image
if self.image.build_type == BuildType.METADATA:
# Don't run anything if we're building just metadata
return
lxcmgr.create_container(self.image.name, self.image.conf)
sh = os.path.join(LXC_STORAGE_DIR, self.image.name, '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)
os.chown(sh, 100000, 100000)
subprocess.run(['lxc-execute', self.image.name, '--', '/bin/sh', '-lc', '/run.sh'], check=True)
os.unlink(sh)
if not self.image.build_type == BuildType.SCRATCH:
# Don't delete the temporary container if we're doing scratch build
lxcmgr.destroy_container(self.image.name)
def set_name(self, name):
# Set name and first (topmost) layer of the image
self.image.name = name
self.image.conf['layers'] = [name]
if self.image.build_type == BuildType.METADATA:
# Don't check or create any directories if we're building just metadata
return
image_path = self.get_layer_path(name)
if os.path.exists(image_path):
if self.image.build_type in (BuildType.FORCE, BuildType.SCRATCH):
self.clean()
else:
raise ImageExistsError(image_path)
os.makedirs(image_path, 0o755, True)
os.chown(image_path, 100000, 100000)
def set_layers(self, image):
# Extend list of layers with the list of layers from parent image
# Raies an exception when IMAGE has no name
pkgmgr = PkgMgr()
self.image.conf['layers'].extend(pkgmgr.installed_packages['images'][image]['layers'])
def copy_files(self, src, dst):
# Copy files from the host or download them from a http(s) URL
if self.image.build_type == BuildType.METADATA:
# Don't copy anything if we're building just metadata
return
dst = os.path.join(LXC_STORAGE_DIR, self.image.name, dst)
if src.startswith('http://') or src.startswith('https://'):
unpack_http_archive(src, dst)
else:
copy_tree(os.path.join(self.image.build_dir, src), dst)
# Shift UID/GID of the files to the unprivileged range
shift_uid(dst)
def add_env(self, key, value):
# Sets lxc.environment records for the image
if 'env' not in self.image.conf:
self.image.conf['env'] = []
self.image.conf['env'].append([key, value])
def set_user(self, uid, gid):
# Sets lxc.init.uid/gid for the image
self.image.conf['uid'] = uid
self.image.conf['gid'] = gid
def set_cmd(self, cmd):
# Sets lxc.init.cmd for the image
self.image.conf['cmd'] = cmd
def set_cwd(self, cwd):
# Sets lxc.init.cwd for the image
self.image.conf['cwd'] = cwd
def set_halt(self, halt):
# Sets lxc.signal.halt for the image
self.image.conf['halt'] = halt
def set_ready(self, cmd):
# Sets a command performed in OpenRC start_post to check readiness of the container
self.image.conf['ready'] = cmd
def clean(self):
lxcmgr.destroy_container(self.image.name)
try:
shutil.rmtree(self.get_layer_path(self.image.name))
except FileNotFoundError:
pass
def unpack_http_archive(src, dst):
# Decompress an archive downloaded via http(s)
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):
# Copies files from the host
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)
def shift_uid(dir):
# Shifts UID/GID of a file or a directory and its contents to the unprivileged range
shift_uid_entry(dir, os.stat(dir, follow_symlinks=True))
shift_uid_recursively(dir)
def shift_uid_recursively(dir):
# Shifts UID/GID of a directory and its contents to the unprivileged range
for entry in os.scandir(dir):
shift_uid_entry(entry.path, entry.stat(follow_symlinks=False))
if entry.is_dir():
shift_uid_recursively(entry.path)
def shift_uid_entry(path, stat):
# Shifts UID/GID of a file or a directory to the unprivileged range
uid = stat.st_uid
gid = stat.st_gid
do_chown = False
if uid < 100000:
uid = uid + 100000
do_chown = True
if gid < 100000:
gid = gid + 100000
do_chown = True
if do_chown:
os.lchown(path, uid, gid)