diff options
author | Anthony G. Basile <blueness@gentoo.org> | 2015-06-30 22:15:35 -0400 |
---|---|---|
committer | Anthony G. Basile <blueness@gentoo.org> | 2015-06-30 22:15:35 -0400 |
commit | 8885d6a5b9035f2eb0c02e697dc0bfb45c8b5a7f (patch) | |
tree | 165b052003e580934854a5e9da1182cde1312d62 /grs | |
download | grss-8885d6a5b9035f2eb0c02e697dc0bfb45c8b5a7f.tar.gz grss-8885d6a5b9035f2eb0c02e697dc0bfb45c8b5a7f.tar.bz2 grss-8885d6a5b9035f2eb0c02e697dc0bfb45c8b5a7f.zip |
Initial commit.
Diffstat (limited to 'grs')
-rw-r--r-- | grs/Constants.py | 97 | ||||
-rw-r--r-- | grs/Daemon.py | 122 | ||||
-rw-r--r-- | grs/Execute.py | 50 | ||||
-rw-r--r-- | grs/Interpret.py | 200 | ||||
-rw-r--r-- | grs/Kernel.py | 102 | ||||
-rw-r--r-- | grs/Log.py | 46 | ||||
-rw-r--r-- | grs/MountDirectories.py | 120 | ||||
-rw-r--r-- | grs/Populate.py | 93 | ||||
-rw-r--r-- | grs/RunScript.py | 28 | ||||
-rw-r--r-- | grs/Seed.py | 72 | ||||
-rw-r--r-- | grs/Synchronize.py | 37 | ||||
-rw-r--r-- | grs/TarIt.py | 54 | ||||
-rw-r--r-- | grs/WorldConf.py | 119 | ||||
-rw-r--r-- | grs/__init__.py | 15 |
14 files changed, 1155 insertions, 0 deletions
diff --git a/grs/Constants.py b/grs/Constants.py new file mode 100644 index 0000000..501d79d --- /dev/null +++ b/grs/Constants.py @@ -0,0 +1,97 @@ +#!/use/bin/env python + +import os +import sys +import configparser +from copy import deepcopy + +CONFIG = '/etc/grs/systems.conf' + +class Constants(): + """ doc here + more doc + """ + + def __init__(self, configfile = CONFIG): + if not os.path.isfile(configfile): + sys.stderr.write('Configuration file %s not found\n' % configfile) + sys.exit(1) + self.config = configparser.ConfigParser(delimiters = ':', comment_prefixes = '#') + self.config.read(configfile) + + self.names = list(self.config.sections()) + + server = 'http://distfiles.gentoo.org/' + stagedir = 'gentoo/releases/amd64/autobuilds/current-stage3-amd64-uclibc-hardened/' + stagefile = 'stage3-amd64-uclibc-hardened-20150510.tar.bz2' + default_stage_uri = server + stagedir + stagefile + + space = { + 'nameserver' : '8.8.8.8', + 'repo_uri' : 'git://tweedledum.dyc.edu/grs', + 'stage_uri' : default_stage_uri, + 'libdir' : '/var/lib/grs/%s', + 'logfile' : '/var/log/grs/%s.log', + 'tmpdir' : '/var/tmp/grs/%s', + 'workdir' : '/var/tmp/grs/%s/work', + 'package' : '/var/tmp/grs/%s/packages', + 'kernelroot' : '/var/tmp/grs/%s/kernel', + 'portage_configroot' : '/var/tmp/grs/%s/system', + 'pidfile' : '/run/grs-%s.pid' + } + + for key in space: + self.__dict__[key+'s'] = [] + + for section in self.config.sections(): + overrides = dict(self.config[section].items()) + + for key in space: + if key in overrides: + value = overrides[key] + else: + try: + value = space[key] % section + except TypeError: + value = space[key] + self.__dict__[key+'s'].append(value) + + + def __setattr__(self, key, value): + if not key in self.__dict__: + self.__dict__[key] = value + else: + pass + + + def __getattr__(self, key, value = None): + if key in self.__dict__: + return deepcopy(self.__dict__[key]) + + + def __delattr__(self, key): + if key in self.__dict__: + pass + + +CONST = Constants() + +CONST.PACKAGE_NAME = "Gentoo Reference System" +CONST.PACKAGE_VERSION = 0.0 +CONST.PACKAGE_DESCRIPTION = "Update a GRS by cloning a predefined system." +CONST.BUG_REPORTS = 'http://bugs.gentoo.org' + +# The are defaults in case objects are instantiated without namespaces +# but they should not be used under normal working condidtions. +CONST.LIBDIR = '/var/lib/grs' +CONST.LOGFILE = '/var/log/grs.log' +CONST.TMPDIR = '/var/tmp/grs' +CONST.WORKDIR = '/var/tmp/grs/work' +CONST.PACKAGE = '/var/tmp/grs/package' +CONST.KERNELROOT = '/var/tmp/grs/kernel' +CONST.PORTAGE_CONFIGROOT = '/var/tmp/grs/system' +CONST.PIDFILE = '/run/grs.pid' + +CONST.PORTAGE_CONFIGDIR = '/etc/portage' +CONST.PORTAGE_DIRTYFILE = '/etc/portage/.grs_dirty' +CONST.WORLD_CONFIG = '/etc/grs/world.conf' diff --git a/grs/Daemon.py b/grs/Daemon.py new file mode 100644 index 0000000..8fed2a1 --- /dev/null +++ b/grs/Daemon.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python + +import atexit +import os +import signal +import sys +import time + +class Daemon: + """ doc here + more doc + """ + + def __init__(self, pidfile, **kwargs): + self.pidfile = pidfile + for k in kwargs: + self.__dict__[k] = kwargs[k] + + def daemonize(self): + """ doc here + more doc + """ + + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError as err: + sys.stderr.write('fork #1 failed: %s\n' % err) + sys.exit(1) + + os.chdir('/') + os.setsid() + os.umask(0o22) + + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError as err: + sys.stderr.write('fork #2 failed %s\n' % err) + sys.exit(1) + + # Close stdin, and redirect both stdout and stderr to grs-daemon-<pid>.err + si = open(os.devnull, 'r') + os.dup2(si.fileno(), sys.stdin.fileno()) + + os.makedirs('/var/log/grs', mode=0o755, exist_ok=True) + se = open('/var/log/grs/grs-daemon-%d.err' % os.getpid(), 'a+') + + sys.stdout.flush() + os.dup2(se.fileno(), sys.stdout.fileno()) + sys.stderr.flush() + os.dup2(se.fileno(), sys.stderr.fileno()) + + atexit.register(self.delpid) + with open(self.pidfile,'w') as pf: + pf.write('%d\n' % os.getpid()) + + + def delpid(self): + os.remove(self.pidfile) + + + def start(self): + try: + with open(self.pidfile, 'r') as pf: + pid = int(pf.read().strip()) + except IOError: + pid = None + + if pid: + if not os.path.exists('/proc/%d' % pid): + sys.stderr.write('unlinking stale pid file %s\n' % self.pidfile) + os.unlink(self.pidfile) + else: + sys.stderr.write('process running with pid = %d\n' % pid) + return + + self.daemonize() + self.run() + + + def stop(self): + try: + with open(self.pidfile,'r') as pf: + pid = int(pf.read().strip()) + except IOError: + pid = None + + if pid and not os.path.exists('/proc/%d' % pid): + sys.stderr.write('process not running\n') + sys.stderr.write('unlinking stale pid file %s\n' % self.pidfile) + os.unlink(self.pidfile) + return + + if not pid: + sys.stderr.write('process not running\n') + return # not an error in a restart + + try: + for i in range(10): + os.kill(pid, signal.SIGTERM) + time.sleep(0.2) + while True: + os.kill(pid, signal.SIGKILL) + time.sleep(0.2) + except ProcessLookupError as err: + try: + os.remove(self.pidfile) + except IOError as err: + sys.stderr.write('%s\n' % err) + except OSError as err: + sys.stderr.write('%s\n' %err) + return + + def restart(self): + self.stop() + self.start() + + def run(self): + pass diff --git a/grs/Execute.py b/grs/Execute.py new file mode 100644 index 0000000..533d269 --- /dev/null +++ b/grs/Execute.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +import os +import signal +import shlex +import subprocess +from grs.Constants import CONST + +class Execute(): + """ doc here + more doc + """ + + def __init__(self, cmd, timeout = 1, extra_env = {}, failok = False, logfile = CONST.LOGFILE): + """ doc here + more doc + """ + def signalexit(): + pid = os.getpid() + f.write('SENDING SIGTERM to pid = %d\n' % pid) + f.close() + os.kill(pid, signal.SIGTERM) + + f = open(logfile, 'a') + args = shlex.split(cmd) + extra_env = dict(os.environ, **extra_env) + + try: + proc = subprocess.Popen(args, stdout=f, stderr=f, env=extra_env) + except FileNotFoundError: + f.write('Illegal cmd %s\n' % cmd) + signalexit() + + try: + proc.wait(timeout) + timed_out = False + except subprocess.TimeoutExpired: + proc.kill() + timed_out = True + + rc = proc.returncode + if rc != 0: + f.write('EXIT CODE: %d\n' % rc) + if not failok: + signalexit() + + if timed_out: + f.write('TIMEOUT ERROR: %s\n' % cmd) + + f.close() diff --git a/grs/Interpret.py b/grs/Interpret.py new file mode 100644 index 0000000..8cef40e --- /dev/null +++ b/grs/Interpret.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python + +import os +import re +import signal +import sys +from grs.Constants import CONST +from grs.Daemon import Daemon +from grs.Log import Log +from grs.Kernel import Kernel +from grs.MountDirectories import MountDirectories +from grs.Populate import Populate +from grs.RunScript import RunScript +from grs.Synchronize import Synchronize +from grs.Seed import Seed +from grs.TarIt import TarIt + + +class Interpret(Daemon): + """ doc here + more doc + """ + + def run(self): + """ doc here + more doc + """ + + def handler(signum, frame): + """ On SIGTERM, propagate the signal to all processes in the cgroup/subcgroup + except yourself. If a process won't terminate nicely, then kill it. + Finally unmount all the mounted filesystems. Hopefully this will work + since there should be no more open files on those filesystems. + """ + mypid = os.getpid() + while True: + with open(os.path.join(self.subcgroupdir, 'tasks'), 'r') as f: + lines = f.readlines() + if len(lines) <= 1: + break + for p in lines: + pid = int(p.strip()) + if mypid == pid: + continue + try: + os.kill(pid, signal.SIGTERM) + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + try: + md.umount_all() + except NameError: + pass + sys.exit(signum + 128) + + signal.signal(signal.SIGINT, handler) + signal.signal(signal.SIGTERM, handler) + + def smartlog(l, obj, has_obj = True): + if (has_obj and not obj) or (not has_obj and obj): + lo.log('Bad command: %s' % l) + return True + if self.mock_run: + lo.log(l) + return True + return False + + def stampit(progress): + open(progress, 'w').close() + + nameserver = CONST.nameservers[self.run_number] + repo_uri = CONST.repo_uris[self.run_number] + stage_uri = CONST.stage_uris[self.run_number] + + name = CONST.names[self.run_number] + libdir = CONST.libdirs[self.run_number] + logfile = CONST.logfiles[self.run_number] + tmpdir = CONST.tmpdirs[self.run_number] + workdir = CONST.workdirs[self.run_number] + package = CONST.packages[self.run_number] + kernelroot = CONST.kernelroots[self.run_number] + portage_configroot = CONST.portage_configroots[self.run_number] + + lo = Log(logfile) + sy = Synchronize(repo_uri, name, libdir, logfile) + se = Seed(stage_uri, tmpdir, portage_configroot, package, logfile) + md = MountDirectories(portage_configroot, package, logfile) + po = Populate(nameserver, libdir, workdir, portage_configroot, logfile) + ru = RunScript(libdir, portage_configroot, logfile) + ke = Kernel(libdir, portage_configroot, kernelroot, package, logfile) + bi = TarIt(name, portage_configroot, logfile) + + os.makedirs(tmpdir, mode=0o755, exist_ok=True) + + lo.rotate_logs() + md.umount_all() + + # Both sync() + seed() are not scripted steps. + # sync() is done unconditionally for an update run. + progress = os.path.join(tmpdir, '.completed_sync') + if not os.path.exists(progress) or self.update_run: + sy.sync() + stampit(progress) + + # seed() is never done for an update run + progress = os.path.join(tmpdir, '.completed_seed') + if not os.path.exists(progress) and not self.update_run: + se.seed() + stampit(progress) + + build_script = os.path.join(libdir, 'build') + try: + with open(build_script, 'r') as s: + line_number = 0 + for l in s.readlines(): + line_number += 1 + + # For a release run, execute every line of the build script. + # For an update run, exexute only lines with a leading +. + ignore_stamp = False + m = re.search('^(\+)(.*)$', l) + if m: + # There is a leading +, so remove it and skip if doing an update run + ignore_stamp = self.update_run + l = m.group(2) + else: + # There is no leading +, so skip if this is an update run + if self.update_run: + continue + + progress = os.path.join(tmpdir, '.completed_%02d' % line_number) + if os.path.exists(progress) and not ignore_stamp: + continue + + try: + m = re.search('(\S+)\s+(\S+)', l) + verb = m.group(1) + obj = m.group(2) + except AttributeError: + verb = l.strip() + obj = None + + if verb == '': + stampit(progress) + continue + if verb == 'log': + if smartlog(l, obj): + stampit(progress) + continue + if obj == 'stamp': + lo.log('='*80) + else: + lo.log(obj) + elif verb == 'mount': + if smartlog(l, obj, False): + stampit(progress) + continue + md.mount_all() + elif verb == 'unmount': + if smartlog(l, obj, False): + stampit(progress) + continue + md.umount_all() + elif verb == 'populate': + if smartlog(l, obj): + stampit(progress) + continue + po.populate(cycle=int(obj)) + elif verb == 'runscript': + if smartlog(l, obj): + stampit(progress) + continue + ru.runscript(obj) + elif verb == 'clean': + if smartlog(l, obj, False): + stampit(progress) + continue + po.clean() + elif verb == 'kernel': + if smartlog(l, obj, False): + stampit(progress) + continue + ke.kernel() + elif verb == 'tarit': + if smartlog(l, obj, False): + stampit(progress) + continue + bi.tarit() + elif verb == 'hashit': + if smartlog(l, obj, False): + stampit(progress) + continue + bi.hashit() + else: + lo.log('Bad command: %s' % l) + + stampit(progress) + + except FileNotFoundError: + lo.log('Failed to open build script: %s' % build_script) diff --git a/grs/Kernel.py b/grs/Kernel.py new file mode 100644 index 0000000..ddae884 --- /dev/null +++ b/grs/Kernel.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python + +import os +import re +import shutil + +from grs.Constants import CONST +from grs.Execute import Execute + + +class Kernel(): + + def __init__(self, libdir = CONST.LIBDIR, portage_configroot = CONST.PORTAGE_CONFIGROOT, kernelroot = CONST.KERNELROOT, package = CONST.PACKAGE, logfile = CONST.LOGFILE): + self.libdir = libdir + self.portage_configroot = portage_configroot + self.kernelroot = kernelroot + self.package = package + self.logfile = logfile + self.kernel_config = os.path.join(self.libdir, 'scripts/kernel-config') + + + def parse_kernel_config(self): + with open(self.kernel_config, 'r') as f: + for i in range(3): + line = f.readline() + m = re.search('^#\s+(\S+)\s+(\S+).+$', line) + gentoo_version = m.group(2) + try: + m = re.search('(\S+?)-(\S+?)-(\S+)', gentoo_version) + vanilla_version = m.group(1) + flavor = m.group(2) + revision = m.group(3) + pkg_name = flavor + '-sources-' + vanilla_version + '-' + revision + except AttributeError: + m = re.search('(\S+?)-(\S+)', gentoo_version) + vanilla_version = m.group(1) + flavor = m.group(2) + pkg_name = flavor + '-sources-' + vanilla_version + pkg_name = '=sys-kernel/' + pkg_name + return (gentoo_version, pkg_name) + + + def kernel(self): + (gentoo_version, pkg_name) = self.parse_kernel_config() + + kernel_source = os.path.join(self.kernelroot, 'usr/src/linux') + image_dir = os.path.join(self.kernelroot, gentoo_version) + boot_dir = os.path.join(image_dir, 'boot') + modprobe_dir = os.path.join(image_dir, 'etc/modprobe.d') + modules_dir = os.path.join(image_dir, 'lib/modules') + + # Remove any old image directory and create a boot directory + # wich genkernel assumes is present. + shutil.rmtree(image_dir) + os.makedirs(boot_dir, mode=0o755, exist_ok=True) + + cmd = 'emerge --nodeps -1n %s' % pkg_name + emerge_env = { 'USE' : 'symlink', 'ROOT' : self.kernelroot, 'ACCEPT_KEYWORDS' : '**' } + Execute(cmd, timeout=600, extra_env=emerge_env, logfile=self.logfile) + + # Build and install the image outside the portage configroot so + # we can both rsync it in *and* tarball it for downloads. + # TODO: add more options (eg splash and firmware), which can be + # specified vi the kernel line in the build script. + cmd = 'genkernel ' + cmd += '--logfile=/dev/null ' + cmd += '--no-save-config ' + cmd += '--makeopts=-j9 ' + cmd += '--no-firmware ' + cmd += '--symlink ' + cmd += '--no-mountboot ' + cmd += '--kernel-config=%s ' % self.kernel_config + cmd += '--kerneldir=%s ' % kernel_source + cmd += '--bootdir=%s ' % boot_dir + cmd += '--module-prefix=%s ' % image_dir + cmd += '--modprobedir=%s ' % modprobe_dir + cmd += 'all' + Execute(cmd, timeout=None, logfile=self.logfile) + + for dirpath, dirnames, filenames in os.walk(modules_dir): + for filename in filenames: + if filename.endswith('.ko'): + module = os.path.join(dirpath, filename) + cmd = 'objcopy -v --strip-unneeded %s' % module + Execute(cmd) + + # Copy the newly compiled kernel image and modules to portage configroot + cmd = 'rsync -a %s/ %s' % (image_dir, self.portage_configroot) + Execute(cmd, timeout=60, logfile=self.logfile) + + # Tar up the kernel image and modules and place them in package/linux-images + linux_images = os.path.join(self.package, 'linux-images') + os.makedirs(linux_images, mode=0o755, exist_ok=True) + tarball_name = 'linux-image-%s.tar.xz' % gentoo_version + tarball_path = os.path.join(linux_images, tarball_name) + + cwd = os.getcwd() + os.chdir(image_dir) + os.unlink(tarball_path) + cmd = 'tar -Jcf %s .' % tarball_path + Execute(cmd, timeout=600, logfile=self.logfile) + os.chdir(cwd) diff --git a/grs/Log.py b/grs/Log.py new file mode 100644 index 0000000..91141e8 --- /dev/null +++ b/grs/Log.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +import datetime +import glob +import os +import re +import shutil +from grs.Constants import CONST + +class Log(): + + def __init__(self, logfile = CONST.LOGFILE): + self.logfile = logfile + try: + os.makedirs(os.path.dirname(self.logfile)) + except FileExistsError: + pass + open(self.logfile, 'a').close() + + + def log(self, msg, stamped = True): + if stamped: + current_time = datetime.datetime.now(datetime.timezone.utc) + unix_timestamp = current_time.timestamp() + msg = '[%f] %s' % (unix_timestamp, msg) + with open(self.logfile, 'a') as f: + f.write('%s\n' % msg) + + + def rotate_logs(self): + logs = glob.glob('%s.*' % self.logfile) + indexed_log = {} + for l in logs: + m = re.search('^.+\.(\d+)$', l) + indexed_log[int(m.group(1))] = l + count = list(indexed_log.keys()) + count.sort() + count.reverse() + for c in count: + current_log = indexed_log[c] + m = re.search('^(.+)\.\d+$', current_log) + next_log = '%s.%d' % (m.group(1), c+1) + shutil.move(current_log, next_log) + if os.path.isfile(self.logfile): + shutil.move(self.logfile, '%s.0' % self.logfile) + open('%s' % self.logfile, 'a').close() diff --git a/grs/MountDirectories.py b/grs/MountDirectories.py new file mode 100644 index 0000000..43cd91a --- /dev/null +++ b/grs/MountDirectories.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python + +import os +from copy import deepcopy +from grs.Constants import CONST +from grs.Execute import Execute + +class MountDirectories(): + """ doc here + more doc + """ + + def __init__(self, portage_configroot = CONST.PORTAGE_CONFIGROOT, package = CONST.PACKAGE, logfile = CONST.LOGFILE): + """ doc here + more doc + """ + # The order is respected + self.directories = [ + 'dev', + 'dev/pts', + { 'dev/shm' : ( 'tmpfs', 'shm' ) }, + 'proc', + 'sys', + 'usr/portage', + [ package, 'usr/portage/packages' ] + ] + # Once initiated, we'll only work with one portage_configroot + self.portage_configroot = portage_configroot + self.package = package + self.logfile = logfile + # We need to umount in the reverse order + self.rev_directories = deepcopy(self.directories) + self.rev_directories.reverse() + + def ismounted(self, mountpoint): + # Obtain all the current mountpoints. os.path.ismount() fails for for bind mounts, + # so we obtain them all ourselves + mountpoints = [] + for line in open('/proc/mounts', 'r').readlines(): + mountpoints.append(line.split()[1]) + # Let's make sure mountoint is canonical real path, no sym links, + # since that's what /proc/mounts reports. + mountpoint = os.path.realpath(mountpoint) + return mountpoint in mountpoints + + def are_mounted(self): + """ doc here + more doc + """ + some_mounted = False + all_mounted = True + for mount in self.directories: + if isinstance(mount, str): + target_directory = mount + elif isinstance(mount, list): + target_directory = mount[1] + elif isinstance(mount, dict): + tmp = list(mount.keys()) + target_directory = tmp[0] + target_directory = os.path.join(self.portage_configroot, target_directory) + if self.ismounted(target_directory): + some_mounted = True + else: + all_mounted = False + return some_mounted, all_mounted + + + def mount_all(self): + """ doc here + more doc + """ + # If any our mounted, let's first unmount all, then mount all + some_mounted, all_mounted = self.are_mounted() + if some_mounted: + self.umount_all() + + for mount in self.directories: + if isinstance(mount, str): + # Here source_directory is assumed to exist relative to / + source_directory = mount + target_directory = mount + elif isinstance(mount, list): + # Here source_directory is assumet to be an abspath + # and we create it if it doesn't exist + source_directory = mount[0] + if not os.path.isdir(source_directory): + os.makedirs(source_directory) + target_directory = mount[1] + elif isinstance(mount, dict): + tmp = list(mount.values()) + tmp = tmp[0] + vfstype = tmp[0] + vfsname = tmp[1] + tmp = list(mount.keys()) + target_directory = tmp[0] + target_directory = os.path.join(self.portage_configroot, target_directory) + if not os.path.isdir(target_directory): + os.makedirs(target_directory) + if isinstance(mount, str): + cmd = 'mount --bind /%s %s' % (source_directory, target_directory) + elif isinstance(mount, list): + cmd = 'mount --bind %s %s' % (source_directory, target_directory) + elif isinstance(mount, dict): + cmd = 'mount -t %s %s %s' % (vfstype, vfsname, target_directory) + Execute(cmd, timeout=60, logfile=self.logfile) + + + def umount_all(self): + for mount in self.rev_directories: + if isinstance(mount, str): + target_directory = mount + elif isinstance(mount, list): + target_directory = mount[1] + elif isinstance(mount, dict): + tmp = list(mount.keys()) + target_directory = tmp[0] + target_directory = os.path.join(self.portage_configroot, target_directory) + if self.ismounted(target_directory): + cmd = 'umount --force %s' % target_directory + Execute(cmd, timeout=60, logfile=self.logfile) diff --git a/grs/Populate.py b/grs/Populate.py new file mode 100644 index 0000000..504283c --- /dev/null +++ b/grs/Populate.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python + +import os +import re +import shutil +from grs.Constants import CONST +from grs.Execute import Execute + +class Populate(): + """ doc here + more doc + """ + + def __init__(self, nameserver, libdir = CONST.LIBDIR, workdir = CONST.WORKDIR, portage_configroot = CONST.PORTAGE_CONFIGROOT, logfile = CONST.LOGFILE): + self.nameserver = nameserver + self.libdir = libdir + self.workdir = workdir + self.portage_configroot = portage_configroot + self.logfile = logfile + + self.etc = os.path.join(self.portage_configroot, 'etc') + self.resolv_conf = os.path.join(self.etc, 'resolv.conf') + + + def populate(self, cycle = True): + cmd = 'rsync -av --delete --exclude=\'.git*\' %s/core/ %s' % (self.libdir, self.workdir) + Execute(cmd, timeout=60, logfile = self.logfile) + + # Select the cycle + if cycle: self.select_cycle(cycle) + + # Copy from /tmp/grs-work to /tmp/system + cmd = 'rsync -av %s/ %s' % (self.workdir, self.portage_configroot) + Execute(cmd, timeout=60, logfile = self. logfile) + + # Add any extra files + try: + os.makedirs(self.etc) + except FileExistsError: + pass + with open(self.resolv_conf, 'w') as f: + f.write('nameserver %s' % self.nameserver) + + + def select_cycle(self, cycle): + cycled_files = {} + for dirpath, dirnames, filenames in os.walk(self.workdir): + for f in filenames: + m = re.search('^(.+)\.CYCLE\.(\d+)', f) + if m: + filename = m.group(1) + cycle_no = int(m.group(2)) + cycled_files.setdefault(cycle_no, []) + cycled_files[cycle_no].append([dirpath, filename]) + + if type(cycle) is bool: + cycle_no = max(cycled_files) + else: + cycle_no = cycle + for c in cycled_files: + for f in cycled_files[c]: + dirpath = f[0] + filename = f[1] + new_file = os.path.join(dirpath, filename) + old_file = "%s.CYCLE.%d" % (new_file, c) + if os.path.isfile(old_file): + if c == cycle_no: + os.rename(old_file, new_file) + else: + os.remove(old_file) + + def clean_subdirs(self, dirpath): + path = os.path.join(self.portage_configroot, dirpath) + try: + uid = os.stat(path).st_uid + gid = os.stat(path).st_gid + mode = os.stat(path).st_mode + shutil.rmtree(path) + os.mkdir(path) + os.chown(path, uid, gid) + os.chmod(path, mode) + except FileNotFoundError: + pass + + + def clean(self): + self.clean_subdirs('tmp') + self.clean_subdirs('var/tmp') + self.clean_subdirs('var/log') + try: + os.unlink(self.resolv_conf) + except FileNotFoundError: + pass diff --git a/grs/RunScript.py b/grs/RunScript.py new file mode 100644 index 0000000..a80f342 --- /dev/null +++ b/grs/RunScript.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +import os +import shutil +from grs.Constants import CONST +from grs.Execute import Execute + +class RunScript(): + """ doc here + more doc + """ + + def __init__(self, libdir = CONST.LIBDIR, portage_configroot = CONST.PORTAGE_CONFIGROOT, logfile = CONST.LOGFILE): + """ doc here + more doc + """ + self.libdir = libdir + self.portage_configroot = portage_configroot + self.logfile = logfile + + def runscript(self, script_name): + script_org = os.path.join(self.libdir, 'scripts/%s' % script_name) + script_dst = os.path.join(self.portage_configroot, 'tmp/script.sh') + shutil.copy(script_org, script_dst) + os.chmod(script_dst, 0o0755) + cmd = 'chroot %s /tmp/script.sh' % self.portage_configroot + Execute(cmd, timeout=None, logfile=self.logfile) + os.unlink(script_dst) diff --git a/grs/Seed.py b/grs/Seed.py new file mode 100644 index 0000000..0da8354 --- /dev/null +++ b/grs/Seed.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python + +import glob +import os +import re +import shutil +import urllib.request +from grs.Constants import CONST +from grs.Execute import Execute + + +class Seed(): + """ doc here + more doc + """ + + def __init__(self, stage_uri, tmpdir = CONST.TMPDIR, portage_configroot = \ + CONST.PORTAGE_CONFIGROOT, package = CONST.PACKAGE, logfile = CONST.LOGFILE): + """ doc here + more doc + """ + self.stage_uri = stage_uri + self.portage_configroot = portage_configroot + self.package = package + filename = os.path.basename(stage_uri) + self.filepath = os.path.join(tmpdir, filename) + self.logfile = logfile + + + def seed(self): + """ doc here + more doc + """ + for directory in [self.portage_configroot, self.package]: + # Rotate any previous directories out of the way + dirs = glob.glob('%s.*' % directory) + indexed_dir = {} + for d in dirs: + m = re.search('^.+\.(\d+)$', d) + indexed_dir[int(m.group(1))] = d + count = list(indexed_dir.keys()) + count.sort() + count.reverse() + for c in count: + current_dir = indexed_dir[c] + m = re.search('^(.+)\.\d+$', current_dir) + next_dir = '%s.%d' % (m.group(1), c+1) + shutil.move(current_dir, next_dir) + # If there is a directory, then move it to %s.0 + if os.path.isdir(directory): + shutil.move(directory, '%s.0' % directory) + # Now that all prevous directory are out of the way, + # create a new empty directory + os.makedirs(directory) + + # Download a stage tarball if we don't have one + if not os.path.isfile(self.filepath): + try: + request = urllib.request.urlopen(self.stage_uri) + with open(self.filepath, 'wb') as f: + shutil.copyfileobj(request, f) + except: #any exception will do here + pid = os.getpid() + with open(self.logfile, 'r') as f: + f.write('SENDING SIGTERM to pid = %d\n' % pid) + f.close() + os.kill(pid, signal.SIGTERM) + + # Because python's tarfile sucks + cmd = 'tar --xattrs -xf %s -C %s' % (self.filepath, self.portage_configroot) + Execute(cmd, timeout=120, logfile=self.logfile) + #os.unlink(self.filepath) diff --git a/grs/Synchronize.py b/grs/Synchronize.py new file mode 100644 index 0000000..1df8ce2 --- /dev/null +++ b/grs/Synchronize.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +import os +from grs.Constants import CONST +from grs.Execute import Execute + +class Synchronize(): + """ doc here + more doc + """ + + def __init__(self, remote_repo, branch, libdir = CONST.LIBDIR, logfile = CONST.LOGFILE): + self.remote_repo = remote_repo + self.branch = branch + self.local_repo = libdir + self.logfile = logfile + + def sync(self): + if self.isgitdir(): + cmd = 'git -C %s reset HEAD --hard' % self.local_repo + Execute(cmd, timeout=60, logfile=self.logfile) + cmd = 'git -C %s clean -f -x -d' % self.local_repo + Execute(cmd, timeout=60, logfile=self.logfile) + cmd = 'git -C %s pull' % self.local_repo + Execute(cmd, timeout=60, logfile=self.logfile) + cmd = 'git -C %s checkout %s' % (self.local_repo, self.branch) + Execute(cmd, timeout=60, logfile=self.logfile) + else: + cmd = 'git clone %s %s' % (self.remote_repo, self.local_repo) + Execute(cmd, timeout=60, logfile=self.logfile) + cmd = 'git -C %s checkout %s' % (self.local_repo, self.branch) + Execute(cmd, timeout=60, logfile=self.logfile) + + def isgitdir(self): + git_configdir = os.path.join(self.local_repo, '.git') + git_configfile = os.path.join(git_configdir, 'config') + return os.path.isdir(git_configdir) and os.path.isfile(git_configfile) diff --git a/grs/TarIt.py b/grs/TarIt.py new file mode 100644 index 0000000..c0c3f0c --- /dev/null +++ b/grs/TarIt.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +import os +from datetime import datetime +from grs.Constants import CONST +from grs.Execute import Execute + +class TarIt(): + + def __init__(self, name, portage_configroot = CONST.PORTAGE_CONFIGROOT, logfile = CONST.LOGFILE): + self.portage_configroot = portage_configroot + self.logfile = logfile + + year = str(datetime.now().year).zfill(4) + month = str(datetime.now().month).zfill(2) + day = str(datetime.now().day).zfill(2) + self.tarball_name = '%s-%s%s%s.tar.xz' % (name, year, month, day) + self.digest_name = '%s.DIGESTS' % self.tarball_name + + def tarit(self): + cwd = os.getcwd() + os.chdir(self.portage_configroot) + tarball_path = os.path.join('..', self.tarball_name) + xattr_opts = '--xattrs --xattrs-include=security.capability --xattrs-include=user.pax.flags' + cmd = 'tar %s -Jcf %s .' % (xattr_opts, tarball_path) + Execute(cmd, timeout=None, logfile=self.logfile) + os.chdir(cwd) + + def hashit(self): + cwd = os.getcwd() + os.chdir(os.path.join(self.portage_configroot, '..')) + + # Note: this first cmd clobbers the contents + cmd = 'echo "# MD5 HASH"' + Execute(cmd, logfile=self.digest_name) + cmd = 'md5sum %s' % self.tarball_name + Execute(cmd, timeout=60, logfile=self.digest_name) + + cmd = 'echo "# SHA1 HASH"' + Execute(cmd, logfile=self.digest_name) + cmd = 'sha1sum %s' % self.tarball_name + Execute(cmd, timeout=60, logfile=self.digest_name) + + cmd = 'echo "# SHA512 HASH"' + Execute(cmd, logfile=self.digest_name) + cmd = 'sha512sum %s' % self.tarball_name + Execute(cmd, timeout=60, logfile=self.digest_name) + + cmd = 'echo "# WHIRLPOOL HASH"' + Execute(cmd, logfile=self.digest_name) + cmd = 'whirlpooldeep %s' % self.tarball_name + Execute(cmd, timeout=60, logfile=self.digest_name) + + os.chdir(cwd) diff --git a/grs/WorldConf.py b/grs/WorldConf.py new file mode 100644 index 0000000..ba683cd --- /dev/null +++ b/grs/WorldConf.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python + +import configparser +import copy +import os +import portage +import re + +from grs.Constants import CONST + +class WorldConf(): + """ doc here + more doc + """ + + @staticmethod + def conf2file(config, s, portage_dir): + """ doc here + more doc + """ + try: + for (f, v) in config[s].items(): + # a '+' at the beginging means append to the file + undecorated_f = re.sub('^\+', '', f) + + filepath = os.path.join(portage_dir, undecorated_f) + dirpath = os.path.dirname(filepath) + os.makedirs(dirpath, mode=0o755, exist_ok=True) + if f == undecorated_f or not os.path.exists(filepath): + with open(filepath, 'w') as g: + g.write('%s\n' % v) + else: + with open(filepath, 'r+') as g: + for l in g.readlines(): + if v == l.strip(): + break + else: + g.write('%s\n' % v) + except KeyError: + pass + + + @staticmethod + def install(): + """ doc here + more doc + """ + config = configparser.RawConfigParser(delimiters=':', allow_no_value=True, comment_prefixes=None) + config.read(CONST.WORLD_CONFIG) + + for s in config.sections(): + WorldConf.conf2file(config, s, portage_dir=CONST.PORTAGE_CONFIGDIR) + + + @staticmethod + def clean(): + """ doc here + more doc + """ + portdb = portage.db[portage.root]["porttree"].dbapi + vardb = portage.db[portage.root]["vartree"].dbapi + + uninstalled = portdb.cp_all() + for p in vardb.cp_all(): + try: + uninstalled.remove(p) + except ValueError: + # These packages are installed on the local system + # but not in the portage tree anymore. + print(p) + + slot_atoms = [] + for p in uninstalled: + cpv = portdb.cp_list(p)[0] + slotvar = portdb.aux_get(cpv, ['SLOT'])[0] + try: + m = re.search('(.+?)\/(.+)', slotvar) + slot = m.group(1) + except AttributeError: + slot = slotvar + slot_atoms.append(re.sub('[/:]', '_', '%s:%s' % (p, slot))) + + env_slot_atoms = [] + for dirpath, dirnames, filenames in os.walk(CONST.PORTAGE_CONFIGDIR): + # Only look at select files and directories. + # TODO: This needs to be expanded as we come up + # with a central class to deal with the internal + # structure of /etc/portage. + skip = True + for p in ['env', 'package.accept_keywords', 'package.use']: + if os.path.basename(dirpath) == p: + skip = False + if skip: + continue + + for f in filenames: + fpath = os.path.realpath(os.path.join(dirpath, f)) + if f in slot_atoms: + os.remove(fpath) + if os.path.basename(dirpath) == 'env': + env_slot_atoms.append(f) + continue + + fpath = os.path.join(CONST.PORTAGE_CONFIGDIR, 'package.env') + update = False + with open(fpath, 'r') as g: + lines = g.readlines() + mylines = copy.deepcopy(lines) + for l in lines: + for slot_atom in env_slot_atoms: + if re.search(re.escape(slot_atom), l): + try: + mylines.remove(l) + update = True + except ValueError: + pass + if update: + with open(fpath, 'w') as g: + g.writelines(mylines) diff --git a/grs/__init__.py b/grs/__init__.py new file mode 100644 index 0000000..5580d1d --- /dev/null +++ b/grs/__init__.py @@ -0,0 +1,15 @@ +#!/use/bin/env python + +from grs.TarIt import TarIt +from grs.Constants import CONST +from grs.Daemon import Daemon +from grs.Execute import Execute +from grs.Interpret import Interpret +from grs.Log import Log +from grs.Kernel import Kernel +from grs.MountDirectories import MountDirectories +from grs.Populate import Populate +from grs.RunScript import RunScript +from grs.Synchronize import Synchronize +from grs.Seed import Seed +from grs.WorldConf import WorldConf |