debian-forge/osbuild/devices.py
Michael Vogt f5d6d11f1d osbuild: error when {Device,Mount} is modified after creation
This is a drive-by change after spending some quality time with the
mount code. The `id` field of `Mount` is calculated only once and
only when creating a `Mount`. This seems slightly dangerous as
any change to an attribute after creation will not update the
id. This means two options:
1. dynamically update the `id` on changes
2. forbid changes after the `id` is calculcated

I went with (2) but happy to discuss of course but it seems more
the spirit of the class.

It also does the same change for "devices.Device"
2024-01-19 02:54:26 +01:00

137 lines
3.9 KiB
Python

"""
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 errno
import hashlib
import json
import os
import stat
from typing import Any, Dict, Optional
from osbuild import host
from osbuild.mixins import MixinImmutableID
from osbuild.util import ctx
class Device(MixinImmutableID):
"""
A single device with its corresponding options
"""
def __init__(self, name, info, parent, options: Dict):
self.name = name
self.info = info
self.parent = parent
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())
if self.parent:
m.update(json.dumps(self.parent.id, sort_keys=True).encode())
m.update(json.dumps(self.options, sort_keys=True).encode())
return m.hexdigest()
class DeviceManager:
"""Manager for Devices
Uses a `host.ServiceManager` to open `Device` instances.
"""
def __init__(self, mgr: host.ServiceManager, devpath: str, tree: str) -> None:
self.service_manager = mgr
self.devpath = devpath
self.tree = tree
self.devices: Dict[str, Dict[str, Any]] = {}
def device_relpath(self, dev: Optional[Device]) -> Optional[str]:
if dev is None:
return None
return self.devices[dev.name]["path"]
def device_abspath(self, dev: Optional[Device]) -> Optional[str]:
relpath = self.device_relpath(dev)
if relpath is None:
return None
return os.path.join(self.devpath, relpath)
def open(self, dev: Device) -> Dict:
parent = self.device_relpath(dev.parent)
args = {
# global options
"dev": self.devpath,
"tree": os.fspath(self.tree),
"parent": parent,
# per device options
"options": dev.options,
}
mgr = self.service_manager
client = mgr.start(f"device/{dev.name}", dev.info.path)
res = client.call("open", args)
self.devices[dev.name] = res
return res
class DeviceService(host.Service):
"""Device host service"""
@staticmethod
def ensure_device_node(path, major: int, minor: int, dir_fd=None):
"""Ensure that the specified device node exists at the given path"""
mode = 0o666 | stat.S_IFBLK
with ctx.suppress_oserror(errno.EEXIST):
os.mknod(path, mode, os.makedev(major, minor), dir_fd=dir_fd)
@abc.abstractmethod
def open(self, devpath: str, parent: 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["parent"],
args["tree"],
args["options"])
return r, None
if method == "close":
r = self.close()
return r, None
raise host.ProtocolError("Unknown method")