finalized. hashtype incorporated into code, streamlined, etc.
This commit is contained in:
parent
af732a1d64
commit
31826960c1
@ -15,7 +15,7 @@ from lxml import etree
|
|||||||
|
|
||||||
|
|
||||||
class BootSync(object):
|
class BootSync(object):
|
||||||
def __init__(self, cfg = None, validate = True, *args, **kwargs):
|
def __init__(self, cfg = None, validate = True, dryrun = False, *args, **kwargs):
|
||||||
if not cfg:
|
if not cfg:
|
||||||
self.cfgfile = '/etc/bootsync.xml'
|
self.cfgfile = '/etc/bootsync.xml'
|
||||||
else:
|
else:
|
||||||
@ -35,9 +35,9 @@ class BootSync(object):
|
|||||||
self.syncs = {}
|
self.syncs = {}
|
||||||
##
|
##
|
||||||
self.getCfg(validate = validate)
|
self.getCfg(validate = validate)
|
||||||
self.chkMounts()
|
self.chkMounts(dryrun = dryrun)
|
||||||
self.chkReboot()
|
self.chkReboot()
|
||||||
self.getHashes()
|
self.getChecks()
|
||||||
self.getBlkids()
|
self.getBlkids()
|
||||||
|
|
||||||
def getCfg(self, validate = True):
|
def getCfg(self, validate = True):
|
||||||
@ -67,22 +67,26 @@ class BootSync(object):
|
|||||||
self.schema.assertValid(self.xml)
|
self.schema.assertValid(self.xml)
|
||||||
return()
|
return()
|
||||||
|
|
||||||
def chkMounts(self):
|
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)}
|
_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)):
|
for esp in self.cfg.findall('{0}partitions/{0}part'.format(self.ns)):
|
||||||
disk = esp.attrib['path']
|
disk = esp.attrib['path']
|
||||||
mount = os.path.abspath(os.path.expanduser(esp.attrib['mount']))
|
mount = os.path.abspath(os.path.expanduser(esp.attrib['mount']))
|
||||||
if not os.path.isdir(mount):
|
if not dryrun:
|
||||||
os.makedirs(mount, exist_ok = True)
|
if not os.path.isdir(mount):
|
||||||
if disk not in _mounts:
|
os.makedirs(mount, exist_ok = True)
|
||||||
with open(os.devnull, 'w') as devnull:
|
if disk not in _mounts:
|
||||||
c = subprocess.run(['/usr/bin/mount', mount],
|
with open(os.devnull, 'w') as devnull:
|
||||||
stderr = devnull)
|
c = subprocess.run(['/usr/bin/mount', mount],
|
||||||
if c.returncode == 1: # Not specified in fstab
|
stderr = devnull)
|
||||||
subprocess.run(['/usr/bin/mount', disk, mount],
|
if c.returncode == 1: # Not specified in fstab
|
||||||
stderr = devnull)
|
subprocess.run(['/usr/bin/mount', disk, mount],
|
||||||
elif c.returncode == 32: # Already mounted
|
stderr = devnull)
|
||||||
pass
|
elif c.returncode == 32: # Already mounted
|
||||||
|
pass
|
||||||
return()
|
return()
|
||||||
|
|
||||||
def chkReboot(self):
|
def chkReboot(self):
|
||||||
@ -99,8 +103,14 @@ class BootSync(object):
|
|||||||
return()
|
return()
|
||||||
|
|
||||||
def getBlkids(self):
|
def getBlkids(self):
|
||||||
c = subprocess.run(['/usr/bin/blkid',
|
cmd = ['/usr/bin/blkid',
|
||||||
'-o', 'export'],
|
'-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)
|
stdout = subprocess.PIPE)
|
||||||
if c.returncode != 0:
|
if c.returncode != 0:
|
||||||
raise RuntimeError('Could not fetch block ID information')
|
raise RuntimeError('Could not fetch block ID information')
|
||||||
@ -116,32 +126,32 @@ class BootSync(object):
|
|||||||
self.blkids[d['DEVNAME']] = d['UUID']
|
self.blkids[d['DEVNAME']] = d['UUID']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
continue
|
continue
|
||||||
c = subprocess.run(['/usr/bin/findmnt',
|
cmd = ['/usr/bin/findmnt',
|
||||||
'--json',
|
'--json',
|
||||||
'-T', '/boot'],
|
'-T', '/boot']
|
||||||
|
# if os.geteuid() != 0:
|
||||||
|
# cmd.insert(0, 'sudo')
|
||||||
|
c = subprocess.run(cmd,
|
||||||
stdout = subprocess.PIPE)
|
stdout = subprocess.PIPE)
|
||||||
# I write ridiculous one-liners.
|
|
||||||
self.dummy_uuid = self.blkids[json.loads(c.stdout.decode('utf-8'))['filesystems'][0]['source']]
|
self.dummy_uuid = self.blkids[json.loads(c.stdout.decode('utf-8'))['filesystems'][0]['source']]
|
||||||
return()
|
return()
|
||||||
|
|
||||||
def getHashes(self):
|
def getChecks(self):
|
||||||
def _get_hash(fpathname):
|
# Get the default hashtype (if one exists)
|
||||||
fpathname = os.path.abspath(os.path.expanduser(fpathname))
|
fc = self.cfg.find('{0}fileChecks'.format(self.ns))
|
||||||
_hash = hashlib.sha512()
|
default_hashtype = fc.attrib.get('hashtype', 'md5').lower()
|
||||||
with open(fpathname, 'rb') as fh:
|
for f in fc.findall('{0}file'.format(self.ns)):
|
||||||
_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
|
# We do /boot files manually in case it isn't specified as a
|
||||||
# separate mount.
|
# separate mount.
|
||||||
|
file_hashtype = f.attrib.get('hashtype', default_hashtype).lower()
|
||||||
rel_fpath = f.text
|
rel_fpath = f.text
|
||||||
fpath = os.path.join('/boot', rel_fpath)
|
fpath = os.path.join('/boot', rel_fpath)
|
||||||
canon_hash = _get_hash(fpath)
|
canon_hash = self._get_hash(fpath, file_hashtype)
|
||||||
for esp in self.cfg.findall('{0}partitions/{0}part'.format(self.ns)):
|
for esp in self.cfg.findall('{0}partitions/{0}part'.format(self.ns)):
|
||||||
mount = os.path.abspath(os.path.expanduser(esp.attrib['mount']))
|
mount = os.path.abspath(os.path.expanduser(esp.attrib['mount']))
|
||||||
new_fpath = os.path.join(mount, f)
|
new_fpath = os.path.join(mount, rel_fpath)
|
||||||
file_hash = _get_hash(new_fpath)
|
file_hash = self._get_hash(new_fpath, file_hashtype)
|
||||||
if file_hash != canon_hash:
|
if not file_hashtype or file_hash != canon_hash or not file_hash:
|
||||||
if rel_fpath not in self.syncs:
|
if rel_fpath not in self.syncs:
|
||||||
self.syncs[rel_fpath] = []
|
self.syncs[rel_fpath] = []
|
||||||
self.syncs[rel_fpath].append(mount)
|
self.syncs[rel_fpath].append(mount)
|
||||||
@ -151,24 +161,30 @@ class BootSync(object):
|
|||||||
if not dryrun:
|
if not dryrun:
|
||||||
if os.geteuid() != 0:
|
if os.geteuid() != 0:
|
||||||
raise PermissionError('You must be root to write to the appropriate destinations')
|
raise PermissionError('You must be root to write to the appropriate destinations')
|
||||||
for f in self.syncs:
|
# fileChecks are a *lot* easier.
|
||||||
for m in self.syncs[f]:
|
for rel_fpath, mounts in self.syncs.items():
|
||||||
orig = os.path.join('/boot', f)
|
for bootdir in mounts:
|
||||||
dest = os.path.join(m, f)
|
source = os.path.join('/boot', rel_fpath)
|
||||||
|
target = os.path.join(bootdir, rel_fpath)
|
||||||
|
destdir = os.path.dirname(target)
|
||||||
if not dryrun:
|
if not dryrun:
|
||||||
shutil.copy2(orig, dest)
|
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))]
|
bootmounts = [e.attrib['mount'] for e in self.cfg.findall('{0}partitions/{0}part'.format(self.ns))]
|
||||||
# syncPaths
|
# syncPaths
|
||||||
for syncpath in self.cfg.findall('{0}syncPaths/{0}path'.format(self.ns)):
|
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']))
|
source = os.path.abspath(os.path.expanduser(syncpath.attrib['source']))
|
||||||
target = syncpath.attrib['target']
|
target = syncpath.attrib['target']
|
||||||
pattern = syncpath.attrib['pattern']
|
pattern = syncpath.attrib['pattern']
|
||||||
|
file_hashtype = syncpath.attrib.get('hashtype', default_hashtype)
|
||||||
# We don't use filecmp for this because:
|
# We don't use filecmp for this because:
|
||||||
# - dircmp doesn't recurse
|
# - dircmp doesn't recurse
|
||||||
# - the reports/lists don't retain relative paths
|
# - the reports/lists don't retain relative paths
|
||||||
# - we can't regex out files
|
# - we can't regex out files
|
||||||
for root, dirs, files in os.walk(source):
|
for root, dirs, files in os.walk(source):
|
||||||
prefix = re.sub('\/?{0}\/?'.format(source), '', root)
|
prefix = re.sub(r'/?{0}/?'.format(source), '', root)
|
||||||
ptrn = re.compile(pattern)
|
ptrn = re.compile(pattern)
|
||||||
for f in files:
|
for f in files:
|
||||||
fname_path = os.path.join(prefix, f)
|
fname_path = os.path.join(prefix, f)
|
||||||
@ -176,8 +192,7 @@ class BootSync(object):
|
|||||||
boottarget = os.path.join(target, fname_path)
|
boottarget = os.path.join(target, fname_path)
|
||||||
if ptrn.search(f):
|
if ptrn.search(f):
|
||||||
# Compare the contents.
|
# Compare the contents.
|
||||||
with open(bootsource, 'rb') as fh:
|
orig_hash = self._get_hash(bootsource, file_hashtype)
|
||||||
orig_hash = hashlib.sha512(fh.read()).hexdigest()
|
|
||||||
for bootdir in bootmounts:
|
for bootdir in bootmounts:
|
||||||
bootfile = os.path.join(bootdir, boottarget)
|
bootfile = os.path.join(bootdir, boottarget)
|
||||||
if not dryrun:
|
if not dryrun:
|
||||||
@ -186,27 +201,9 @@ class BootSync(object):
|
|||||||
exist_ok = True)
|
exist_ok = True)
|
||||||
shutil.copy2(bootsource, bootfile)
|
shutil.copy2(bootsource, bootfile)
|
||||||
else:
|
else:
|
||||||
with open(bootfile, 'rb') as fh:
|
dest_hash = self._get_hash(bootfile, file_hashtype)
|
||||||
dest_hash = hashlib.sha512(fh.read()).hexdigest()
|
if not file_hashtype or orig_hash != dest_hash:
|
||||||
if orig_hash != dest_hash:
|
|
||||||
shutil.copy2(bootsource, bootfile)
|
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()
|
return()
|
||||||
|
|
||||||
|
|
||||||
@ -243,6 +240,17 @@ class BootSync(object):
|
|||||||
f.write('{0}\n'.format(line))
|
f.write('{0}\n'.format(line))
|
||||||
return()
|
return()
|
||||||
|
|
||||||
|
def _get_hash(self, fpathname, hashtype):
|
||||||
|
if hashtype.lower() == 'false':
|
||||||
|
return (None)
|
||||||
|
if not os.path.isfile(fpathname):
|
||||||
|
return(None)
|
||||||
|
fpathname = os.path.abspath(os.path.expanduser(fpathname))
|
||||||
|
_hash = hashlib.sha512()
|
||||||
|
with open(fpathname, 'rb') as fh:
|
||||||
|
_hash.update(fh.read())
|
||||||
|
return (_hash.hexdigest())
|
||||||
|
|
||||||
def _getRunningKernel(self):
|
def _getRunningKernel(self):
|
||||||
_vers = []
|
_vers = []
|
||||||
# If we change the version string capture in get_file_kernel_ver(),
|
# If we change the version string capture in get_file_kernel_ver(),
|
||||||
|
@ -15,14 +15,6 @@ PREPARATION:
|
|||||||
--no-nvram \
|
--no-nvram \
|
||||||
--recheck
|
--recheck
|
||||||
|
|
||||||
grub-install \
|
|
||||||
--boot-directory=/mnt/boot1 \
|
|
||||||
--bootloader-id="Arch (Fallback)" \
|
|
||||||
--efi-directory=/mnt/boot1/ \
|
|
||||||
--target=x86_64-efi \
|
|
||||||
--no-nvram \
|
|
||||||
--recheck
|
|
||||||
|
|
||||||
grub-install \
|
grub-install \
|
||||||
--boot-directory=/mnt/boot2 \
|
--boot-directory=/mnt/boot2 \
|
||||||
--bootloader-id=Arch \
|
--bootloader-id=Arch \
|
||||||
@ -31,13 +23,22 @@ PREPARATION:
|
|||||||
--no-nvram \
|
--no-nvram \
|
||||||
--recheck
|
--recheck
|
||||||
|
|
||||||
grub-install \
|
# These are not strictly necessary, as the same path is used in efibootmgr for the primary and the fallback.
|
||||||
--boot-directory=/mnt/boot2 \
|
# grub-install \
|
||||||
--bootloader-id="Arch (Fallback)" \
|
# --boot-directory=/mnt/boot1 \
|
||||||
--efi-directory=/mnt/boot2/ \
|
# --bootloader-id="Arch (Fallback)" \
|
||||||
--target=x86_64-efi \
|
# --efi-directory=/mnt/boot1/ \
|
||||||
--no-nvram \
|
# --target=x86_64-efi \
|
||||||
--recheck
|
# --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.
|
3.) Prepare the ESPs. See sample.config.xml for context for the below examples.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user