-
Notifications
You must be signed in to change notification settings - Fork 73
chore(main): add reusable workflow to test installing slices #119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
cjdcordeiro
merged 15 commits into
canonical:main
from
rebornplusplus:ci/tests/install-slices/main
Feb 22, 2024
Merged
Changes from 11 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
ce59ab8
chore: add script to test installing slices
c26a676
chore: add reusable workflow to test installing slices
e66f727
chore: modify reusable workflow to be more generic
8a161ee
chore: add ARCH matrix in reusable job
175280d
chore: set proper bash options
95a4313
chore: move scheduled tests to main branch
2015045
chore: add release-branch option to reduce duplication
8682974
chore: add preparation job before install
71a897e
chore: merge install slices steps
84bce6b
chore: re-write install-slices from bash to python
6ffe284
chore: ensure package existence in archive
0787956
chore: improve the install_slices script
bbb4c82
chore: add future TODO comments
6acfb15
Merge branch 'main' into ci/tests/install-slices/main
127f08c
chore: change pkglist filename to deb-requirements.txt
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,324 @@ | ||
| #!/usr/bin/python3 | ||
|
|
||
| import argparse | ||
| import logging | ||
| import subprocess | ||
| import tempfile | ||
|
|
||
| import requests | ||
| import yaml | ||
|
|
||
|
|
||
| def configure_logging() -> None: | ||
| """ | ||
| Configure the logging options for this script. | ||
| """ | ||
| logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.DEBUG) | ||
|
|
||
|
|
||
| class CommandArgs: | ||
| arch: str | ||
| release: str | ||
| files: list[str] | ||
| ignore_missing: bool | ||
|
|
||
|
|
||
| def parse_args() -> CommandArgs: | ||
| """ | ||
| Get an argument parser tailored to this script. | ||
| """ | ||
| parser = argparse.ArgumentParser( | ||
| description="Verify slice definition files by installing the slices", | ||
| ) | ||
| parser.add_argument( | ||
| "--arch", | ||
| required=True, | ||
| help="Package architecture", | ||
| ) | ||
| parser.add_argument( | ||
| "--release", | ||
| required=True, | ||
| help="Chisel-releases branch name or directory", | ||
|
rebornplusplus marked this conversation as resolved.
Outdated
|
||
| ) | ||
| parser.add_argument( | ||
| "--ignore-missing", | ||
| required=False, | ||
| action="store_true", | ||
| help=str( | ||
| "Ignore arch-specific package not found in archive errors. " | ||
| "If set, also ensure that packages exist for any arch." | ||
| ), | ||
| ) | ||
| parser.add_argument( | ||
| "files", | ||
| metavar="file", | ||
| help="Chisel slice definition file(s)", | ||
| nargs="*", | ||
| ) | ||
| args = parser.parse_args() | ||
| cli_args = CommandArgs() | ||
| cli_args.arch = args.arch | ||
| cli_args.release = args.release | ||
| cli_args.files = args.files | ||
| cli_args.ignore_missing = args.ignore_missing | ||
| return cli_args | ||
|
|
||
|
|
||
| class Archive: | ||
| """ | ||
| Minimal data class replicating ubuntu archive in chisel.yaml | ||
| """ | ||
|
|
||
| version: str | ||
|
rebornplusplus marked this conversation as resolved.
Outdated
|
||
| components: list[str] | ||
| suites: list[str] | ||
|
|
||
|
|
||
| def parse_archive(release: str) -> Archive: | ||
| """ | ||
| Parse the "ubuntu" archive from the chisel.yaml file of a release. | ||
| The chisel.yaml file has the following structure: | ||
| ... | ||
| archives: | ||
| ubuntu: | ||
| version: 22.04 | ||
| components: [main, universe] | ||
| suites: [jammy, jammy-security, jammy-updates] | ||
| ... | ||
| ... | ||
| """ | ||
| logging.debug("Parsing ubuntu archive info...") | ||
| # (download and) parse chisel.yaml for ubuntu archive info | ||
| try: | ||
| if release.endswith("/"): | ||
|
rebornplusplus marked this conversation as resolved.
Outdated
|
||
| with open(f"{release}chisel.yaml", "r") as stream: | ||
| data = yaml.safe_load(stream) | ||
| else: | ||
| req_url = f"https://raw.githubusercontent.com/canonical/chisel-releases/{release}/chisel.yaml" | ||
| response = requests.get(req_url) | ||
| if response.status_code < 200 or response.status_code > 299: | ||
| logging.error(f"Cannot download chisel.yaml from remote") | ||
| exit(1) | ||
|
rebornplusplus marked this conversation as resolved.
Outdated
|
||
| data = yaml.safe_load(response.content) | ||
| except yaml.YAMLError as e: | ||
| logging.error(f"chisel.yaml: {e}") | ||
| exit(1) | ||
| except Exception as e: | ||
| logging.error(e) | ||
| exit(1) | ||
| # load the yaml data into Archive | ||
| archive = Archive() | ||
| try: | ||
| archive_data = data["archives"]["ubuntu"] | ||
| archive.version = archive_data["version"] | ||
| archive.components = archive_data["components"] | ||
| archive.suites = archive_data["suites"] | ||
|
rebornplusplus marked this conversation as resolved.
Outdated
|
||
| except KeyError as e: | ||
| logging.error(f"{release}: key {e} not found") | ||
|
rebornplusplus marked this conversation as resolved.
Outdated
|
||
| exit(1) | ||
| return archive | ||
|
|
||
|
|
||
| class Package: | ||
| """ | ||
| Minimal data class to store package info. | ||
| """ | ||
|
|
||
| path: str | ||
| package: str | ||
| slices: list[str] | ||
|
|
||
| def full_slice_name(self, slice: str) -> str: | ||
| return f"{self.package}_{slice}" | ||
|
|
||
|
|
||
| def parse_package(filepath: str) -> Package: | ||
| """ | ||
| Parse a slice definition file and return the Package. | ||
| """ | ||
| logging.debug(f"Parsing {filepath}...") | ||
| with open(filepath, "r") as stream: | ||
| try: | ||
| data = yaml.safe_load(stream) | ||
| except yaml.YAMLError as e: | ||
| logging.error(f"{filepath}: {e}") | ||
| exit(1) | ||
| p = Package() | ||
| p.path = filepath | ||
| try: | ||
| p.package = data["package"] | ||
| p.slices = [] | ||
| for key in data["slices"]: | ||
| p.slices.append(key) | ||
|
rebornplusplus marked this conversation as resolved.
Outdated
|
||
| p.slices = sorted(p.slices) | ||
| except KeyError as e: | ||
| logging.error(f"{filepath}: key {e} not found") | ||
| exit(1) | ||
| except Exception as e: | ||
| logging.error(f"{filepath}: {e}") | ||
| exit(1) | ||
| return p | ||
|
|
||
|
|
||
| def query_package_existence( | ||
| packages: list[str], | ||
| arch: list[str] = [], | ||
| components: list[str] = [], | ||
| suites: list[str] = [], | ||
| ) -> tuple[list[str], list[str]]: | ||
| """ | ||
| Check which packages exist in the archive. Return a list of packages | ||
| that exist and another list for which do not. | ||
| """ | ||
| # prepare cmd | ||
| args = ["rmadison"] | ||
| if len(arch) > 0: | ||
| args += ["--architecture", ",".join(arch)] | ||
| if len(components) > 0: | ||
| args += ["--component", ",".join(components)] | ||
| if len(suites) > 0: | ||
| args += ["--suite", ",".join(suites)] | ||
| args.append(" ".join(packages)) | ||
| # query the archives using rmadison | ||
| logging.debug("Querying the archives for packages...") | ||
| logging.debug("Executing " + " ".join(args)) | ||
| res = subprocess.run(args, capture_output=True, text=True) | ||
| if res.returncode != 0: | ||
| logging.error(f"Failed to query the archives {res.returncode}") | ||
| exit(res.returncode) | ||
| output = res.stdout.rstrip() | ||
| logging.debug(f"Archive query output:\n{output}") | ||
| # parse the output for available packages | ||
| available = {} | ||
| for line in output.split("\n"): | ||
| line = line.strip() | ||
| if line == "": | ||
| continue | ||
| pkg = line.split("|")[0].strip() | ||
| available[pkg] = True | ||
| found = [] | ||
| missing = [] | ||
| for p in packages: | ||
| if available.get(p): | ||
| found.append(p) | ||
| else: | ||
| missing.append(p) | ||
|
rebornplusplus marked this conversation as resolved.
Outdated
|
||
| return found, missing | ||
|
|
||
|
|
||
| def ensure_package_existence(packages: list[str], archive: Archive) -> None: | ||
| """ | ||
| Ensure that packages exist in the archive for any arch. | ||
| """ | ||
| _, missing = query_package_existence( | ||
| packages, | ||
| components=archive.components, | ||
| suites=archive.suites, | ||
| ) | ||
| if len(missing) > 0: | ||
|
rebornplusplus marked this conversation as resolved.
Outdated
|
||
| logging.error( | ||
| "The following packages do not exist:\n" | ||
| + "\n".join(f" - {p}" for p in missing) | ||
|
rebornplusplus marked this conversation as resolved.
Outdated
|
||
| ) | ||
| exit(1) | ||
|
|
||
|
|
||
| def filter_packages( | ||
| packages: list[Package], | ||
| arch: str, | ||
| release: str, | ||
| ignore_missing: bool = False, | ||
| ) -> tuple[list[Package], list[Package]]: | ||
| """ | ||
| Filter the Packages using the following criteria: | ||
| - if "ignore_missing" is true, ignore package if it does not exist | ||
| in the archive for [arch, release]. | ||
| """ | ||
| ignored = [] | ||
| if ignore_missing: | ||
|
rebornplusplus marked this conversation as resolved.
Outdated
|
||
| package_names = [p.package for p in packages] | ||
| archive = parse_archive(release) | ||
| # ensure that packages exist in the archive. | ||
| ensure_package_existence(package_names, archive) | ||
|
rebornplusplus marked this conversation as resolved.
Outdated
|
||
| # check package existence for particular arch | ||
| found, _ = query_package_existence( | ||
| packages=package_names, | ||
| arch=[arch], | ||
| components=archive.components, | ||
| suites=archive.suites, | ||
| ) | ||
| available = {} | ||
| for p in found: | ||
| available[p] = True | ||
| filtered = [] | ||
| for p in packages: | ||
| if available.get(p.package): | ||
| filtered.append(p) | ||
| else: | ||
| ignored.append(p) | ||
| packages = filtered | ||
|
rebornplusplus marked this conversation as resolved.
Outdated
|
||
| return packages, ignored | ||
|
|
||
|
|
||
| def install_slice(slice: str, arch: str, release: str) -> None: | ||
| """ | ||
| Install the slice by running "chisel cut". | ||
| """ | ||
| logging.info(f"Installing {slice} on {arch}...") | ||
| with tempfile.TemporaryDirectory() as tmpfs: | ||
| res = subprocess.run( | ||
| args=[ | ||
| "chisel", | ||
| "cut", | ||
| "--arch", | ||
| arch, | ||
| "--release", | ||
| release, | ||
| "--root", | ||
| tmpfs, | ||
| slice, | ||
| ], | ||
| capture_output=True, | ||
| text=True, | ||
| ) | ||
| if res.returncode != 0: | ||
| print("==============================================") | ||
| print(res.stderr, end="") | ||
| exit(res.returncode) | ||
|
|
||
|
|
||
| def main(): | ||
| configure_logging() | ||
| # parse CLI args | ||
| cli_args = parse_args() | ||
| # parse slice definition files | ||
| packages = [] | ||
| for file in cli_args.files: | ||
| pkg = parse_package(file) | ||
| packages.append(pkg) | ||
| # choose the packages to install slices from | ||
| packages, ignored = filter_packages( | ||
| packages, | ||
| cli_args.arch, | ||
| cli_args.release, | ||
| cli_args.ignore_missing, | ||
| ) | ||
| if len(ignored) > 0: | ||
| logging.info("The following packages will be IGNORED:") | ||
| for pkg in ignored: | ||
| logging.info(f" - {pkg.package}") | ||
| if len(packages) > 0: | ||
| logging.info("Slices of the following packages will be INSTALLED:") | ||
| for pkg in packages: | ||
| logging.info(f" - {pkg.package}") | ||
| else: | ||
| logging.info("No slices will be installed.") | ||
| exit(0) | ||
|
rebornplusplus marked this conversation as resolved.
Outdated
|
||
| # install the slices | ||
| for pkg in packages: | ||
| for slice in pkg.slices: | ||
| install_slice(pkg.full_slice_name(slice), cli_args.arch, cli_args.release) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| pyyaml | ||
| requests |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.