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):
|
||||
def __init__(self, cfg = None, validate = True, *args, **kwargs):
|
||||
def __init__(self, cfg = None, validate = True, dryrun = False, *args, **kwargs):
|
||||
if not cfg:
|
||||
self.cfgfile = '/etc/bootsync.xml'
|
||||
else:
|
||||
@ -35,9 +35,9 @@ class BootSync(object):
|
||||
self.syncs = {}
|
||||
##
|
||||
self.getCfg(validate = validate)
|
||||
self.chkMounts()
|
||||
self.chkMounts(dryrun = dryrun)
|
||||
self.chkReboot()
|
||||
self.getHashes()
|
||||
self.getChecks()
|
||||
self.getBlkids()
|
||||
|
||||
def getCfg(self, validate = True):
|
||||
@ -67,11 +67,15 @@ class BootSync(object):
|
||||
self.schema.assertValid(self.xml)
|
||||
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)}
|
||||
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:
|
||||
@ -99,8 +103,14 @@ class BootSync(object):
|
||||
return()
|
||||
|
||||
def getBlkids(self):
|
||||
c = subprocess.run(['/usr/bin/blkid',
|
||||
'-o', 'export'],
|
||||
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')
|
||||
@ -116,32 +126,32 @@ class BootSync(object):
|
||||
self.blkids[d['DEVNAME']] = d['UUID']
|
||||
except KeyError:
|
||||
continue
|
||||
c = subprocess.run(['/usr/bin/findmnt',
|
||||
cmd = ['/usr/bin/findmnt',
|
||||
'--json',
|
||||
'-T', '/boot'],
|
||||
'-T', '/boot']
|
||||
# if os.geteuid() != 0:
|
||||
# cmd.insert(0, 'sudo')
|
||||
c = subprocess.run(cmd,
|
||||
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'):
|
||||
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 = _get_hash(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, f)
|
||||
file_hash = _get_hash(new_fpath)
|
||||
if file_hash != canon_hash:
|
||||
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)
|
||||
@ -151,24 +161,30 @@ class BootSync(object):
|
||||
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)
|
||||
# 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:
|
||||
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))]
|
||||
# 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']))
|
||||
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('\/?{0}\/?'.format(source), '', root)
|
||||
prefix = re.sub(r'/?{0}/?'.format(source), '', root)
|
||||
ptrn = re.compile(pattern)
|
||||
for f in files:
|
||||
fname_path = os.path.join(prefix, f)
|
||||
@ -176,8 +192,7 @@ class BootSync(object):
|
||||
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()
|
||||
orig_hash = self._get_hash(bootsource, file_hashtype)
|
||||
for bootdir in bootmounts:
|
||||
bootfile = os.path.join(bootdir, boottarget)
|
||||
if not dryrun:
|
||||
@ -186,27 +201,9 @@ class BootSync(object):
|
||||
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:
|
||||
dest_hash = self._get_hash(bootfile, file_hashtype)
|
||||
if not file_hashtype or 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()
|
||||
|
||||
|
||||
@ -243,6 +240,17 @@ class BootSync(object):
|
||||
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)
|
||||
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):
|
||||
_vers = []
|
||||
# If we change the version string capture in get_file_kernel_ver(),
|
||||
|
@ -15,14 +15,6 @@ PREPARATION:
|
||||
--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 \
|
||||
@ -31,13 +23,22 @@ PREPARATION:
|
||||
--no-nvram \
|
||||
--recheck
|
||||
|
||||
grub-install \
|
||||
--boot-directory=/mnt/boot2 \
|
||||
--bootloader-id="Arch (Fallback)" \
|
||||
--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.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user