stages: add org.osbuild.machine-id stage
This is a variation of PR https://github.com/osbuild/osbuild/pull/960 that put the machine-id handling into it's own stage and adds explicit handling what should happen with it. For machine-id(5) we essentially want the following three states implemented: 1. `first-boot: yes` will ensure that /etc/machine-id is in the "uninitialized" state. This means on boot the systemd `ConditionFirstBoot` is triggered and a new id in `/etc/machine-id` is created. This will work for systemd v247+. 2. `first-boot: no` will ensure that /etc/machine-id exists but is empty. This will trigger the creation of a new machine-id but will *not* trigger `ConditionFirstBoot`. 3. `first-boot: preserve` will just keep the existing machine-id. Note that it will error if there is no /etc/machine-id Note that the `org.osbuild.rpm` will also create a `{tree}/etc/machine-id` while it runs to ensure that postinst scripts will not fail that rely on this file. This is an implementation detail but unfortunately the rpm stage will leave an empty machine-id file if it was missing. So we cannot just remove /etc/machine-id because any following rpm stage would re-create it again (and we cannot change that without breaking backward compatiblity). Thanks to the special semantic that a missing /etc/machine-id and an /etc/machine-id with the `uninitialized` string are equivalent we don't care. To support systemd versions below v247 we could offer an option to remove /etc/machine-id. But the downside of this is that it would only work if the org.osbuild.machine-id stage is after the rpm stage. See also the discussion in PR#960. Thanks to Tom, Christian for the PR and the background.
This commit is contained in:
parent
e191dc1d0d
commit
0a2e0bb3d2
2 changed files with 137 additions and 0 deletions
55
stages/org.osbuild.machine-id
Executable file
55
stages/org.osbuild.machine-id
Executable file
|
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/python3
|
||||
"""
|
||||
Deal with /etc/machine-id
|
||||
|
||||
Explicitly define the state to /etc/machine-id. The possible values for
|
||||
first-boot are:
|
||||
|
||||
- yes: This sets the machine-id to "uninitialized" and this will trigger
|
||||
ContidionFirstBoot in systemd
|
||||
- no: This creates an empty machine-id. It will trigger the generation
|
||||
of a new machine-id but *not* the ConditionFirstBoot
|
||||
- preserve: Leave the existing machine-id in place. Not having a machine-id
|
||||
with that set is an error.
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import osbuild.api
|
||||
|
||||
SCHEMA = """
|
||||
"additionalProperties": false,
|
||||
"required": ["first-boot"],
|
||||
"properties": {
|
||||
"first-boot": {
|
||||
"enum": ["yes", "no", "preserve"],
|
||||
"description": "Set the first boot behavior of the /etc/machine-id file in the tree"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def main(tree, options):
|
||||
mode = options["first-boot"]
|
||||
|
||||
machine_id_file = pathlib.Path(tree) / "etc/machine-id"
|
||||
if mode == "yes":
|
||||
# available since systemd v247, systemd PR#16939
|
||||
machine_id_file.write_bytes(b"uninitialized\n")
|
||||
elif mode == "no":
|
||||
with machine_id_file.open("wb") as fp:
|
||||
fp.truncate(0)
|
||||
elif mode == "preserve":
|
||||
if not machine_id_file.is_file():
|
||||
print(f"{tree}/etc/machine-id cannot be preserved, it does not exist")
|
||||
return 1
|
||||
else:
|
||||
raise ValueError(f"unexpected machine-id mode '{mode}'")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = osbuild.api.arguments()
|
||||
r = main(args["tree"], args["options"])
|
||||
sys.exit(r)
|
||||
82
stages/test/test_machine-id.py
Normal file
82
stages/test/test_machine-id.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import unittest.mock
|
||||
|
||||
import pytest
|
||||
|
||||
import osbuild.meta
|
||||
from osbuild.testutil.imports import import_module_from_path
|
||||
|
||||
|
||||
def stage(stage_name):
|
||||
test_dir = pathlib.Path(__file__).parent
|
||||
stage_path = pathlib.Path(test_dir) / f"../org.osbuild.{stage_name}"
|
||||
return import_module_from_path("stage", os.fspath(stage_path))
|
||||
|
||||
|
||||
@pytest.fixture(name="machine_id_path")
|
||||
def machine_id_path_fixture(tmp_path):
|
||||
machine_id_path = tmp_path / "etc/machine-id"
|
||||
machine_id_path.parent.mkdir()
|
||||
return machine_id_path
|
||||
|
||||
|
||||
@pytest.mark.parametrize("already_has_etc_machine_id", [True, False])
|
||||
def test_machine_id_first_boot_yes(tmp_path, machine_id_path, already_has_etc_machine_id):
|
||||
if already_has_etc_machine_id:
|
||||
machine_id_path.touch()
|
||||
|
||||
stage("machine-id").main(tmp_path, {"first-boot": "yes"})
|
||||
assert machine_id_path.read_bytes() == b"uninitialized\n"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("already_has_etc_machine_id", [True, False])
|
||||
def test_machine_id_first_boot_no(tmp_path, machine_id_path, already_has_etc_machine_id):
|
||||
if already_has_etc_machine_id:
|
||||
machine_id_path.write_bytes(b"\x01\x02\x03")
|
||||
|
||||
stage("machine-id").main(tmp_path, {"first-boot": "no"})
|
||||
assert machine_id_path.stat().st_size == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("already_has_etc_machine_id", [True, False])
|
||||
@unittest.mock.patch("builtins.print")
|
||||
def test_machine_id_first_boot_preserve(mock_print, tmp_path, machine_id_path, already_has_etc_machine_id):
|
||||
if already_has_etc_machine_id:
|
||||
machine_id_path.write_bytes(b"\x01\x02\x03")
|
||||
|
||||
ret = stage("machine-id").main(tmp_path, {"first-boot": "preserve"})
|
||||
if already_has_etc_machine_id:
|
||||
assert os.stat(machine_id_path).st_size == 3
|
||||
else:
|
||||
assert ret == 1
|
||||
mock_print.assert_called_with(f"{tmp_path}/etc/machine-id cannot be preserved, it does not exist")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("test_data,expected_err", [
|
||||
({"first-boot": "invalid-option"}, "'invalid-option' is not one of "),
|
||||
])
|
||||
def test_machine_id_schema_validation(test_data, expected_err):
|
||||
name = "org.osbuild.machine-id"
|
||||
root = pathlib.Path(__file__).parents[2]
|
||||
mod_info = osbuild.meta.ModuleInfo.load(root, "Stage", name)
|
||||
schema = osbuild.meta.Schema(mod_info.get_schema(), name)
|
||||
|
||||
test_input = {
|
||||
"name": "org.osbuild.machine-id",
|
||||
"options": {},
|
||||
}
|
||||
test_input["options"].update(test_data)
|
||||
res = schema.validate(test_input)
|
||||
|
||||
assert res.valid is False
|
||||
assert len(res.errors) == 1
|
||||
err_msgs = [e.as_dict()["message"] for e in res.errors]
|
||||
assert expected_err in err_msgs[0]
|
||||
|
||||
|
||||
def test_machine_id_first_boot_unknown(tmp_path):
|
||||
with pytest.raises(ValueError, match=r"unexpected machine-id mode 'invalid'"):
|
||||
stage("machine-id").main(tmp_path, {"first-boot": "invalid"})
|
||||
Loading…
Add table
Add a link
Reference in a new issue