2019-06-25 15:56:35 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
import os
|
|
|
|
import shutil
|
|
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
|
2019-09-20 10:13:41 +02:00
|
|
|
from lxcmgr import lxcmgr
|
2019-09-20 15:43:01 +02:00
|
|
|
from lxcmgr.paths import LXC_STORAGE_DIR
|
2019-06-25 15:56:35 +02:00
|
|
|
|
2019-09-20 10:13:41 +02:00
|
|
|
class ImageExistsError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class ImageNotFoundError(Exception):
|
|
|
|
pass
|
|
|
|
|
2019-10-05 22:26:54 +02:00
|
|
|
class ImageBuilder:
|
|
|
|
def __init__(self, image):
|
|
|
|
self.image = image
|
2019-06-25 15:56:35 +02:00
|
|
|
self.script = []
|
|
|
|
self.script_eof = None
|
|
|
|
|
2019-10-05 22:26:54 +02:00
|
|
|
def build(self):
|
2019-06-25 15:56:35 +02:00
|
|
|
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:
|
2019-09-18 11:29:58 +02:00
|
|
|
self.set_name(args)
|
2019-06-25 15:56:35 +02:00
|
|
|
elif 'LAYER' == directive:
|
2019-09-18 11:29:58 +02:00
|
|
|
self.add_layer(args)
|
2019-11-17 15:04:34 +01:00
|
|
|
elif 'MERGE' == directive:
|
|
|
|
self.merge_layers(args.split())
|
2019-06-25 15:56:35 +02:00
|
|
|
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)
|
2019-09-18 11:29:58 +02:00
|
|
|
elif 'READY' == directive:
|
|
|
|
self.set_ready(args)
|
2019-06-25 15:56:35 +02:00
|
|
|
|
|
|
|
def get_layer_path(self, layer):
|
2019-09-20 15:43:01 +02:00
|
|
|
return os.path.join(LXC_STORAGE_DIR, layer)
|
2019-06-25 15:56:35 +02:00
|
|
|
|
|
|
|
def run_script(self, script):
|
2019-09-20 15:43:01 +02:00
|
|
|
lxcmgr.create_container(self.image.name, self.image.conf)
|
2019-10-05 22:26:54 +02:00
|
|
|
sh = os.path.join(LXC_STORAGE_DIR, self.image.name, 'run.sh')
|
2019-06-25 15:56:35 +02:00
|
|
|
with open(sh, 'w') as f:
|
|
|
|
f.write('#!/bin/sh\nset -ev\n\n{}\n'.format('\n'.join(script)))
|
|
|
|
os.chmod(sh, 0o700)
|
2019-09-18 11:29:58 +02:00
|
|
|
os.chown(sh, 100000, 100000)
|
|
|
|
subprocess.run(['lxc-execute', self.image.name, '--', '/bin/sh', '-lc', '/run.sh'], check=True)
|
2019-06-25 15:56:35 +02:00
|
|
|
os.unlink(sh)
|
2019-10-05 22:26:54 +02:00
|
|
|
if not self.image.scratch_build:
|
|
|
|
lxcmgr.destroy_container(self.image.name)
|
2019-06-25 15:56:35 +02:00
|
|
|
|
2019-09-18 11:29:58 +02:00
|
|
|
def set_name(self, name):
|
2019-06-25 15:56:35 +02:00
|
|
|
self.image.name = name
|
2019-09-20 10:13:41 +02:00
|
|
|
self.image.conf['layers'] = [name]
|
2019-10-05 22:26:54 +02:00
|
|
|
image_path = self.get_layer_path(name)
|
|
|
|
if os.path.exists(image_path):
|
|
|
|
if self.image.force_build:
|
2019-09-20 10:13:41 +02:00
|
|
|
self.clean()
|
|
|
|
else:
|
2019-10-05 22:26:54 +02:00
|
|
|
raise ImageExistsError(image_path)
|
|
|
|
os.makedirs(image_path, 0o755, True)
|
|
|
|
os.chown(image_path, 100000, 100000)
|
2019-06-25 15:56:35 +02:00
|
|
|
|
2019-09-18 11:29:58 +02:00
|
|
|
def add_layer(self, name):
|
2019-09-20 10:13:41 +02:00
|
|
|
layer_path = self.get_layer_path(name)
|
|
|
|
if not os.path.exists(layer_path):
|
|
|
|
raise ImageNotFoundError(layer_path)
|
2019-09-24 19:15:22 +02:00
|
|
|
self.image.conf['layers'].insert(1, name)
|
2019-06-25 15:56:35 +02:00
|
|
|
|
2019-11-17 15:04:34 +01:00
|
|
|
def merge_layers(self, cmd):
|
2019-09-18 11:29:58 +02:00
|
|
|
layers = [self.get_layer_path(layer) for layer in self.image.conf['layers']]
|
2019-10-03 12:13:39 +02:00
|
|
|
subprocess.run(cmd + layers, check=True)
|
2019-06-25 15:56:35 +02:00
|
|
|
|
|
|
|
def copy_files(self, src, dst):
|
2019-10-05 22:26:54 +02:00
|
|
|
dst = os.path.join(LXC_STORAGE_DIR, self.image.name, dst)
|
2019-06-25 15:56:35 +02:00
|
|
|
if src.startswith('http://') or src.startswith('https://'):
|
|
|
|
unpack_http_archive(src, dst)
|
|
|
|
else:
|
2019-09-20 10:13:41 +02:00
|
|
|
copy_tree(os.path.join(self.image.build_dir, src), dst)
|
2019-09-18 11:29:58 +02:00
|
|
|
shift_uid(dst)
|
2019-06-25 15:56:35 +02:00
|
|
|
|
2019-09-20 10:13:41 +02:00
|
|
|
def add_env(self, key, value):
|
2019-09-18 11:29:58 +02:00
|
|
|
if 'env' not in self.image.conf:
|
|
|
|
self.image.conf['env'] = []
|
2019-10-03 12:13:39 +02:00
|
|
|
self.image.conf['env'].append([key, value])
|
2019-06-25 15:56:35 +02:00
|
|
|
|
|
|
|
def set_user(self, uid, gid):
|
2019-09-18 11:29:58 +02:00
|
|
|
self.image.conf['uid'] = uid
|
|
|
|
self.image.conf['gid'] = gid
|
2019-06-25 15:56:35 +02:00
|
|
|
|
|
|
|
def set_cmd(self, cmd):
|
2019-09-18 11:29:58 +02:00
|
|
|
self.image.conf['cmd'] = cmd
|
2019-06-25 15:56:35 +02:00
|
|
|
|
|
|
|
def set_cwd(self, cwd):
|
2019-09-18 11:29:58 +02:00
|
|
|
self.image.conf['cwd'] = cwd
|
2019-06-25 15:56:35 +02:00
|
|
|
|
|
|
|
def set_halt(self, halt):
|
2019-09-18 11:29:58 +02:00
|
|
|
self.image.conf['halt'] = halt
|
|
|
|
|
|
|
|
def set_ready(self, cmd):
|
|
|
|
self.image.conf['ready'] = cmd
|
2019-06-25 15:56:35 +02:00
|
|
|
|
2019-09-20 10:13:41 +02:00
|
|
|
def clean(self):
|
2019-09-24 19:15:22 +02:00
|
|
|
lxcmgr.destroy_container(self.image.name)
|
2019-10-05 22:26:54 +02:00
|
|
|
shutil.rmtree(self.get_layer_path(self.image.name))
|
2019-09-20 10:13:41 +02:00
|
|
|
|
2019-06-25 15:56:35 +02:00
|
|
|
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)
|
2019-09-18 11:29:58 +02:00
|
|
|
|
|
|
|
def shift_uid(dir):
|
|
|
|
shift_uid_entry(dir, os.stat(dir, follow_symlinks=True))
|
|
|
|
shift_uid_recursively(dir)
|
|
|
|
|
|
|
|
def shift_uid_recursively(dir):
|
|
|
|
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):
|
|
|
|
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)
|