diff --git a/sys/BootSync/bootsync.py b/sys/BootSync/bootsync.py
new file mode 100755
index 0000000..fd39c37
--- /dev/null
+++ b/sys/BootSync/bootsync.py
@@ -0,0 +1,311 @@
+#!/usr/bin/env python3
+
+import argparse
+import hashlib
+import json
+import os
+import platform
+import re
+import shutil
+import subprocess
+##
+import magic # From http://darwinsys.com/file/, not https://github.com/ahupp/python-magic
+import psutil
+from lxml import etree
+
+
+ # def get_file_kernel_ver(self, kpath):
+ # # Gets the version of a kernel file.
+ # kpath = os.path.abspath(os.path.expanduser(kpath))
+ # _kinfo = {}
+ # with open(kpath, 'rb') as f:
+ # _m = magic.detect_from_content(f.read())
+ # for i in _m.name.split(','):
+ # l = i.strip().split()
+ # # Note: this only grabs the version number.
+ # # If we want to get e.g. the build user/machine, date, etc.,
+ # # then we need to join l[1:].
+ # # We technically don't even need a dict, either. We can just iterate.
+ # # TODO.
+ # _kinfo[l[0].lower()] = (l[1] if len(l) > 1 else None)
+ # if 'version' not in _kinfo:
+ # raise RuntimeError('Cannot deterimine the version of {0}'.format(
+ # kpath))
+ # else:
+ # return (_kinfo['version'])
+
+class BootSync(object):
+ def __init__(self, cfg = None, *args, **kwargs):
+ if not cfg:
+ self.cfgfile = '/etc/bootsync.xml'
+ else:
+ self.cfgfile = os.path.abspath(os.path.expanduser(cfg))
+ self.ns = '{http://git.square-r00t.net/OpTools/tree/sys/BootSync/}'
+ self.cfg = None
+ self.xml = None
+ # This is the current live kernel.
+ self.currentKernVer = self._getRunningKernel()
+ # This is the installed kernel from the package manager.
+ self.kernelFile = None
+ self.installedKernVer = None
+ self.RequireReboot = False # If a reboot is needed (WARN, don't execute!)
+ self.blkids = {}
+ self.dummy_uuid = None
+ self.syncs = {}
+ ##
+ self.getCfg()
+ self.chkMounts()
+ self.chkReboot()
+ self.getHashes()
+ self.getBlkids()
+ # self.sync()
+ # self.writeConfs()
+
+ def getCfg(self):
+ if not os.path.isfile(self.cfgfile):
+ raise FileNotFoundError('Configuration file {0} does not exist!'.format(self.cfgfile))
+ try:
+ with open(self.cfgfile, 'rb') as f:
+ self.xml = etree.parse(f)
+ self.xml.xinclude()
+ self.cfg = self.xml.getroot()
+ except etree.XMLSyntaxError:
+ # self.logger.error('{0} is invalid XML'.format(self.cfgfile))
+ raise ValueError(('{0} does not seem to be valid XML. '
+ 'See sample.config.xml for an example configuration.').format(self.cfgfile))
+ return()
+
+ def chkMounts(self):
+ _mounts = {m.device: m.mountpoint for m in psutil.disk_partitions(all = True)}
+ for esp in self.cfg.findall('{0}partitions/{0}part'.format(self.ns)):
+ disk = esp.attrib['path']
+ mount = os.path.abspath(os.path.expanduser(esp.attrib['mount']))
+ if not os.path.isdir(mount):
+ os.makedirs(mount, exist_ok = True)
+ if disk not in _mounts:
+ with open(os.devnull, 'w') as devnull:
+ c = subprocess.run(['/usr/bin/mount', mount],
+ stderr = devnull)
+ if c.returncode == 1: # Not specified in fstab
+ subprocess.run(['/usr/bin/mount', disk, mount],
+ stderr = devnull)
+ elif c.returncode == 32: # Already mounted
+ pass
+ return()
+
+ def chkReboot(self):
+ self._getInstalledKernel()
+ if not self.kernelFile:
+ return() # No isKernel="true" was specified in the config.
+ if self.installedKernVer != self.currentKernVer:
+ self.RequireReboot = True
+ # TODO: logger instead?
+ print(('NOTE: REBOOT REQUIRED. '
+ 'New kernel is {0}. '
+ 'Running kernel is {1}.').format(self.installedKernVer,
+ self.currentKernVer))
+ return()
+
+ def getBlkids(self):
+ c = subprocess.run(['/usr/bin/blkid',
+ '-o', 'export'],
+ stdout = subprocess.PIPE)
+ if c.returncode != 0:
+ raise RuntimeError('Could not fetch block ID information')
+ for p in c.stdout.decode('utf-8').split('\n\n'):
+ line = [i.strip() for i in p.splitlines()]
+ d = dict(map(lambda i: i.split('='), line))
+ if d.get('TYPE') == 'squashfs':
+ continue
+ self.blkids[d['DEVNAME']] = d.get('PARTUUID', d['UUID'])
+ c = subprocess.run(['/usr/bin/findmnt',
+ '--json',
+ '-T', '/boot'],
+ stdout = subprocess.PIPE)
+ # I write ridiculous one-liners.
+ self.dummy_uuid = self.blkids[json.loads(c.stdout.decode('utf-8'))['filesystems'][0]['source']]
+ return()
+
+ def getHashes(self):
+ def _get_hash(fpathname):
+ fpathname = os.path.abspath(os.path.expanduser(fpathname))
+ _hash = hashlib.sha512()
+ with open(fpathname, 'rb') as fh:
+ _hash.update(fh.read())
+ return(_hash.hexdigest())
+ for f in self.cfg.findall('{0}fileChecks/{0}file'):
+ # We do /boot files manually in case it isn't specified as a
+ # separate mount.
+ rel_fpath = f.text
+ fpath = os.path.join('/boot', rel_fpath)
+ canon_hash = _get_hash(fpath)
+ for esp in self.cfg.findall('{0}partitions/{0}part'.format(self.ns)):
+ mount = os.path.abspath(os.path.expanduser(esp.attrib['mount']))
+ new_fpath = os.path.join(mount, f)
+ file_hash = _get_hash(new_fpath)
+ if file_hash != canon_hash:
+ if rel_fpath not in self.syncs:
+ self.syncs[rel_fpath] = []
+ self.syncs[rel_fpath].append(mount)
+ return()
+
+ def sync(self, dryrun = False, *args, **kwargs):
+ if not dryrun:
+ if os.geteuid() != 0:
+ raise PermissionError('You must be root to write to the appropriate destinations')
+ for f in self.syncs:
+ for m in self.syncs[f]:
+ orig = os.path.join('/boot', f)
+ dest = os.path.join(m, f)
+ if not dryrun:
+ shutil.copy2(orig, dest)
+ bootmounts = [e.attrib['mount'] for e in self.cfg.findall('{0}partitions/{0}part'.format(self.ns))]
+ # syncPaths
+ for syncpath in self.cfg.findall('{0}syncPaths/{0}path'.format(self.ns)):
+ source = os.path.abspath(os.path.expanduser(syncpath.attrib['source']))
+ target = syncpath.attrib['target']
+ pattern = syncpath.attrib['pattern']
+ # We don't use filecmp for this because:
+ # - dircmp doesn't recurse
+ # - the reports/lists don't retain relative paths
+ # - we can't regex out files
+ for root, dirs, files in os.walk(source):
+ prefix = re.sub('\/?{0}\/?'.format(source), '', root)
+ ptrn = re.compile(pattern)
+ for f in files:
+ fname_path = os.path.join(prefix, f)
+ bootsource = os.path.join(source, fname_path)
+ boottarget = os.path.join(target, fname_path)
+ if ptrn.search(f):
+ # Compare the contents.
+ with open(bootsource, 'rb') as fh:
+ orig_hash = hashlib.sha512(fh.read()).hexdigest()
+ for bootdir in bootmounts:
+ bootfile = os.path.join(bootdir, boottarget)
+ if not dryrun:
+ if not os.path.isfile(bootfile):
+ os.makedirs(os.path.dirname(bootfile),
+ exist_ok = True)
+ shutil.copy2(bootsource, bootfile)
+ else:
+ with open(bootfile, 'rb') as fh:
+ dest_hash = hashlib.sha512(fh.read()).hexdigest()
+ if orig_hash != dest_hash:
+ shutil.copy2(bootsource, bootfile)
+ # fileChecks are a *lot* easier.
+ for f in self.cfg.findall('{0}fileChecks/{0}file'.format(self.ns)):
+ source = os.path.join('/boot', f.text)
+ with open(source, 'rb') as fh:
+ orig_hash = hashlib.sha512(fh.read()).hexdigest()
+ for bootdir in bootmounts:
+ bootfile = os.path.join(bootdir, f.text)
+ if not dryrun:
+ if not os.path.isfile(bootfile):
+ os.makedirs(os.path.dirname(bootfile),
+ exist_ok = True)
+ shutil.copy2(source, bootfile)
+ else:
+ with open(bootfile, 'rb') as fh:
+ dest_hash = hashlib.sha512(fh.read()).hexdigest()
+ if orig_hash != dest_hash:
+ shutil.copy2(source, bootfile)
+ return()
+
+
+ def writeConfs(self, dryrun = False, *args, **kwargs):
+ if not dryrun:
+ if os.geteuid() != 0:
+ raise PermissionError('You must be root to write to the appropriate destinations')
+ else:
+ return()
+ # Get a fresh config in place.
+ with open(os.devnull, 'wb') as DEVNULL:
+ c = subprocess.run(['/usr/bin/grub-mkconfig',
+ '-o', '/boot/grub/grub.cfg'],
+ stdout = DEVNULL,
+ stderr = DEVNULL)
+ if c.returncode != 0:
+ raise RuntimeError('An error occurred when generating the GRUB configuration file.')
+ with open('/boot/grub/grub.cfg', 'r') as f:
+ _grubcfg = f.read()
+ for esp in self.cfg.findall('{0}partitions/{0}part'.format(self.ns)):
+ mount = os.path.abspath(os.path.expanduser(esp.attrib['mount']))
+ disk = os.path.abspath(os.path.expanduser(esp.attrib['path']))
+ with open(os.path.join(mount, 'grub/grub.cfg'), 'w') as f:
+ for line in _grubcfg.splitlines():
+ # if re.search(r'^\s*search\s+(.*)\s(-u|--fs-uuid)', line):
+ # pass
+ i = re.sub(r'(?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sys/BootSync/prep.txt b/sys/BootSync/prep.txt
new file mode 100644
index 0000000..6514763
--- /dev/null
+++ b/sys/BootSync/prep.txt
@@ -0,0 +1,56 @@
+PREPARATION:
+0.) Comment out all /boot mounts in /etc/fstab and umount /boot if mounted as a separate mountpoint.
+ You want to use *the /boot on your / mount*.
+
+1.) Prepare each target partition (partitions/part below) as an ESP
+ (https://wiki.archlinux.org/index.php/EFI_system_partition#Format_the_partition).
+
+2.) Install GRUB2 to *each ESP*. See sample.config.xml for context for the below examples.
+
+ grub-install \
+ --boot-directory=/mnt/boot1 \
+ --bootloader-id=Arch \
+ --efi-directory=/mnt/boot1/ \
+ --target=x86_64-efi \
+ --no-nvram \
+ --recheck
+
+ grub-install \
+ --boot-directory=/mnt/boot1 \
+ --bootloader-id="Arch (Fallback)" \
+ --efi-directory=/mnt/boot1/ \
+ --target=x86_64-efi \
+ --no-nvram \
+ --recheck
+
+ grub-install \
+ --boot-directory=/mnt/boot2 \
+ --bootloader-id=Arch \
+ --efi-directory=/mnt/boot2/ \
+ --target=x86_64-efi \
+ --no-nvram \
+ --recheck
+
+ grub-install \
+ --boot-directory=/mnt/boot2 \
+ --bootloader-id="Arch (Fallback)" \
+ --efi-directory=/mnt/boot2/ \
+ --target=x86_64-efi \
+ --no-nvram \
+ --recheck
+
+3.) Prepare the ESPs. See sample.config.xml for context for the below examples.
+
+ efibootmgr \
+ --create \
+ --disk /dev/sdd \
+ --part 1 \
+ --loader /EFI/Arch/grubx64.efi \
+ --label "Arch (Fallback)"
+
+ efibootmgr \
+ --create \
+ --disk /dev/sdb \
+ --part 1 \
+ --loader /EFI/Arch/grubx64.efi \
+ --label "Arch"
diff --git a/sys/BootSync/sample.config.xml b/sys/BootSync/sample.config.xml
new file mode 100644
index 0000000..b982ae9
--- /dev/null
+++ b/sys/BootSync/sample.config.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ initramfs-linux.img
+ intel-ucode.img
+ memtest86+/memtest.bin
+ vmlinuz-linux
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file