From db6f685b8494a5e6137144d6b741bfd4afc07559 Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle <michael.hudson@canonical.com> Date: Mon, 28 Aug 2023 11:30:11 +1200 Subject: [PATCH] snap-seed-parse.py: Update to allow parsing uc20-style seeds. (LP: #2028984) --- debian/changelog | 2 + live-build/snap-seed-parse.py | 141 +++++++++++++++++++++++++++------- 2 files changed, 117 insertions(+), 26 deletions(-) diff --git a/debian/changelog b/debian/changelog index 017b3f93..d6370350 100644 --- a/debian/changelog +++ b/debian/changelog @@ -3,6 +3,8 @@ livecd-rootfs (23.10.24) UNRELEASED; urgency=medium * update-source-catalog: Fix case where a variaton does not point at the base layer (i.e. most builds) (LP: #2033168) * Configure universe sources in canary ISO. (LP: #2033109) + * snap-seed-parse.py: Update to allow parsing uc20-style seeds. + (LP: #2028984) -- Michael Hudson-Doyle <michael.hudson@ubuntu.com> Mon, 28 Aug 2023 10:39:52 +1200 diff --git a/live-build/snap-seed-parse.py b/live-build/snap-seed-parse.py index 0e47231c..058a3d2c 100755 --- a/live-build/snap-seed-parse.py +++ b/live-build/snap-seed-parse.py @@ -10,6 +10,7 @@ The $chroot_dir argument is optional and will default to the empty string. """ import argparse +import glob import os.path import re import yaml @@ -32,37 +33,125 @@ CHROOT_ROOT = ARGS.chroot FNAME = ARGS.file # Trim any trailing slashes for correct appending +CHROOT_ROOT = CHROOT_ROOT.rstrip('/') log("CHROOT_ROOT: {}".format(CHROOT_ROOT)) -if len(CHROOT_ROOT) > 0 and CHROOT_ROOT[-1] == '/': - CHROOT_ROOT = CHROOT_ROOT[:-1] - -# This is where we expect to find the seed.yaml file -YAML_PATH = CHROOT_ROOT + '/var/lib/snapd/seed/seed.yaml' # Snaps are prepended with this string in the manifest LINE_PREFIX = 'snap:' +# This is where we expect to find the seed.yaml file +YAML_PATH = CHROOT_ROOT + '/var/lib/snapd/seed/seed.yaml' + log("yaml path: {}".format(YAML_PATH)) -if not os.path.isfile(YAML_PATH): - log("WARNING: yaml path not found; no seeded snaps found.") - exit(0) + + +def make_manifest_from_seed_yaml(path): + with open(YAML_PATH, 'r') as fh: + yaml_lines = yaml.safe_load(fh)['snaps'] + + log('Writing manifest to {}'.format(FNAME)) + + with open(FNAME, 'a+') as fh: + for item in yaml_lines: + filestring = item['file'] + # Pull the revision number off the file name + revision = filestring[filestring.rindex('_')+1:] + revision = re.sub(r'[^0-9]', '', revision) + fh.write("{}{}\t{}\t{}\n".format(LINE_PREFIX, + item['name'], + item['channel'], + revision, + )) + + +def look_for_uc20_model(chroot): + modeenv = f"{chroot}/var/lib/snapd/modeenv" + system_name = None + if os.path.isfile(modeenv): + log(f"found modeenv file at {modeenv}") + with open(modeenv) as fh: + for line in fh: + if line.startswith("recovery_system="): + system_name = line.split('=', 1)[1].strip() + log(f"read system name {system_name!r} from modeenv") + break + if system_name is None: + system_names = os.listdir(f"{chroot}/var/lib/snapd/seed/systems") + if len(system_names) == 0: + log("no systems found") + return None + elif len(system_names) > 1: + log("multiple systems found, refusing to guess which to parse") + return None + else: + system_name = system_names[0] + log(f"parsing only system found {system_name}") + system_dir = f"{chroot}/var/lib/snapd/seed/systems/{system_name}" + if not os.path.isdir(system_dir): + log(f"could not find system called {system_name}") + return None + return system_dir + + +def parse_assertion_file(asserts, filename): + # Parse the snapd assertions file 'filename' and store the + # assertions found in 'asserts'. + with open(filename) as fp: + text = fp.read() + + k = '' + + for block in text.split('\n\n'): + if block.startswith('type:'): + this_assert = {} + for line in block.split('\n'): + if line.startswith(' '): + this_assert[k.strip()] += '\n' + line + continue + k, v = line.split(':', 1) + this_assert[k.strip()] = v.strip() + asserts.setdefault(this_assert['type'], []).append(this_assert) + + +def make_manifest_from_system(system_dir): + files = [f"{system_dir}/model"] + glob.glob(f"{system_dir}/assertions/*") + + asserts = {} + for filename in files: + parse_assertion_file(asserts, filename) + + [model] = asserts['model'] + snaps = yaml.safe_load(model['snaps']) + + snap_names = [] + for snap in snaps: + snap_names.append(snap['name']) + snap_names.sort() + + snap_name_to_id = {} + snap_id_to_rev = {} + for decl in asserts['snap-declaration']: + snap_name_to_id[decl['snap-name']] = decl['snap-id'] + for rev in asserts['snap-revision']: + snap_id_to_rev[rev['snap-id']] = rev['snap-revision'] + + log('Writing manifest to {}'.format(FNAME)) + + with open(FNAME, 'a+') as fh: + for snap_name in snap_names: + channel = snap['default-channel'] + rev = snap_id_to_rev[snap_name_to_id[snap_name]] + fh.write(f"{LINE_PREFIX}{snap_name}\t{channel}\t{rev}\n") + + +if os.path.isfile(YAML_PATH): + log(f"seed.yaml found at {YAML_PATH}") + make_manifest_from_seed_yaml(YAML_PATH) else: - log("yaml path found.") - -with open(YAML_PATH, 'r') as fh: - yaml_lines = yaml.safe_load(fh)['snaps'] - -log('Writing manifest to {}'.format(FNAME)) - -with open(FNAME, 'a+') as fh: - for item in yaml_lines: - filestring = item['file'] - # Pull the revision number off the file name - revision = filestring[filestring.rindex('_')+1:] - revision = re.sub(r'[^0-9]', '', revision) - fh.write("{}{}\t{}\t{}\n".format(LINE_PREFIX, - item['name'], - item['channel'], - revision, - )) + system_dir = look_for_uc20_model(CHROOT_ROOT) + if system_dir is None: + log("WARNING: could not find seed.yaml or uc20-style seed") + exit(0) + make_manifest_from_system(system_dir) + log('Manifest output finished.')