Skip to content

Commit

Permalink
Include image information in Checkbox submission (New) (#1460)
Browse files Browse the repository at this point in the history
This creates a new system_information collector that collects the URL if possible (namely, if ubuntu-report is installed on the machine and if the DCD string is there and valid). This safely fails in unexpected situations, reporting the error. This also stores (in the stderr of the collector) the full report non-destructively, so in any situation we can go back and re-interpret the data.

This is inspired by PC Sanity provider's `get-image-url.sh` script.
  • Loading branch information
Hook25 authored Dec 2, 2024
1 parent e17d292 commit 4f7e7d5
Show file tree
Hide file tree
Showing 4 changed files with 288 additions and 0 deletions.
13 changes: 13 additions & 0 deletions checkbox-ng/plainbox/impl/session/system_information.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,19 @@ def __init__(self):
)


class ImageInfoCollector(Collector):
COLLECTOR_NAME = "image_info"

def __init__(self):
super().__init__(
collection_cmd=[
"python3",
str(vendor.IMAGE_INFO),
],
version_cmd=["python3", str(vendor.IMAGE_INFO), "--version"],
)


if __name__ == "__main__":
collection = collect()
print(collection.to_json())
1 change: 1 addition & 0 deletions checkbox-ng/plainbox/vendor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@
from pathlib import Path

INXI_PATH = (Path(__file__).parent / "inxi").resolve()
IMAGE_INFO = (Path(__file__).parent / "image_info.py").resolve()
146 changes: 146 additions & 0 deletions checkbox-ng/plainbox/vendor/image_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
import sys
import json
import argparse
import subprocess

BASE_URL = "https://oem-share.canonical.com/partners"
VERSION = 1


def parse_args(argv):
parser = argparse.ArgumentParser()
parser.add_argument(
"--version",
"-v",
help="Prints the version of the tool and exits",
action="store_true",
)

return parser.parse_args(argv)


def parse_ubuntu_report():
try:
return json.loads(
subprocess.check_output(
["ubuntu-report", "show"],
universal_newlines=True,
),
)
except FileNotFoundError:
raise SystemExit(
"ubuntu-report is not installed, "
"install it to collect this information"
)


def dcd_string_to_info(dcd_string):
"""
Creates a dict with all available information that can be extracted from
the dcd string, at the very least:
- project
- series
- kernel type
- build date
- build number
- url
"""
# prefix, should always be present
dcd_string = dcd_string.replace("canonical-", "")
dcd_string = dcd_string.replace("oem-", "")
dcd_string_arr = dcd_string.split("-")
if len(dcd_string_arr) == 5:
project, series, kernel_type, build_date, build_number = dcd_string_arr
info = {
"base_url": BASE_URL,
"project": project,
"series": series,
"kernel_type": kernel_type,
"build_date": build_date,
"build_number": build_number,
}
info["url"] = (
"{base_url}/{project}/share/releases/"
"{series}/{kernel_type}/{build_date}-{build_number}/"
).format(**info)
elif len(dcd_string_arr) == 6:
(
project,
series,
kernel_type,
kernel_version,
build_date,
build_number,
) = dcd_string_arr
info = {
"base_url": BASE_URL,
"project": project,
"series": series,
"kernel_type": kernel_type,
"kernel_version": kernel_version,
"build_date": build_date,
"build_number": build_number,
}
info["url"] = (
"{base_url}/{project}/share/releases/"
"{series}/{kernel_type}-{kernel_version}/"
"{build_date}-{build_number}/"
).format(**info)
elif len(dcd_string_arr) == 7:
(
project,
series,
kernel_type,
kernel_version,
kernel_suffix,
build_date,
build_number,
) = dcd_string_arr
info = {
"base_url": BASE_URL,
"project": project,
"series": series,
"kernel_type": kernel_type,
"kernel_version": kernel_version,
"kernel_suffix": kernel_suffix,
"build_date": build_date,
"build_number": build_number,
}
info["url"] = (
"{base_url}/{project}/share/releases/"
"{series}/{kernel_type}-{kernel_version}-{kernel_suffix}/"
"{build_date}-{build_number}/"
).format(**info)
else:
raise SystemExit("Unknown dcd string format: {}".format(dcd_string))
return info


def dcd_info():
ubuntu_report = parse_ubuntu_report()
print(
"Parsed report: {}".format(json.dumps(ubuntu_report)), file=sys.stderr
)
try:
dcd_string = ubuntu_report["OEM"]["DCD"]
except KeyError:
raise SystemExit(
"Unable to find the OEM DCD string in the parsed report"
)
return dcd_string_to_info(dcd_string)


def main(argv=None):
if argv is None:
argv = sys.argv[1:]
args = parse_args(argv)
if args.version:
print(VERSION)
return
info = dcd_info()
json.dump(info, sys.stdout)


if __name__ == "__main__":
main(sys.argv[1:])
128 changes: 128 additions & 0 deletions checkbox-ng/plainbox/vendor/test_image_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from unittest import TestCase
from unittest.mock import patch
from plainbox.vendor import image_info


class TestDCDStringToInfo(TestCase):
def test_len_5(self):
example = "canonical-oem-pinktiger-noble-hwe-20240709-33.iso"
info = image_info.dcd_string_to_info(example)

self.assertEqual(info["project"], "pinktiger")
self.assertEqual(info["series"], "noble")

def test_len_6(self):
example = "canonical-oem-pinktiger-noble-oem-24.04a-20240709-33.iso"
info = image_info.dcd_string_to_info(example)

self.assertEqual(info["project"], "pinktiger")
self.assertEqual(info["series"], "noble")

def test_len_7(self):
example = (
"canonical-oem-pinktiger-noble-oem-24.04a-proposed-20240709-33.iso"
)
info = image_info.dcd_string_to_info(example)

self.assertEqual(info["project"], "pinktiger")
self.assertEqual(info["series"], "noble")

def test_len_wrong(self):
with self.assertRaises(SystemExit):
image_info.dcd_string_to_info("canonical-oem-test-stub-dcd")

with self.assertRaises(SystemExit):
image_info.dcd_string_to_info("")


class TestDCDStringE2E(TestCase):
@patch("subprocess.check_output")
def test_ok_output(self, mock_check_output):
mock_check_output.return_value = """{
"Version": "24.04",
"OEM": {
"Vendor": "Some Inc.",
"Product": "13312",
"Family": "NOFAMILY",
"DCD": "canonical-oem-pinktiger-noble-oem-24.04a-20240823-74"
},
"BIOS": {
"Vendor": "Some Inc.",
"Version": "0.1.20"
},
"Arch": "amd64",
"HwCap": "x86-64-v3",
"GPU": [
{
"Vendor": "0000",
"Model": "0000"
}
],
"RAM": 16,
"Partitions": [
477.7,
1.1
],
"Autologin": true,
"LivePatch": false,
"Session": {
"DE": "",
"Name": "",
"Type": "tty"
},
"Language": "en_US",
"Timezone": "Etc/UTC",
"Install": {
"Type": "Flutter",
"OEM": false,
"Media": "Ubuntu OEM 24.04.1 LTS",
"Stages": {
"20": "loading",
"218": "done"
}
}
}"""
image_info.main([])

@patch("subprocess.check_output")
def test_fail_no_dcd_output(self, mock_check_output):
mock_check_output.return_value = """{
"Version": "24.04",
"BIOS": {
"Vendor": "Some Inc.",
"Version": "0.1.20"
},
"Arch": "amd64",
"HwCap": "x86-64-v3",
"GPU": [
{
"Vendor": "0000",
"Model": "0000"
}
],
"RAM": 16,
"Partitions": [
477.7,
1.1
],
"Autologin": true,
"LivePatch": false,
"Session": {
"DE": "",
"Name": "",
"Type": "tty"
},
"Language": "en_US",
"Timezone": "Etc/UTC",
"Install": {
"Type": "Flutter",
"OEM": false,
"Media": "Ubuntu OEM 24.04.1 LTS",
"Stages": {
"20": "loading",
"218": "done"
}
}
}"""
with self.assertRaises(SystemExit):
image_info.main([])

0 comments on commit 4f7e7d5

Please sign in to comment.