Skip to content

Commit 3c9ac93

Browse files
authored
feat: include update_hardware_registry command on crontab (#1916)
* Added an option to read an index of registries on update_hardware_registry * Added option to read urls as update_hardware_registry entries * Add to dashboard cronjob list Closes #1899 Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
1 parent 8286fc8 commit 3c9ac93

2 files changed

Lines changed: 102 additions & 17 deletions

File tree

backend/kernelCI/settings.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ def get_json_env_var(name, default):
150150
}
151151
"""Maps monitoring_id to the relative_path that will be appended to the base healthcheck URL."""
152152

153+
DEFAULT_HARDWARE_REGISTRY_INDEX_URL = (
154+
"https://raw.githubusercontent.com/kernelci/kernelci-pipeline/main/"
155+
"config/hardware_registry/index.yaml"
156+
)
157+
HARDWARE_REGISTRY_INDEX_URL = os.environ.get(
158+
"HARDWARE_REGISTRY_INDEX_URL", DEFAULT_HARDWARE_REGISTRY_INDEX_URL
159+
)
160+
153161
SKIP_CRONJOBS = is_boolean_or_string_true(os.environ.get("SKIP_CRONJOBS", False))
154162
if SKIP_CRONJOBS:
155163
CRONJOBS = []
@@ -236,6 +244,14 @@ def get_json_env_var(name, default):
236244
"--yes",
237245
],
238246
),
247+
(
248+
"0 3 * * *",
249+
"django.core.management.call_command",
250+
[
251+
"update_hardware_registry",
252+
f"--index={HARDWARE_REGISTRY_INDEX_URL}",
253+
],
254+
),
239255
]
240256

241257
# Email settings for SMTP backend

backend/kernelCI_app/management/commands/update_hardware_registry.py

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
from pathlib import Path
66
from typing import Any
7+
from urllib.parse import urljoin, urlparse
78

89
import yaml
910
from django.core.management.base import BaseCommand, CommandError
1011

12+
import requests
1113
from kernelCI_app.models import (
1214
HardwareRegistryPlatform,
1315
HardwareRegistryPlatformVendor,
@@ -34,57 +36,105 @@ def get_update_fields(model):
3436
)
3537

3638

39+
def _is_url(value: str) -> bool:
40+
parsed = urlparse(value)
41+
return parsed.scheme in ("http", "https")
42+
43+
3744
class Command(BaseCommand):
3845
help = (
3946
"Parse a KernelCI hardware registry YAML file (same shape as "
40-
"kernelci-pipeline config/hardware_registry/*.yaml) and print its data."
47+
"kernelci-pipeline config/hardware_registry/*.yaml) and print its data. "
48+
"Accepts a local file path or a URL."
4149
)
4250

4351
def add_arguments(self, parser):
4452
parser.add_argument(
4553
"registry_file",
54+
nargs="?",
55+
type=str,
56+
help="Path or URL to a single hardware registry YAML file.",
57+
)
58+
parser.add_argument(
59+
"--index",
4660
type=str,
47-
help="Path to the hardware registry YAML file.",
61+
help=(
62+
"Path or URL to an index YAML listing registry files "
63+
"(e.g. hardware_registry/index.yaml). Entries in the "
64+
"'registries' key are resolved relative to the index location."
65+
),
4866
)
4967

50-
def load_yaml(self, path: str) -> dict:
68+
def _fetch_url(self, url: str) -> str:
69+
try:
70+
response = requests.get(url, timeout=30)
71+
response.raise_for_status()
72+
return response.text
73+
except requests.RequestException as exc:
74+
raise CommandError(f"Failed to fetch {url}: {exc}") from exc
75+
76+
def _read_file(self, path: Path) -> str:
5177
if not path.is_file():
5278
raise CommandError(f"Registry file not found: {path}")
5379
try:
54-
raw = path.read_text(encoding="utf-8")
80+
return path.read_text(encoding="utf-8")
5581
except OSError as exc:
5682
raise CommandError(f"Cannot read registry file: {exc}") from exc
5783

84+
def load_yaml(self, source: str) -> dict:
85+
if _is_url(source):
86+
raw = self._fetch_url(source)
87+
else:
88+
raw = self._read_file(Path(source).expanduser().resolve())
89+
5890
try:
59-
data = yaml.safe_load(raw)
60-
return data
91+
return yaml.safe_load(raw)
6192
except yaml.YAMLError as exc:
6293
raise CommandError(f"Invalid YAML: {exc}") from exc
6394

64-
def handle(self, *args: Any, registry_file: str, **options: Any) -> None:
65-
path = Path(registry_file).expanduser().resolve()
66-
data = self.load_yaml(path)
95+
def _resolve_relative(self, base: str, relative: str) -> str:
96+
"""Resolve a relative filename against the base index location."""
97+
if _is_url(base):
98+
return urljoin(base, relative)
99+
return str(Path(base).expanduser().resolve().parent / relative)
100+
101+
def _resolve_index(self, index_source: str) -> list[str]:
102+
"""Load an index YAML and return resolved paths/URLs for each registry."""
103+
data = self.load_yaml(index_source)
104+
if not isinstance(data, dict) or "registries" not in data:
105+
raise CommandError(
106+
"Index YAML must contain a 'registries' key with a list of filenames."
107+
)
108+
entries = data["registries"]
109+
if not isinstance(entries, list) or not entries:
110+
raise CommandError("Index 'registries' must be a non-empty list.")
111+
return [self._resolve_relative(index_source, entry) for entry in entries]
112+
113+
def _process_registry(self, source: str) -> None:
114+
"""Load and persist a single registry YAML."""
115+
data = self.load_yaml(source)
67116

68117
if data is None:
69-
raise CommandError("Registry file is empty or parses to null.")
118+
raise CommandError(f"Registry is empty or parses to null: {source}")
70119
if not isinstance(data, dict):
71120
raise CommandError(
72-
f"Expected a YAML mapping at root, got {type(data).__name__}."
121+
f"Expected a YAML mapping at root, got {type(data).__name__}: {source}"
73122
)
74123

75124
missing = [k for k in EXPECTED_SECTIONS if k not in data]
76125
if missing:
77126
self.stderr.write(
78127
self.style.WARNING(
79-
f"Missing typical hardware-registry sections : {', '.join(missing)}"
128+
f"Missing typical hardware-registry sections in {source}: "
129+
f"{', '.join(missing)}"
80130
)
81131
)
82132

83-
silicon_vendors = data["silicon_vendors"] or []
84-
platform_vendors = data["platform_vendors"] or []
85-
processors = data["processors"] or []
86-
system_modules = data["system_modules"] or []
87-
platforms = data["platforms"] or []
133+
silicon_vendors = data["silicon_vendors"] or {}
134+
platform_vendors = data["platform_vendors"] or {}
135+
processors = data["processors"] or {}
136+
system_modules = data["system_modules"] or {}
137+
platforms = data["platforms"] or {}
88138

89139
HardwareRegistrySiliconVendor.objects.bulk_create(
90140
[HardwareRegistrySiliconVendor(**sv) for sv in silicon_vendors.values()],
@@ -120,3 +170,22 @@ def handle(self, *args: Any, registry_file: str, **options: Any) -> None:
120170
unique_fields=["id"],
121171
update_fields=get_update_fields(HardwareRegistryPlatform),
122172
)
173+
174+
self.stdout.write(self.style.SUCCESS(f"Processed: {source}"))
175+
176+
def handle(self, *args: Any, **options: Any) -> None:
177+
registry_file = options.get("registry_file")
178+
index = options.get("index")
179+
180+
if not registry_file and not index:
181+
raise CommandError("Provide either a registry_file or --index.")
182+
if registry_file and index:
183+
raise CommandError("Provide either a registry_file or --index, not both.")
184+
185+
if index:
186+
sources = self._resolve_index(index)
187+
else:
188+
sources = [registry_file]
189+
190+
for source in sources:
191+
self._process_registry(source)

0 commit comments

Comments
 (0)