diff --git a/osbuild/devices.py b/osbuild/devices.py new file mode 100644 index 00000000..1b5294ab --- /dev/null +++ b/osbuild/devices.py @@ -0,0 +1,84 @@ +""" +Device Handling for pipeline stages + +Specific type of artifacts require device support, such as +loopback devices or device mapper. Since stages are always +run in a container and are isolated from the host, they do +not have direct access to devices and specifically can not +setup new ones. +Therefore device handling is done at the osbuild level with +the help of a device host services. Device specific modules +provide the actual functionality and thus the core device +support in osbuild itself is abstract. +""" + +import abc +import hashlib +import json + +from typing import Dict + +from osbuild import host + + +class Device: + """ + A single device with its corresponding options + """ + + def __init__(self, name, info, options: Dict): + self.name = name + self.info = info + self.options = options or {} + self.id = self.calc_id() + + def calc_id(self): + # NB: Since the name of the device is arbitrary or prescribed + # by the stage, it is not included in the id calculation. + m = hashlib.sha256() + m.update(json.dumps(self.info.name, sort_keys=True).encode()) + m.update(json.dumps(self.options, sort_keys=True).encode()) + return m.hexdigest() + + def open(self, mgr: host.ServiceManager, dev: str, tree: str) -> Dict: + args = { + # global options + "dev": dev, + "tree": tree, + + # per device options + "options": self.options, + } + + client = mgr.start(f"device/{self.name}", self.info.path) + res = client.call("open", args) + + return res + + +class DeviceService(host.Service): + """Device host service""" + + @abc.abstractmethod + def open(self, devpath: str, tree: str, options: Dict): + """Open a specific device + + This method must be implemented by the specific device service. + It should open the device and create a device node in `devpath`. + The return value must contain the relative path to the device + node. + """ + + @abc.abstractmethod + def close(self): + """Close the device""" + + def stop(self): + self.close() + + def dispatch(self, method: str, args, _fds): + if method == "open": + r = self.open(args["dev"], args["tree"], args["options"]) + return r, None + + raise host.ProtocolError("Unknown method") diff --git a/osbuild/formats/v2.py b/osbuild/formats/v2.py index b240d821..ebcf154e 100644 --- a/osbuild/formats/v2.py +++ b/osbuild/formats/v2.py @@ -28,6 +28,23 @@ def describe(manifest: Manifest, *, with_id=False) -> Dict: pl = manifest[pid] return f"name:{pl.name}" + def describe_device(dev): + desc = { + "type": dev.info.name + } + + if dev.options: + desc["options"] = dev.options + + return desc + + def describe_devices(devs: Dict): + desc = { + name: describe_device(dev) + for name, dev in devs.items() + } + return desc + def describe_input(ip: Input): origin = ip.origin desc = { @@ -66,6 +83,10 @@ def describe(manifest: Manifest, *, with_id=False) -> Dict: if s.options: desc["options"] = s.options + devs = describe_devices(s.devices) + if devs: + desc["devices"] = devs + ips = describe_inputs(s.inputs) if ips: desc["inputs"] = ips @@ -130,6 +151,16 @@ def resolve_ref(name: str, manifest: Manifest) -> str: return target.id +def load_device(name: str, description: Dict, index: Index, stage: Stage): + device_type = description["type"] + options = description.get("options", {}) + + info = index.get_module_info("Device", device_type) + if not info: + raise TypeError(f"Missing meta information for {device_type}") + stage.add_device(name, info, options) + + def load_input(name: str, description: Dict, index: Index, stage: Stage, manifest: Manifest): input_type = description["type"] origin = description["origin"] @@ -163,6 +194,10 @@ def load_stage(description: Dict, index: Index, pipeline: Pipeline, manifest: Ma stage = pipeline.add_stage(info, opts) + devs = description.get("devices", {}) + for name, desc in devs.items(): + load_device(name, desc, index, stage) + ips = description.get("inputs", {}) for name, desc in ips.items(): load_input(name, desc, index, stage, manifest) @@ -319,6 +354,7 @@ def validate(manifest: Dict, index: Index) -> ValidationResult: res = schema.validate(stage) result.merge(res, path=path) + validate_stage_modules("Device", stage, path) validate_stage_modules("Input", stage, path) def validate_pipeline(pipeline, path): diff --git a/osbuild/meta.py b/osbuild/meta.py index 61af8cd1..f0251a44 100644 --- a/osbuild/meta.py +++ b/osbuild/meta.py @@ -281,6 +281,7 @@ class ModuleInfo: # Known modules and their corresponding directory name MODULES = { "Assembler": "assemblers", + "Device": "devices", "Input": "inputs", "Source": "sources", "Stage": "stages", @@ -330,6 +331,13 @@ class ModuleInfo: **opts, } schema["required"] = [type_id] + elif self.type in ("Device", ): + schema["additionalProperties"] = True + opts = self._load_opts(version, "1") + schema["properties"] = { + "type": {"enum": [self.name]}, + "options": opts + } else: opts = self._load_opts(version, "1") schema.update(opts) diff --git a/osbuild/pipeline.py b/osbuild/pipeline.py index 7e94b651..d87004e7 100644 --- a/osbuild/pipeline.py +++ b/osbuild/pipeline.py @@ -10,6 +10,7 @@ from . import buildroot from . import host from . import objectstore from . import remoteloop +from .devices import Device from .inputs import Input from .sources import Source from .util import osrelease @@ -43,6 +44,7 @@ class Stage: self.options = options self.checkpoint = False self.inputs = {} + self.devices = {} @property def name(self): @@ -65,6 +67,11 @@ class Stage: self.inputs[name] = ip return ip + def add_device(self, name, info, options): + dev = Device(name, info, options) + self.devices[name] = dev + return dev + def run(self, tree, runner, build_tree, store, monitor, libdir): with contextlib.ExitStack() as cm: @@ -76,12 +83,17 @@ class Stage: inputs_mapped = "/run/osbuild/inputs" inputs = {} + devices_mapped = "/dev" + devices = {} + args = { "tree": "/run/osbuild/tree", "options": self.options, "paths": { + "devices": devices_mapped, "inputs": inputs_mapped }, + "devices": devices, "inputs": inputs, "meta": { "id": self.id @@ -103,6 +115,10 @@ class Stage: data = ip.map(mgr, storeapi, inputs_tmpdir) inputs[key] = data + for key, dev in self.devices.items(): + reply = dev.open(mgr, build_root.dev, tree) + devices[key] = reply + api = API(args, monitor) build_root.register_api(api) diff --git a/schemas/osbuild2.json b/schemas/osbuild2.json index 4e258810..a1d8742b 100644 --- a/schemas/osbuild2.json +++ b/schemas/osbuild2.json @@ -16,6 +16,26 @@ "definitions": { + "devices": { + "title": "Collection of devices for a stage", + "additionalProperties": { + "$ref": "#/definitions/device" + } + }, + + "device": { + "title": "Device for a stage", + "additionalProperties": false, + "required": ["type", "options"], + "properties": { + "type": { "type": "string" }, + "options": { + "type": "object", + "additionalProperties": true + } + } + }, + "inputs": { "title": "Collection of inputs for a stage", "additionalProperties": false, @@ -102,6 +122,7 @@ "additionalProperties": false, "properties": { "type": { "type": "string" }, + "devices": { "$ref": "#/definitions/devices" }, "inputs": {"$ref": "#/definitions/inputs" }, "options": { "type": "object",