aboutsummaryrefslogtreecommitdiff
path: root/grs
diff options
context:
space:
mode:
authorAnthony G. Basile <blueness@gentoo.org>2015-06-30 22:15:35 -0400
committerAnthony G. Basile <blueness@gentoo.org>2015-06-30 22:15:35 -0400
commit8885d6a5b9035f2eb0c02e697dc0bfb45c8b5a7f (patch)
tree165b052003e580934854a5e9da1182cde1312d62 /grs
downloadgrss-8885d6a5b9035f2eb0c02e697dc0bfb45c8b5a7f.tar.gz
grss-8885d6a5b9035f2eb0c02e697dc0bfb45c8b5a7f.tar.bz2
grss-8885d6a5b9035f2eb0c02e697dc0bfb45c8b5a7f.zip
Initial commit.
Diffstat (limited to 'grs')
-rw-r--r--grs/Constants.py97
-rw-r--r--grs/Daemon.py122
-rw-r--r--grs/Execute.py50
-rw-r--r--grs/Interpret.py200
-rw-r--r--grs/Kernel.py102
-rw-r--r--grs/Log.py46
-rw-r--r--grs/MountDirectories.py120
-rw-r--r--grs/Populate.py93
-rw-r--r--grs/RunScript.py28
-rw-r--r--grs/Seed.py72
-rw-r--r--grs/Synchronize.py37
-rw-r--r--grs/TarIt.py54
-rw-r--r--grs/WorldConf.py119
-rw-r--r--grs/__init__.py15
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