initial commit
This commit is contained in:
commit
9ca03aaf08
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
# https://git-scm.com/docs/gitignore
|
||||
# https://help.github.com/articles/ignoring-files
|
||||
# Example .gitignore files: https://github.com/github/gitignore
|
||||
*.bak
|
||||
screenlog*
|
||||
*.swp
|
||||
*.lck
|
||||
*~
|
||||
.~lock.*
|
||||
.editix
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.tar
|
||||
*.tar.bz2
|
||||
*.tar.xz
|
||||
*.tar.gz
|
||||
*.tgz
|
||||
*.txz
|
||||
*.tbz
|
||||
*.tbz2
|
||||
*.zip
|
||||
*.run
|
||||
*.7z
|
||||
*.rar
|
||||
*.sqlite3
|
||||
*.deb
|
||||
.idea/
|
5
README
Normal file
5
README
Normal file
@ -0,0 +1,5 @@
|
||||
This mini-project allows you to maintain segregated ESP failover in a multi-disk setup.
|
||||
|
||||
If you have just one disk in your system, this project is useless to you. I promise.
|
||||
|
||||
The examples assume use of UEFI and GRUB2, but it can easily be tweaked to use other bootloaders with a little research and modification to the configuration (and commenting out/modifying the writeConfs() function; I'll maybe make that a little smarter in the future).
|
27
bootsync.hook
Normal file
27
bootsync.hook
Normal file
@ -0,0 +1,27 @@
|
||||
# The following would be placed in /etc/pacman.d/hooks/ directory (you may need to create it if it doesn't exist) as bootsync.hook
|
||||
# It assumes you have:
|
||||
# * a properly configured /etc/bootsync.xml
|
||||
# * /usr/local/bin/bootsync symlinked to <OpTools>/sys/BootSync/bootsync.py
|
||||
[Trigger]
|
||||
Operation = Install
|
||||
Operation = Upgrade
|
||||
Operation = Remove
|
||||
Type = File
|
||||
Target = boot/*
|
||||
Target = usr/lib/modules/*/vmlinuz
|
||||
Target = usr/lib/initcpio/*
|
||||
|
||||
[Trigger]
|
||||
Operation = Install
|
||||
Operation = Upgrade
|
||||
Operation = Remove
|
||||
Type = Package
|
||||
Target = linux
|
||||
Target = mkinitcpio
|
||||
|
||||
[Action]
|
||||
When = PostTransaction
|
||||
Exec = /usr/local/bin/bootsync
|
||||
Depends = python-magic
|
||||
Depends = python-psutil
|
||||
Depends = python-lxml
|
327
bootsync.py
Executable file
327
bootsync.py
Executable file
@ -0,0 +1,327 @@
|
||||
#!/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
|
||||
|
||||
|
||||
class BootSync(object):
|
||||
def __init__(self, cfg = None, validate = True, dryrun = False, *args, **kwargs):
|
||||
if not cfg:
|
||||
self.cfgfile = '/etc/bootsync.xml'
|
||||
else:
|
||||
self.cfgfile = os.path.abspath(os.path.expanduser(cfg))
|
||||
self.ns = None
|
||||
self.cfg = None
|
||||
self.xml = None
|
||||
self.schema = 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(validate = validate)
|
||||
self.chkMounts(dryrun = dryrun)
|
||||
self.chkReboot()
|
||||
self.getChecks()
|
||||
self.getBlkids()
|
||||
|
||||
def getCfg(self, validate = True):
|
||||
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))
|
||||
self.ns = self.cfg.nsmap.get(None, 'http://git.square-r00t.net/OpTools/tree/sys/BootSync/')
|
||||
self.ns = '{{{0}}}'.format(self.ns)
|
||||
if validate:
|
||||
if not self.schema:
|
||||
from urllib.request import urlopen
|
||||
xsi = self.cfg.nsmap.get('xsi', 'http://www.w3.org/2001/XMLSchema-instance')
|
||||
schemaLocation = '{{{0}}}schemaLocation'.format(xsi)
|
||||
schemaURL = self.cfg.attrib.get(schemaLocation,
|
||||
('http://git.square-r00t.net/OpTools/plain/sys/BootSync/bootsync.xsd'))
|
||||
with urlopen(schemaURL) as url:
|
||||
self.schema = url.read()
|
||||
self.schema = etree.XMLSchema(etree.XML(self.schema))
|
||||
self.schema.assertValid(self.xml)
|
||||
return()
|
||||
|
||||
def chkMounts(self, dryrun = False):
|
||||
if not dryrun:
|
||||
if os.geteuid() != 0:
|
||||
raise PermissionError('You must be root to write to the appropriate destinations')
|
||||
_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 dryrun:
|
||||
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):
|
||||
cmd = ['/usr/bin/blkid',
|
||||
'-o', 'export']
|
||||
if os.geteuid() != 0:
|
||||
# TODO: logger?
|
||||
print(('sudo is required to get device information. '
|
||||
'You may be prompted to enter your sudo password.'))
|
||||
cmd.insert(0, 'sudo')
|
||||
c = subprocess.run(cmd,
|
||||
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
|
||||
try:
|
||||
self.blkids[d['DEVNAME']] = d.get('UUID', d['PARTUUID'])
|
||||
except KeyError:
|
||||
try:
|
||||
self.blkids[d['DEVNAME']] = d['UUID']
|
||||
except KeyError:
|
||||
continue
|
||||
cmd = ['/usr/bin/findmnt',
|
||||
'--json',
|
||||
'-T', '/boot']
|
||||
# if os.geteuid() != 0:
|
||||
# cmd.insert(0, 'sudo')
|
||||
c = subprocess.run(cmd,
|
||||
stdout = subprocess.PIPE)
|
||||
self.dummy_uuid = self.blkids[json.loads(c.stdout.decode('utf-8'))['filesystems'][0]['source']]
|
||||
return()
|
||||
|
||||
def getChecks(self):
|
||||
# Get the default hashtype (if one exists)
|
||||
fc = self.cfg.find('{0}fileChecks'.format(self.ns))
|
||||
default_hashtype = fc.attrib.get('hashtype', 'md5').lower()
|
||||
for f in fc.findall('{0}file'.format(self.ns)):
|
||||
# We do /boot files manually in case it isn't specified as a
|
||||
# separate mount.
|
||||
file_hashtype = f.attrib.get('hashtype', default_hashtype).lower()
|
||||
rel_fpath = f.text
|
||||
fpath = os.path.join('/boot', rel_fpath)
|
||||
canon_hash = self._get_hash(fpath, file_hashtype)
|
||||
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, rel_fpath)
|
||||
file_hash = self._get_hash(new_fpath, file_hashtype)
|
||||
if not file_hashtype or file_hash != canon_hash or not file_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')
|
||||
# fileChecks are a *lot* easier.
|
||||
for rel_fpath, mounts in self.syncs.items():
|
||||
for bootdir in mounts:
|
||||
source = os.path.join('/boot', rel_fpath)
|
||||
target = os.path.join(bootdir, rel_fpath)
|
||||
destdir = os.path.dirname(target)
|
||||
if not dryrun:
|
||||
os.makedirs(destdir, exist_ok = True)
|
||||
shutil.copy2(source, target)
|
||||
bootmounts = [e.attrib['mount'] for e in self.cfg.findall('{0}partitions/{0}part'.format(self.ns))]
|
||||
# syncPaths
|
||||
syncpaths = self.cfg.find('{0}syncPaths'.format(self.ns))
|
||||
default_hashtype = syncpaths.attrib.get('hashtype', 'md5').lower()
|
||||
for syncpath in syncpaths.findall('{0}path'.format(self.ns)):
|
||||
source = os.path.abspath(os.path.expanduser(syncpath.attrib['source']))
|
||||
target = syncpath.attrib['target']
|
||||
pattern = syncpath.attrib['pattern']
|
||||
file_hashtype = syncpath.attrib.get('hashtype', default_hashtype)
|
||||
# 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(r'/?{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.
|
||||
orig_hash = self._get_hash(bootsource, file_hashtype)
|
||||
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:
|
||||
dest_hash = self._get_hash(bootfile, file_hashtype)
|
||||
if not file_hashtype or orig_hash != dest_hash:
|
||||
shutil.copy2(bootsource, 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 the array is in a degraded state, this will still let us at LEAST boot.
|
||||
line = re.sub(r'\s+--hint=[\'"]?mduuid/[a-f0-9]{32}[\'"]?', '', line)
|
||||
line = re.sub(r'^(\s*set\s+root=){0}$'.format(self.dummy_uuid),
|
||||
self.blkids[disk],
|
||||
line)
|
||||
line = re.sub(r'(?<!\=UUID\=){0}'.format(self.dummy_uuid),
|
||||
self.blkids[disk],
|
||||
line)
|
||||
line = re.sub('(^\s*|\s+)/boot', '', line)
|
||||
f.write('{0}\n'.format(line))
|
||||
return()
|
||||
|
||||
def _get_hash(self, fpathname, hashtype):
|
||||
if hashtype.lower() == 'false':
|
||||
return (None)
|
||||
if not os.path.isfile(fpathname):
|
||||
return(None)
|
||||
if hashtype not in hashlib.algorithms_available:
|
||||
raise ValueError('Hashtype {0} is not supported on this system'.format(hashtype))
|
||||
hasher = getattr(hashlib, hashtype)
|
||||
fpathname = os.path.abspath(os.path.expanduser(fpathname))
|
||||
_hash = hasher()
|
||||
with open(fpathname, 'rb') as fh:
|
||||
_hash.update(fh.read())
|
||||
return (_hash.hexdigest())
|
||||
|
||||
def _getRunningKernel(self):
|
||||
_vers = []
|
||||
# If we change the version string capture in get_file_kernel_ver(),
|
||||
# this will need to be expanded as well.
|
||||
# Really we only need to pick one, but #YOLO; why not sanity-check.
|
||||
# ALL of these should match, hence the reduction with set() down to (what SHOULD be) just 1 item.
|
||||
_vers.append(os.uname().release)
|
||||
_vers.append(platform.release())
|
||||
_vers.append(platform.uname().release)
|
||||
_vers = sorted(list(set(_vers)))
|
||||
if len(_vers) != 1:
|
||||
raise RuntimeError('Cannot reliably determine current running kernel version!')
|
||||
else:
|
||||
return(_vers[0])
|
||||
|
||||
def _getInstalledKernel(self):
|
||||
# Could we maybe remove the dependency for the "magic" module with a struct?
|
||||
# http://lxr.linux.no/#linux+v2.6.39/Documentation/x86/boot.txt
|
||||
# https://stackoverflow.com/a/11179559/733214
|
||||
try:
|
||||
len(self.cfg)
|
||||
except TypeError:
|
||||
raise RuntimeError('Tried to find the isKernel with no config set up and parsed')
|
||||
for f in self.cfg.findall('{0}fileChecks/{0}file'.format(self.ns)):
|
||||
isKernel = (True
|
||||
if f.attrib.get('isKernel', 'false').lower() in ('true', '1')
|
||||
else
|
||||
False)
|
||||
if isKernel:
|
||||
self.kernelFile = f.text
|
||||
if self.kernelFile:
|
||||
with open(os.path.join('/boot', self.kernelFile), 'rb') as fh:
|
||||
magicname = magic.detect_from_content(fh.read())
|
||||
names = [i.strip().split(None, 1) for i in magicname.name.split(',') if i.strip() != '']
|
||||
for n in names:
|
||||
if len(n) != 2:
|
||||
continue
|
||||
k, v = n
|
||||
# Note: this only grabs the version number.
|
||||
# If we want to get e.g. the build user/machine, date, etc.,
|
||||
# then we need to do a join. Shouldn't be necessary, though.
|
||||
if k.lower() == 'version':
|
||||
self.installedKernVer = v.split(None, 1)[0]
|
||||
return()
|
||||
|
||||
def parseArgs():
|
||||
args = argparse.ArgumentParser(description = ('Sync files to assist using mdadm RAID arrays with UEFI'))
|
||||
args.add_argument('-V', '--no-validate',
|
||||
dest = 'validate',
|
||||
action = 'store_false',
|
||||
help = ('If specified, do not attempt to validate the configuration file (-c/--cfg) against'
|
||||
'its schema (otherwise it is fetched dynamically and requires network connection)'))
|
||||
args.add_argument('-c', '--cfg',
|
||||
dest = 'cfg',
|
||||
default = '/etc/bootsync.xml',
|
||||
help = ('The path to the bootsync configuration file. Default is /etc/bootsync.xml'))
|
||||
args.add_argument('-n', '--dry-run',
|
||||
dest = 'dryrun',
|
||||
action = 'store_true',
|
||||
help = ('If specified, don\'t write any changes'))
|
||||
return(args)
|
||||
|
||||
def main():
|
||||
args = vars(parseArgs().parse_args())
|
||||
bs = BootSync(**args)
|
||||
bs.sync(**args)
|
||||
bs.writeConfs(**args)
|
||||
return()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
123
bootsync.xsd
Normal file
123
bootsync.xsd
Normal file
@ -0,0 +1,123 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
targetNamespace="http://git.square-r00t.net/OpTools/tree/sys/BootSync/"
|
||||
xmlns="http://git.square-r00t.net/OpTools/tree/sys/BootSync/"
|
||||
xmlns:bootsync="http://git.square-r00t.net/OpTools/tree/sys/BootSync/"
|
||||
elementFormDefault="qualified"
|
||||
attributeFormDefault="unqualified">
|
||||
|
||||
<xs:simpleType name="diskdev">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:pattern value="/dev/([A-Za-z0-9+_-]+/)?[A-Za-z0-9+_-]+[0-9]?"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="path">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:pattern value="/([A-Za-z0-9+_.-]+/)*[A-Za-z0-9+_.-]+"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="relpath">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:pattern value="([A-Za-z0-9+_.-]+/)*[A-Za-z0-9+_.-]+"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="hashtype_choice">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="false"/>
|
||||
<!-- https://docs.python.org/3/library/hashlib.html#hash-algorithms -->
|
||||
<xs:enumeration value="md5"/><!-- Not available on FIPS-compliant Python -->
|
||||
<xs:enumeration value="sha1"/>
|
||||
<xs:enumeration value="sha224"/>
|
||||
<xs:enumeration value="sha256"/>
|
||||
<xs:enumeration value="sha384"/>
|
||||
<xs:enumeration value="sha512"/>
|
||||
<xs:enumeration value="blake2b"/>
|
||||
<xs:enumeration value="blake2s"/>
|
||||
<!-- The below are only available for more recent versions of system's OpenSSL. -->
|
||||
<xs:enumeration value="sha3_224"/>
|
||||
<xs:enumeration value="sha3_256"/>
|
||||
<xs:enumeration value="sha3_384"/>
|
||||
<xs:enumeration value="sha3_512"/>
|
||||
<xs:enumeration value="shake_128"/>
|
||||
<xs:enumeration value="shake_256"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:element name="bootsync">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="partitions" maxOccurs="1" minOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="part" minOccurs="2" maxOccurs="unbounded">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="path" type="diskdev" use="required"/>
|
||||
<xs:attribute name="mount" type="path" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:unique name="partition_unique_mount">
|
||||
<xs:selector xpath="bootsync:part"/>
|
||||
<xs:field xpath="@mount"/>
|
||||
</xs:unique>
|
||||
<xs:unique name="partition_unique_path">
|
||||
<xs:selector xpath="bootsync:part"/>
|
||||
<xs:field xpath="@path"/>
|
||||
</xs:unique>
|
||||
</xs:element>
|
||||
<xs:element name="fileChecks" maxOccurs="1" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="file" maxOccurs="unbounded" minOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="relpath">
|
||||
<xs:attribute name="isKernel" type="xs:boolean"
|
||||
use="optional" default="false"/>
|
||||
<xs:attribute name="hashtype" type="hashtype_choice"
|
||||
use="optional" default="md5"/>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="hashtype" type="hashtype_choice" use="optional" default="md5"/>
|
||||
</xs:complexType>
|
||||
<xs:unique name="filechk_unique">
|
||||
<xs:selector xpath="bootsync:file"/>
|
||||
<xs:field xpath="."/>
|
||||
</xs:unique>
|
||||
</xs:element>
|
||||
<xs:element name="syncPaths" maxOccurs="1" minOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="path" maxOccurs="unbounded" minOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="source" type="path" use="required"/>
|
||||
<xs:attribute name="target" type="relpath" use="required"/>
|
||||
<!-- TODO: make this optional? -->
|
||||
<xs:attribute name="pattern" type="xs:string" use="required"/>
|
||||
<xs:attribute name="hashtype" type="hashtype_choice"
|
||||
use="optional" default="md5"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="hashtype" type="hashtype_choice" use="optional" default="md5"/>
|
||||
</xs:complexType>
|
||||
<xs:unique name="syncpath_unique_source">
|
||||
<xs:selector xpath="bootsync:path"/>
|
||||
<xs:field xpath="@source"/>
|
||||
</xs:unique>
|
||||
<xs:unique name="syncpath_unique_target">
|
||||
<xs:selector xpath="bootsync:path"/>
|
||||
<xs:field xpath="@target"/>
|
||||
</xs:unique>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
57
prep.txt
Normal file
57
prep.txt
Normal file
@ -0,0 +1,57 @@
|
||||
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/boot2 \
|
||||
--bootloader-id="Arch" \
|
||||
--efi-directory=/mnt/boot2/ \
|
||||
--target=x86_64-efi \
|
||||
--no-nvram \
|
||||
--recheck
|
||||
|
||||
# These are not strictly necessary, as the same path is used in efibootmgr for the primary and the fallback.
|
||||
# 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 (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"
|
90
sample.config.xml
Normal file
90
sample.config.xml
Normal file
@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- This example assumes Grub2 and UEFI. It should be flexible enough to tweak for other use cases
|
||||
if you know what the hell you're doing. -->
|
||||
<!--
|
||||
SEE prep.txt FOR WHAT MUST BE DONE BEFORE RUNNING BOOTSYNC.
|
||||
-->
|
||||
<bootsync xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://git.square-r00t.net/OpTools/tree/sys/BootSync/"
|
||||
xsi:schemaLocation="http://git.square-r00t.net/OpTools/plain/sys/BootSync/bootsync.xsd">
|
||||
<!-- The actual EFI System Partitions (ESP). At least two are required. -->
|
||||
<!-- Each drive should be prepared ahead of time with efibootmgr and grub-install (see prep.txt). -->
|
||||
<partitions>
|
||||
<!-- These should all be unique values.
|
||||
Don't do something dumb like mount different partitions on the same mountpoint or the
|
||||
the same partition on multiple mountpoints. -->
|
||||
<!-- Future versions of this script may support things like UUIDs or labels as an alternative to path=,
|
||||
but in the meanwhile it relies on device paths explicitly. -->
|
||||
<part path="/dev/sdb1" mount="/mnt/boot1"/>
|
||||
<part path="/dev/sdd1" mount="/mnt/boot2"/>
|
||||
</partitions>
|
||||
<!--
|
||||
If fileChecks isn't provided, we will still sync syncPaths (which IS required).
|
||||
These files will be evaluated to and synced to the ESP partitions with the same relative path.
|
||||
i.e. (part[@mount])/(file)
|
||||
|
||||
fileChecks[@hashtype] attribute is used to checksum all <file> children,
|
||||
unless they specify their own hashtype attribute (the default is hashtype="md5" for speed optimizations).
|
||||
|
||||
Guaranteed valid hashtype values are:
|
||||
|
||||
sha1
|
||||
sha224
|
||||
sha256
|
||||
sha384
|
||||
sha512
|
||||
blake2b
|
||||
blake2s
|
||||
|
||||
If you have a non-FIPS-compliant Python (you very most likely do), the following hashtype is also available:
|
||||
|
||||
md5
|
||||
|
||||
If you have a recent enough OpenSSL (and python3), you have the additional hashtype values available:
|
||||
|
||||
sha3_224
|
||||
sha3_256
|
||||
sha3_384
|
||||
sha3_512
|
||||
shake_128
|
||||
shake_256
|
||||
|
||||
Additionally, the value "false" is always available to disable hashing for a specific <file> object or to set
|
||||
the default. No checksumming will be done in this case and the file will always be overwritten on every run.
|
||||
|
||||
This is *highly not recommended* for solid-state disks or if you have large files you plan on syncing to
|
||||
the alternate ESPs (e.g. loop-mounted ISO/IMG files).
|
||||
|
||||
"isKernel" is by default false, but if true it is treated specially to get a kernel version.
|
||||
This is to notify you if a reboot is required. If no <file> item is isKernel="true", no reboot requirement
|
||||
detection will be done.
|
||||
Only *the last* <file> with isKernel="true" listed is used as the kernel identifier.
|
||||
-->
|
||||
<fileChecks hashtype="sha1">
|
||||
<!-- RELATIVE paths to /boot on your / mount. -->
|
||||
<file>initramfs-linux.img</file>
|
||||
<file>intel-ucode.img</file>
|
||||
<file>memtest86+/memtest.bin</file>
|
||||
<file isKernel="true" hashtype="sha512">vmlinuz-linux</file>
|
||||
</fileChecks>
|
||||
<!--
|
||||
These are system paths to sync to the ESPs. They are synced recursively.
|
||||
|
||||
"hashtype" is also supported for syncPaths and path objects just as the same with fileChecks
|
||||
and file objects (see above).
|
||||
|
||||
"source" should be absolute.
|
||||
"target" should be relative (to the ESP mount).
|
||||
"pattern" is a regex to match to restrict *filenames* to sync. Use ".*" for all files.
|
||||
-->
|
||||
<syncPaths hashtype="sha1">
|
||||
<!-- For example, these are grub theme files. -->
|
||||
<path source="/usr/share/grub/themes" target="grub/themes" pattern=".*"/>
|
||||
<!-- These are grub modules - specifically, UEFI. -->
|
||||
<path source="/usr/lib/grub/x86_64-efi" target="grub/x86_64-efi" pattern="^.*\.(mod|lst|sh)$"/>
|
||||
<!-- And these are ISO files, in case you're like me and do loop mounting in Grub for rescue situations.
|
||||
(e.g. https://wiki.archlinux.org/index.php/Multiboot_USB_drive#Using_GRUB_and_loopback_devices) -->
|
||||
<!-- <path hashtype="false" source="/boot/iso" target="iso" pattern="^.*\.(iso|img)$"/> -->
|
||||
<path hashtype="md5" source="/boot/iso" target="iso" pattern=".*"/>
|
||||
</syncPaths>
|
||||
</bootsync>
|
Loading…
Reference in New Issue
Block a user