adding bootsync
This commit is contained in:
		
							parent
							
								
									b8622c4462
								
							
						
					
					
						commit
						981d92db92
					
				
							
								
								
									
										249
									
								
								storage/bootsync.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										249
									
								
								storage/bootsync.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,249 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env python
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import hashlib
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					# From http://darwinsys.com/file/, not https://github.com/ahupp/python-magic
 | 
				
			||||||
 | 
					import magic
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import platform
 | 
				
			||||||
 | 
					import psutil
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import shutil
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# The device:mountpoint of the mounts for the failover partitions.
 | 
				
			||||||
 | 
					mounts = {'/dev/sdb1': '/mnt/boot1',
 | 
				
			||||||
 | 
					          '/dev/sdd1': '/mnt/boot2'}
 | 
				
			||||||
 | 
					# The files we checksum.
 | 
				
			||||||
 | 
					files = ['initramfs-linux.img', 'intel-ucode.img', 'memtest86+/memtest.bin',
 | 
				
			||||||
 | 
					         'vmlinuz-linux']
 | 
				
			||||||
 | 
					# These paths are used to ensure an up-to-date grub.
 | 
				
			||||||
 | 
					grub = {'themes': {'orig': '/usr/share/grub/themes',
 | 
				
			||||||
 | 
					                   'dest': 'grub/themes',
 | 
				
			||||||
 | 
					                   'pattern': '.*'},
 | 
				
			||||||
 | 
					        'modules': {'orig': '/usr/lib/grub/x86_64-efi',
 | 
				
			||||||
 | 
					                    'dest': 'grub/x86_64-efi',
 | 
				
			||||||
 | 
					                    'pattern': '^.*\.(mod|lst|sh)$'},
 | 
				
			||||||
 | 
					        'isos': {'orig': '/boot/iso',
 | 
				
			||||||
 | 
					                 'dest': 'iso',
 | 
				
			||||||
 | 
					                 'pattern': '^.*\.(iso|img)$'}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					###############################################################################
 | 
				
			||||||
 | 
					# NOTE:
 | 
				
			||||||
 | 
					# If I need to rebuild,
 | 
				
			||||||
 | 
					# efibootmgr -c -d /dev/<PRIMARY DEVICE> -p <PARTITION NUMBER> \
 | 
				
			||||||
 | 
					#       -l /EFI/Arch/grubx64.efi -L Arch
 | 
				
			||||||
 | 
					# efibootmgr -c -d /dev/<FALLBACK DEVICE> -p <PARTITION NUMBER> \
 | 
				
			||||||
 | 
					#       -l /EFI/Arch/grubx64.efi -L 'Arch (Fallback)'
 | 
				
			||||||
 | 
					# And don't forget to install grub.
 | 
				
			||||||
 | 
					# 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
 | 
				
			||||||
 | 
					# You need to have grub's config set to use UUIDs.
 | 
				
			||||||
 | 
					###############################################################################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_file_kernel_ver(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'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_cur_kernel_ver():
 | 
				
			||||||
 | 
					    _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.
 | 
				
			||||||
 | 
					    _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])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BootSync(object):
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        self.chk_mounts()
 | 
				
			||||||
 | 
					        # This is the current live kernel.
 | 
				
			||||||
 | 
					        self.cur_kern_ver = get_cur_kernel_ver()
 | 
				
			||||||
 | 
					        # This is the installed kernel from Pacman.
 | 
				
			||||||
 | 
					        self.installed_kern_ver = get_file_kernel_ver('/boot/vmlinuz-linux')
 | 
				
			||||||
 | 
					        self.reboot = False  # If a reboot is needed (WARN, don't execute!)
 | 
				
			||||||
 | 
					        self.syncs = {}
 | 
				
			||||||
 | 
					        self.blkids = {}
 | 
				
			||||||
 | 
					        self.dummy_uuid = None
 | 
				
			||||||
 | 
					        self.chk_reboot()
 | 
				
			||||||
 | 
					        self.get_hashes()
 | 
				
			||||||
 | 
					        self.get_blkids()
 | 
				
			||||||
 | 
					        self.sync()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def chk_mounts(self):
 | 
				
			||||||
 | 
					        _mounts = {m.device:m.mountpoint for m in \
 | 
				
			||||||
 | 
					                                            psutil.disk_partitions(all = True)}
 | 
				
			||||||
 | 
					        for m in mounts:
 | 
				
			||||||
 | 
					            mntpt = os.path.abspath(os.path.expanduser(mounts[m]))
 | 
				
			||||||
 | 
					            if not os.path.isdir(mntpt):
 | 
				
			||||||
 | 
					                os.makedirs(mntpt, exist_ok = True)
 | 
				
			||||||
 | 
					            if m not in _mounts:
 | 
				
			||||||
 | 
					                with open(os.devnull, 'w') as devnull:
 | 
				
			||||||
 | 
					                    c = subprocess.run(['/usr/bin/mount', mounts[m]], stderr = devnull)
 | 
				
			||||||
 | 
					                    if c.returncode == 1:  # Not specified in fstab
 | 
				
			||||||
 | 
					                        subprocess.run(['/usr/bin/mount', m, mntpt], stderr = devnull)
 | 
				
			||||||
 | 
					                    elif c.returncode == 32:  # Already mounted
 | 
				
			||||||
 | 
					                        pass
 | 
				
			||||||
 | 
					        return()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def chk_reboot(self):
 | 
				
			||||||
 | 
					        if self.installed_kern_ver != self.cur_kern_ver:
 | 
				
			||||||
 | 
					            self.reboot = True
 | 
				
			||||||
 | 
					            print(
 | 
				
			||||||
 | 
					                'NOTE: REBOOT REQUIRED. New kernel is {0}. Running kernel is '
 | 
				
			||||||
 | 
					                '{1}.'.format(self.installed_kern_ver, self.cur_kern_ver))
 | 
				
			||||||
 | 
					        return()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_blkids(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 'PARTUUID' in d:
 | 
				
			||||||
 | 
					                self.blkids[d['DEVNAME']] = d['PARTUUID']
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                self.blkids[d['DEVNAME']] = 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 get_hashes(self):
 | 
				
			||||||
 | 
					        def _get_hash(fpath):
 | 
				
			||||||
 | 
					            fpath = os.path.abspath(os.path.expanduser(fpath))
 | 
				
			||||||
 | 
					            _hash = hashlib.sha512()
 | 
				
			||||||
 | 
					            with open(fpath, 'rb') as fh:
 | 
				
			||||||
 | 
					                _hash.update(fh.read())
 | 
				
			||||||
 | 
					            return(_hash.hexdigest())
 | 
				
			||||||
 | 
					        for f in files:
 | 
				
			||||||
 | 
					            # We do /boot files manually in case it isn't specified as a
 | 
				
			||||||
 | 
					            # separate mount.
 | 
				
			||||||
 | 
					            fpath = os.path.join('/boot', f)
 | 
				
			||||||
 | 
					            canon_hash = _get_hash(fpath)
 | 
				
			||||||
 | 
					            for m in mounts:
 | 
				
			||||||
 | 
					                fpath = os.path.join(mounts[m], f)
 | 
				
			||||||
 | 
					                file_hash = _get_hash(fpath)
 | 
				
			||||||
 | 
					                if file_hash != canon_hash:
 | 
				
			||||||
 | 
					                    if f not in self.syncs:
 | 
				
			||||||
 | 
					                        self.syncs[f] = []
 | 
				
			||||||
 | 
					                    self.syncs[f].append(mounts[m])
 | 
				
			||||||
 | 
					        return()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def sync(self):
 | 
				
			||||||
 | 
					        # NOTE: We *may* be able to get away with instead just doing the above
 | 
				
			||||||
 | 
					        # grub-install commands to each of the boot disks.
 | 
				
			||||||
 | 
					        for f in self.syncs:
 | 
				
			||||||
 | 
					            for m in self.syncs[f]:
 | 
				
			||||||
 | 
					                orig = os.path.join('/boot', f)
 | 
				
			||||||
 | 
					                dest = os.path.join(m, f)
 | 
				
			||||||
 | 
					                shutil.copy2(orig, dest)
 | 
				
			||||||
 | 
					        _mounts = list(mounts.values()) + ['/boot']
 | 
				
			||||||
 | 
					        for g in grub:
 | 
				
			||||||
 | 
					            _fnames = []
 | 
				
			||||||
 | 
					            # 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(grub[g]['orig']):
 | 
				
			||||||
 | 
					                prefix = re.sub('\/?{0}\/?'.format(grub[g]['orig']), '', root)
 | 
				
			||||||
 | 
					                ptrn = re.compile(grub[g]['pattern'])
 | 
				
			||||||
 | 
					                for f in files:
 | 
				
			||||||
 | 
					                    if ptrn.search(f):
 | 
				
			||||||
 | 
					                        _fnames.append(os.path.join(prefix, f))
 | 
				
			||||||
 | 
					            # If we want to delete files in the destination that don't exist in
 | 
				
			||||||
 | 
					            # the original, here's where we would do it.
 | 
				
			||||||
 | 
					            # for root, dirs, files in os.walk(grub[g]['dest']):
 | 
				
			||||||
 | 
					            #     _pre_prefix = re.sub('\/?$', '', grub[g]['dest'])
 | 
				
			||||||
 | 
					            #     prefix = re.sub(_pre_prefix, '', root)
 | 
				
			||||||
 | 
					            #     #ptrn = re.compile(grub[g]['pattern'])
 | 
				
			||||||
 | 
					            #     for f in files:
 | 
				
			||||||
 | 
					            #         _p = os.path.join(prefix, f)
 | 
				
			||||||
 | 
					            #         if _p not in _fnames:
 | 
				
			||||||
 | 
					            #             os.remove(os.path.join(grub[g]['dest'], _p))
 | 
				
			||||||
 | 
					            # Now we compare the contents.
 | 
				
			||||||
 | 
					            for f in _fnames:
 | 
				
			||||||
 | 
					                origfile = os.path.join(grub[g]['orig'], f)
 | 
				
			||||||
 | 
					                destfile = os.path.join(grub[g]['dest'], f)
 | 
				
			||||||
 | 
					                with open(origfile, 'rb') as f:
 | 
				
			||||||
 | 
					                    _orig = hashlib.sha512(f.read()).hexdigest()
 | 
				
			||||||
 | 
					                for m in _mounts:
 | 
				
			||||||
 | 
					                    real_destfile = os.path.join(m, destfile)
 | 
				
			||||||
 | 
					                    if not os.path.isfile(real_destfile):
 | 
				
			||||||
 | 
					                        os.makedirs(os.path.dirname(real_destfile),
 | 
				
			||||||
 | 
					                                    exist_ok = True)
 | 
				
			||||||
 | 
					                        shutil.copy2(origfile, real_destfile)
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
 | 
					                        with open(real_destfile, 'rb') as f:
 | 
				
			||||||
 | 
					                            _dest = hashlib.sha512(f.read()).hexdigest()
 | 
				
			||||||
 | 
					                        if _orig != _dest:
 | 
				
			||||||
 | 
					                            shutil.copy2(origfile, real_destfile)
 | 
				
			||||||
 | 
					        return()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def write_confs(self):
 | 
				
			||||||
 | 
					        # 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 d in mounts:
 | 
				
			||||||
 | 
					            with open(os.path.join(mounts[d], 'grub/grub.cfg'), 'w') as f:
 | 
				
			||||||
 | 
					                for line in _grubcfg.splitlines():
 | 
				
			||||||
 | 
					                    i = re.sub('(?<!\=UUID\=){0}'.format(self.dummy_uuid),
 | 
				
			||||||
 | 
					                               self.blkids[d],
 | 
				
			||||||
 | 
					                               line)
 | 
				
			||||||
 | 
					                    i = re.sub('\s--hint=\'mduuid\/[a-f0-9]{32}\'', '', i)
 | 
				
			||||||
 | 
					                    f.write('{0}\n'.format(i))
 | 
				
			||||||
 | 
					        return()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    if os.geteuid() != 0:
 | 
				
			||||||
 | 
					        exit('You must be root to run this!')
 | 
				
			||||||
 | 
					    bs = BootSync()
 | 
				
			||||||
 | 
					    bs.sync()
 | 
				
			||||||
 | 
					    bs.write_confs()
 | 
				
			||||||
 | 
					    return()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user