#!/usr/bin/env python3 import os import subprocess import random import string import tempfile import urllib.request import math import optparse from invirt import database def lvcreate(name, size): subprocess.run(['lvcreate', '-L', size, '-n', name, 'xenvg'], check_output=True, encoding='utf-8', check=True) def lvrename(dest, src): subprocess.run(['lvchange', '-an', f'xenvg/{src}'], check_output=True, encoding='utf-8', check=True) subprocess.run(['lvrename', f'xenvg/{src}', f'xenvg/{dest}'], check_output=True, encoding='utf-8', check=True) subprocess.run(['lvchange', '-ay', f'xenvg/{dest}'], check_output=True, encoding='utf-8', check=True) def lv_random(func, pattern, *args): """ Run some LVM-related command with a random string in the LV name. func takes an LV name plus whatever's in *args and returns the return code of some LVM command, such as lvcreate or lvrename pattern must contain one '{}' pattern, which will be replaced by a 6-character random string. the script will attempt to re-run itself if the error code indicates that the destination already exists """ # Keep trying until it works while True: letters = (random.choice(string.ascii_letters) for _ in range(6)) rand_string = ''.join(letters) name = pattern.format(rand_string) try: func(name, *args) except subprocess.CalledProcessError as e: # 5 is the return code if the destination already exists if e.returncode != 5: raise else: return name def fetch_image(cdrom): """ Download a cdrom from a URI, shelling out to rsync if appropriate and otherwise trying to use urllib """ full_uri = os.path.join(cdrom.mirror.uri_prefix, cdrom.uri_suffix) temp_file = tempfile.mkstemp()[1] print(f'Fetching image {cdrom.cdrom_id} from {full_uri} to {temp_file}') try: if full_uri.startswith('rsync://'): subprocess.run(['rsync', '--no-motd', '-tLP', full_uri, temp_file], check_output=True, encoding='utf-8', check=True) else: # I'm not going to look for errors here, because I bet it'll # throw its own exceptions urllib.request.urlretrieve(full_uri, temp_file) return temp_file except: os.remove(temp_file) raise def copy_file(src, dest): """ Copy a file from one location to another using dd """ subprocess.run(['dd', f'if={src}', f'of={dest}', 'bs=1M'], check_output=True, encoding='utf-8', check=True) def load_image(cdrom): """ Update a cdrom image by downloading the latest version, transferring it into an LV, moving the old LV out of the way and the new LV into place """ if cdrom.mirror_id is None: return temp_file = fetch_image(cdrom) st_size = os.stat(temp_file).st_size assert st_size > 0, 'CD-ROM image size is 0' megabytes = math.ceil((float(st_size) / (1024 * 1024))) cdrom_size = f'{megabytes}M' try: new_lv = lv_random(lvcreate, f'image-new_{cdrom.cdrom_id}' + '_{}', cdrom_size) copy_file(temp_file, f'/dev/xenvg/{new_lv}') lv_random(lvrename, f'image-old_{cdrom.cdrom_id}' + '_{}', 'image_{cdrom.cdrom_id}') lv_random(lvrename, f'image_{cdrom.cdrom_id}', new_lv) reap_images() finally: os.remove(temp_file) def reap_images(): """ Remove stale cdrom images that are no longer in use load_image doesn't attempt to remove the old image because it might still be in use. reap_images attempts to delete any LVs starting with 'image-old_', but ignores errors, in case they're still being used. """ lvm_list = subprocess.run(['lvs', '-o', 'lv_name', '--noheadings'], check_output=True, encoding='utf-8', check=True) for lv in (s.strip() for s in lvm_list.stdout.readlines()): if lv.startswith('image-old_'): subprocess.run(['lvchange', '-a', 'n', f'/dev/xenvg/{lv}']) subprocess.run(['lvchange', '-a', 'n', f'/dev/xenvg/{lv}']) subprocess.run(['lvchange', '-a', 'ey', f'/dev/xenvg/{lv}']) subprocess.run(['lvremove', '--force', f'/dev/xenvg/{lv}']) def main(): database.connect() database.session.begin() usage = """%prog [options] --add [--cdrom] cdrom_id description mirror_id uri_suffix %prog [options] --add --mirror mirror_id uri_prefix %prog [options] --update [short_name1 [short_name2 ...]] %prog [options] --reap""" parser = optparse.OptionParser(usage=usage) parser.set_defaults(verbosity=0, item='cdrom') parser.add_option('-a', '--add', action='store_const', dest='action', const='add', help='Add a new item to the database') parser.add_option('-u', '--update', action='store_const', dest='action', const='update', help='Update all cdrom images in the database with the latest version') parser.add_option('-r', '--reap', action='store_const', dest='action', const='reap', help='Reap stale cdrom images that are no longer in use') a_group = optparse.OptionGroup(parser, 'Adding new items') a_group.add_option('-c', '--cdrom', action='store_const', dest='item', const='cdrom', help='Add a new cdrom to the database') a_group.add_option('-m', '--mirror', action='store_const', dest='item', const='mirror', help='Add a new mirror to the database') parser.add_option_group(a_group) v_group = optparse.OptionGroup(parser, "Verbosity levels") v_group.add_option("-q", "--quiet", action='store_const', dest='verbosity', const=0, help='Show no output from commands this script runs (default)') v_group.add_option("-v", "--verbose", action='store_const', dest='verbosity', const=1, help='Show only errors from commands this script runs') v_group.add_option("--noisy", action='store_const', dest='verbosity', const=2, help='Show all output from commands this script runs') parser.add_option_group(v_group) (options, args) = parser.parse_args() if options.action is None: print(parser.format_help()) elif options.action == 'add': if options.item == 'cdrom': attrs = dict(list(zip(('cdrom_id', 'description', 'mirror_id', 'uri_suffix'), args))) cdrom = database.CDROM(**attrs) database.session.add(cdrom) database.session.commit() load_image(cdrom) elif options.item == 'mirror': attrs = dict(list(zip(('mirror_id', 'uri_prefix'), args))) mirror = database.Mirror(**attrs) database.session.add(mirror) database.session.commit() elif options.action == 'update': if len(args) > 0: images = [database.CDROM.query().get(arg) for arg in args] else: images = database.CDROM.query().all() for cdrom in images: if cdrom is not None: load_image(cdrom) elif options.action == 'reap': reap_images() if __name__ == '__main__': main()