165 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			165 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/python3
 | |
| 
 | |
| 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
 | |
| lxc.net.0.ipv4.address = 172.17.0.254/16
 | |
| lxc.net.0.ipv4.gateway = auto
 | |
| 
 | |
| # Volumes
 | |
| lxc.rootfs.path = {rootfs}
 | |
| 
 | |
| # Mounts
 | |
| lxc.mount.entry = /etc/hosts etc/hosts none bind 0 0
 | |
| lxc.mount.entry = /etc/resolv.conf etc/resolv.conf none bind 0 0
 | |
| {mounts}
 | |
| 
 | |
| # Init
 | |
| lxc.init.cmd = {cmd}
 | |
| lxc.init.uid = {uid}
 | |
| lxc.init.gid = {gid}
 | |
| 
 | |
| # Halt
 | |
| lxc.signal.halt = SIGTERM
 | |
| 
 | |
| # Other
 | |
| lxc.arch = x86_64
 | |
| lxc.cap.drop = sys_admin
 | |
| lxc.include = /usr/share/lxc/config/alpine.common.conf
 | |
| '''
 | |
| 
 | |
| class LXCImage:
 | |
|     def __init__(self, build_path):
 | |
|         self.name = None
 | |
|         self.layers = []
 | |
|         self.mounts = []
 | |
|         self.uid = 0
 | |
|         self.gid = 0
 | |
|         self.cmd = '/bin/true'
 | |
| 
 | |
|         if os.path.isfile(build_path):
 | |
|             self.lxcfile = os.path.realpath(build_path)
 | |
|             self.build_dir = os.path.dirname(self.lxcfile)
 | |
|         else:
 | |
|             self.build_dir = os.path.realpath(build_path)
 | |
|             self.lxcfile = os.path.join(self.build_dir, 'lxcfile')
 | |
| 
 | |
|     def build(self):
 | |
|         with open(self.lxcfile, 'r') as fd:
 | |
|             lxcfile = [l.strip() for l in fd.readlines()]
 | |
| 
 | |
|         in_script = False
 | |
|         script = []
 | |
| 
 | |
|         for line in lxcfile:
 | |
|             if line == 'RUN':
 | |
|                 in_script = False
 | |
|                 self.run_script(script)
 | |
|             elif in_script:
 | |
|                 script.append(line)
 | |
|             elif line == 'SCRIPT':
 | |
|                 script = []
 | |
|                 in_script = True
 | |
|             elif line.startswith('IMAGE'):
 | |
|                 self.set_name(line.split()[1])
 | |
|             elif line.startswith('LAYER'):
 | |
|                 self.add_layer(line.split()[1])
 | |
|             elif line.startswith('COPY'):
 | |
|                 srcdst = line.split()
 | |
|                 self.copy_files(srcdst[1], srcdst[2] if len(srcdst) == 3 else '')
 | |
|             elif line.startswith('MOUNT'):
 | |
|                 srcdst = line.split()
 | |
|                 self.add_mount(srcdst[1], srcdst[2])
 | |
|             elif line.startswith('USER'):
 | |
|                 uidgid = line.split()
 | |
|                 self.set_user(uidgid[1], uidgid[2])
 | |
|             elif line.startswith('CMD'):
 | |
|                 self.set_cmd(' '.join(line.split()[1:]))
 | |
|         # Add the final layer which can be treated as nonpersistent
 | |
|         self.add_layer('{}/delta0'.format(self.name))
 | |
| 
 | |
|     def rebuild_config(self):
 | |
|         if len(self.layers) == 1:
 | |
|             rootfs_path = self.layers[0]
 | |
|         else:
 | |
|             # Multiple lower overlayfs layers are ordered from right to left (lower2:lower1:rootfs:upper)
 | |
|             rootfs_path = 'overlay:{}:{}'.format(':'.join(self.layers[:-1][::-1]), self.layers[-1])
 | |
|         mount_entries = '\n'.join(['lxc.mount.entry = {} none bind 0 0'.format(m) for m in self.mounts])
 | |
|         with open(os.path.join(LXC_ROOT, self.name, 'config'), 'w') as fd:
 | |
|             fd.write(CONFIG_TEMPLATE.format(name=self.name, rootfs=rootfs_path, mounts=mount_entries, uid=self.uid, gid=self.gid, cmd=self.cmd))
 | |
| 
 | |
|     def run_script(self, script):
 | |
|         sh = os.path.join(self.layers[-1], 'run.sh')
 | |
|         with open(sh, 'w') as fd:
 | |
|             fd.write('#!/bin/sh\nset -ev\n\n{}\n'.format('\n'.join(script)))
 | |
|         os.chmod(sh, 0o700)
 | |
|         subprocess.run(['lxc-execute', '-n', self.name, '--', '/bin/sh', '-lc', '/run.sh'], check=True)
 | |
|         os.unlink(sh)
 | |
| 
 | |
|     def set_name(self, name):
 | |
|         self.name = name
 | |
|         os.makedirs(os.path.join(LXC_ROOT, self.name), 0o755, True)
 | |
| 
 | |
|     def add_layer(self, layer):
 | |
|         layer = os.path.join(LXC_ROOT, layer)
 | |
|         self.layers.append(layer)
 | |
|         os.makedirs(layer, 0o755, True)
 | |
|         self.rebuild_config()
 | |
| 
 | |
|     def copy_files(self, src, dst):
 | |
|         src = os.path.join(self.build_dir, src)
 | |
|         dst = os.path.join(self.layers[-1], dst)
 | |
|         copy_tree(src, dst)
 | |
| 
 | |
|     def add_mount(self, src, dst):
 | |
|         self.mounts.append('{} {}'.format(src, dst))
 | |
|         self.rebuild_config()
 | |
| 
 | |
|     def set_user(self, uid, gid):
 | |
|         self.uid = uid
 | |
|         self.gid = gid
 | |
|         self.rebuild_config()
 | |
| 
 | |
|     def set_cmd(self, cmd):
 | |
|         self.cmd = cmd
 | |
|         self.rebuild_config()
 | |
| 
 | |
| 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)
 | |
| 
 | |
| # def fix_world():
 | |
| #     world_items = []
 | |
| #     last_world = []
 | |
| #     for layer in layers[:-1]:
 | |
| #         with open(os.path.join(layer, 'etc/apk/world'), 'r') as fd:
 | |
| #             last_world = fd.read().splitlines()
 | |
| #             world_items.extend(last_world)
 | |
| #     world_items = sorted(set(world_items))
 | |
| #     if world_items != sorted(last_world):
 | |
| #         os.makedirs(os.path.join(layers[-1], 'etc/apk', 0o755, True))
 | |
| #         with open(os.path.join(layers[-1], 'etc/apk/world'), 'w') as fd:
 | |
| #             fd.writelines(world_items)
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     if len(sys.argv) != 2:
 | |
|         print('Usage: lxc-build <buildpath>\n  where the buildpath can be either specific lxcfile or a directory containing one')
 | |
|     else:
 | |
|         i = LXCImage(sys.argv[1])
 | |
|         i.build()
 |