Skip to content

Commit dc4bdb7

Browse files
committed
runtime: Add Pull-Only labs runtime implementation
New PULL_LABS runtime for KernelCI that implements the Job Definition creation according to the PULL_LABS protocol specification. New files: kernelci/runtime/pull_labs.py - Main runtime implementation config/runtime/base/pull_labs.jinja2 - JSON job definition template kernelci/config/runtime.py - Added RuntimePullLabs configuration class Properties: poll_interval, timeout, storage_name Registered in RuntimeFactory._lab_types as 'pull_labs' Signed-off-by: Denys Fedoryshchenko <[email protected]>
1 parent f463d76 commit dc4bdb7

File tree

4 files changed

+647
-52
lines changed

4 files changed

+647
-52
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
{# SPDX-License-Identifier: LGPL-2.1-or-later -#}
2+
{# PULL_LABS job definition template - generates JSON format #}
3+
{# This template creates a job definition according to PULL_LABS protocol Section 3 #}
4+
{
5+
"artifacts": {
6+
{%- set artifacts_entries = [] %}
7+
{%- if node.artifacts.kernel %}
8+
{%- do artifacts_entries.append(' "kernel": "' ~ node.artifacts.kernel ~ '"') %}
9+
{%- endif %}
10+
{%- if node.artifacts.modules %}
11+
{%- do artifacts_entries.append(' "modules": "' ~ node.artifacts.modules ~ '"') %}
12+
{%- endif %}
13+
{%- if node.artifacts.kselftest %}
14+
{%- do artifacts_entries.append(' "kselftest": "' ~ node.artifacts.kselftest ~ '"') %}
15+
{%- endif %}
16+
{%- if ramdisk %}
17+
{%- do artifacts_entries.append(' "ramdisk": "' ~ ramdisk ~ '"') %}
18+
{%- endif %}
19+
{%- if nfsroot is defined and nfsroot %}
20+
{%- do artifacts_entries.append(' "nfsroot": "' ~ nfsroot ~ '"') %}
21+
{%- endif %}
22+
{%- if node.artifacts.initrd %}
23+
{%- do artifacts_entries.append(' "initrd": "' ~ node.artifacts.initrd ~ '"') %}
24+
{%- endif %}
25+
{%- if node.artifacts.dtb %}
26+
{%- do artifacts_entries.append(' "dtb": "' ~ node.artifacts.dtb ~ '"') %}
27+
{%- endif %}
28+
{%- if artifacts_entries %}
29+
{{ artifacts_entries | join(',\n') }}
30+
{%- endif %}
31+
},
32+
"tests": [
33+
{%- if test_definitions %}
34+
{%- for test in test_definitions %}
35+
{
36+
"id": "{{ test.id }}",
37+
"type": "{{ test.type }}",
38+
"depends": {{ test.depends | tojson }},
39+
"timeout_s": {{ test.timeout_s | default(600) }}
40+
{%- if test.subset %},
41+
"subset": {{ test.subset | tojson }}
42+
{%- endif %}
43+
{%- if test.parameters %},
44+
"parameters": "{{ test.parameters }}"
45+
{%- endif %}
46+
{%- if test.pre_commands %},
47+
"pre-commands": {{ test.pre_commands | tojson }}
48+
{%- else %},
49+
"pre-commands": []
50+
{%- endif %}
51+
{%- if test.post_commands %},
52+
"post-commands": {{ test.post_commands | tojson }}
53+
{%- else %},
54+
"post-commands": []
55+
{%- endif %}
56+
}{{ "," if not loop.last else "" }}
57+
{%- endfor %}
58+
{%- else %}
59+
{
60+
"id": "boot-test-{{ node.id[:8] }}",
61+
"type": "boot",
62+
"depends": [],
63+
"timeout_s": 600,
64+
"pre-commands": [],
65+
"post-commands": []
66+
}
67+
{%- endif %}
68+
],
69+
"environment": {
70+
"platform": "{{ platform_config.name }}",
71+
"arch": "{{ platform_config.arch }}",
72+
"console": {
73+
"method": "{{ platform_config.console_method | default('serial') }}",
74+
"baud": {{ platform_config.console_baud | default(115200) }}
75+
}
76+
{%- if platform_config.params and platform_config.params.requirements %}
77+
,"requirements": {{ platform_config.params.requirements | tojson }}
78+
{%- endif %}
79+
},
80+
"callback": {
81+
"url": "{{ instance_callback }}/node/{{ node.id }}",
82+
"token_name": "{{ callback_token_name | default('kernelci-pipeline-callback') }}"
83+
}
84+
{%- if node.artifacts.metadata %}
85+
,"integrity": {
86+
"sha256": {
87+
{%- if checksums %}
88+
{%- for artifact_name, checksum in checksums.items() %}
89+
"{{ artifact_name }}": "{{ checksum }}"{{ "," if not loop.last else "" }}
90+
{%- endfor %}
91+
{%- else %}
92+
"_comment": "Checksums would be populated from metadata.json if available"
93+
{%- endif %}
94+
}
95+
}
96+
{%- endif %}
97+
}

kernelci/config/runtime.py

Lines changed: 89 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
#
33
# Copyright (C) 2019, 2021-2023 Collabora Limited
44
# Author: Guillaume Tucker <[email protected]>
5+
#
6+
# Copyright (C) 2025 Collabora Limited
7+
# Author: Denys Fedoryshchenko <[email protected]>
58

69
"""KernelCI Runtime environment configuration"""
710

@@ -11,7 +14,7 @@
1114
class Runtime(YAMLConfigObject):
1215
"""Runtime environment configuration"""
1316

14-
yaml_tag = '!Runtime'
17+
yaml_tag = "!Runtime"
1518

1619
def __init__(self, name, lab_type, filters=None, rules=None):
1720
"""A runtime environment configuration object
@@ -44,25 +47,24 @@ def rules(self):
4447
@classmethod
4548
def _get_yaml_attributes(cls):
4649
attrs = super()._get_yaml_attributes()
47-
attrs.update({'lab_type', 'rules'})
50+
attrs.update({"lab_type", "rules"})
4851
return attrs
4952

5053
@classmethod
5154
def _to_yaml_dict(cls, data):
52-
yaml_dict = {
53-
key: getattr(data, key)
54-
for key in cls._get_yaml_attributes()
55-
}
56-
yaml_dict.update({
57-
# pylint: disable=protected-access
58-
'filters': [{fil.name: fil} for fil in data._filters],
59-
})
55+
yaml_dict = {key: getattr(data, key) for key in cls._get_yaml_attributes()}
56+
yaml_dict.update(
57+
{
58+
# pylint: disable=protected-access
59+
"filters": [{fil.name: fil} for fil in data._filters],
60+
}
61+
)
6062
return yaml_dict
6163

6264
@classmethod
6365
def to_yaml(cls, dumper, data):
6466
return dumper.represent_mapping(
65-
'tag:yaml.org,2002:map', cls._to_yaml_dict(data)
67+
"tag:yaml.org,2002:map", cls._to_yaml_dict(data)
6668
)
6769

6870
def match(self, data):
@@ -73,19 +75,26 @@ def match(self, data):
7375
class RuntimeLAVA(Runtime):
7476
"""Configuration for LAVA runtime environments"""
7577

76-
yaml_tag = '!RuntimeLAVA'
78+
yaml_tag = "!RuntimeLAVA"
7779

7880
PRIORITIES = {
79-
'low': 0,
80-
'medium': 50,
81-
'high': 100,
81+
"low": 0,
82+
"medium": 50,
83+
"high": 100,
8284
}
8385

8486
# This should be solved by dropping the "priority" attribute
8587
# pylint: disable=too-many-arguments
86-
def __init__(self, url=None, priority=None, priority_min=None,
87-
priority_max=None, queue_timeout=None, notify=None,
88-
**kwargs):
88+
def __init__(
89+
self,
90+
url=None,
91+
priority=None,
92+
priority_min=None,
93+
priority_max=None,
94+
queue_timeout=None,
95+
notify=None,
96+
**kwargs,
97+
):
8998
super().__init__(**kwargs)
9099

91100
def _set_priority_value(value, default):
@@ -135,24 +144,25 @@ def notify(self):
135144
@classmethod
136145
def _get_yaml_attributes(cls):
137146
attrs = super()._get_yaml_attributes()
138-
attrs.update({
139-
'priority',
140-
'priority_min',
141-
'priority_max',
142-
'queue_timeout',
143-
'url',
144-
'notify',
145-
})
147+
attrs.update(
148+
{
149+
"priority",
150+
"priority_min",
151+
"priority_max",
152+
"queue_timeout",
153+
"url",
154+
"notify",
155+
}
156+
)
146157
return attrs
147158

148159

149160
class RuntimeDocker(Runtime):
150161
"""Configuration for Docker runtime environments"""
151162

152-
yaml_tag = '!RuntimeDocker'
163+
yaml_tag = "!RuntimeDocker"
153164

154-
def __init__(self, env_file=None, volumes=None, user=None, timeout=None,
155-
**kwargs):
165+
def __init__(self, env_file=None, volumes=None, user=None, timeout=None, **kwargs):
156166
super().__init__(**kwargs)
157167
self._env_file = env_file
158168
self._volumes = volumes or []
@@ -182,14 +192,14 @@ def timeout(self):
182192
@classmethod
183193
def _get_yaml_attributes(cls):
184194
attrs = super()._get_yaml_attributes()
185-
attrs.update({'env_file', 'volumes', 'user', 'timeout'})
195+
attrs.update({"env_file", "volumes", "user", "timeout"})
186196
return attrs
187197

188198

189199
class RuntimeKubernetes(Runtime):
190200
"""Configuration for Kubernetes runtime environments"""
191201

192-
yaml_tag = '!RuntimeKubernetes'
202+
yaml_tag = "!RuntimeKubernetes"
193203

194204
def __init__(self, context=None, **kwargs):
195205
super().__init__(**kwargs)
@@ -203,43 +213,77 @@ def context(self):
203213
@classmethod
204214
def _get_yaml_attributes(cls):
205215
attrs = super()._get_yaml_attributes()
206-
attrs.update({'context'})
216+
attrs.update({"context"})
217+
return attrs
218+
219+
220+
class RuntimePullLabs(Runtime):
221+
"""Configuration for PULL_LABS runtime environments"""
222+
223+
yaml_tag = "!RuntimePullLabs"
224+
225+
def __init__(self, poll_interval=None, timeout=None, storage_name=None, **kwargs):
226+
super().__init__(**kwargs)
227+
self._poll_interval = poll_interval or 30
228+
self._timeout = timeout or 3600
229+
self._storage_name = storage_name
230+
231+
@property
232+
def poll_interval(self):
233+
"""Polling interval for events in seconds"""
234+
return self._poll_interval
235+
236+
@property
237+
def timeout(self):
238+
"""Job timeout in seconds"""
239+
return self._timeout
240+
241+
@property
242+
def storage_name(self):
243+
"""Storage configuration name for job definitions"""
244+
return self._storage_name
245+
246+
@classmethod
247+
def _get_yaml_attributes(cls):
248+
attrs = super()._get_yaml_attributes()
249+
attrs.update({"poll_interval", "timeout", "storage_name"})
207250
return attrs
208251

209252

210253
class RuntimeFactory: # pylint: disable=too-few-public-methods
211254
"""Factory to create lab objects from YAML data."""
212255

213256
_lab_types = {
214-
'docker': RuntimeDocker,
215-
'kubernetes': RuntimeKubernetes,
216-
'lava': RuntimeLAVA,
217-
'legacy.lava_xmlrpc': RuntimeLAVA,
218-
'legacy.lava_rest': RuntimeLAVA,
219-
'shell': Runtime,
257+
"docker": RuntimeDocker,
258+
"kubernetes": RuntimeKubernetes,
259+
"lava": RuntimeLAVA,
260+
"legacy.lava_xmlrpc": RuntimeLAVA,
261+
"legacy.lava_rest": RuntimeLAVA,
262+
"pull_labs": RuntimePullLabs,
263+
"shell": Runtime,
220264
}
221265

222266
@classmethod
223267
def from_yaml(cls, name, config, default_filters):
224268
"""Load the configuration from YAML data"""
225-
lab_type = config.get('lab_type')
269+
lab_type = config.get("lab_type")
226270
kwargs = {
227-
'name': name,
228-
'lab_type': lab_type,
229-
'filters': FilterFactory.from_data(config, default_filters),
271+
"name": name,
272+
"lab_type": lab_type,
273+
"filters": FilterFactory.from_data(config, default_filters),
230274
}
231275
lab_cls = cls._lab_types[lab_type] if lab_type else Runtime
232276
return lab_cls.load_from_yaml(config, **kwargs)
233277

234278

235279
def from_yaml(data, filters):
236280
"""Load the runtime environment from YAML based on its type"""
237-
runtimes_filters = filters.get('runtimes')
281+
runtimes_filters = filters.get("runtimes")
238282
runtimes = {
239283
name: RuntimeFactory.from_yaml(name, runtime, runtimes_filters)
240-
for name, runtime in data.get('runtimes', {}).items()
284+
for name, runtime in data.get("runtimes", {}).items()
241285
}
242286

243287
return {
244-
'runtimes': runtimes,
288+
"runtimes": runtimes,
245289
}

kernelci/runtime/__init__.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from jinja2 import Environment, FileSystemLoader, ChoiceLoader
1717

18-
from kernelci.config.base import get_system_arch
18+
from kernelci.config.base import get_system_arch, _format_dict_strings
1919

2020

2121
class Job:
@@ -179,14 +179,28 @@ def get_params(self, job, api_config=None):
179179
'runtime_image': job.config.image,
180180
'device_type': device_type,
181181
}
182-
if job.platform_config.params:
183-
params.update(job.platform_config.params)
184-
if device_dtb:
185-
params['device_dtb'] = device_dtb
186-
params.update(job.config.params)
187-
arch = params.get('arch') or job.platform_config.arch
182+
platform_params = job.platform_config.params or {}
183+
job_params = job.config.params
184+
arch = (
185+
(job_params.get('arch') if job_params else None)
186+
or platform_params.get('arch')
187+
or job.platform_config.arch
188+
)
189+
params['arch'] = arch
188190
for system in ('brarch', 'crosarch', 'debarch', 'karch'):
189191
params.update({system: get_system_arch(system, arch)})
192+
if platform_params:
193+
platform_map = params.copy()
194+
platform_map.update(platform_params)
195+
formatted_platform_params = _format_dict_strings(platform_params.copy(), platform_map)
196+
params.update(formatted_platform_params)
197+
if device_dtb:
198+
params['device_dtb'] = device_dtb
199+
if job_params:
200+
job_map = params.copy()
201+
job_map.update(job_params)
202+
formatted_job_params = _format_dict_strings(job_params.copy(), job_map)
203+
params.update(formatted_job_params)
190204
return params
191205

192206
@classmethod

0 commit comments

Comments
 (0)