Skip to content

Commit

Permalink
stages: add org.osbuild.machine-id stage
Browse files Browse the repository at this point in the history
This is a variation of PR osbuild#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. `machine-id: no` will ensure that no /etc/machine-id file is preent
   in the tree. This means on boot the systemd `ConditionFirstBoot`
   is triggered and a new `/etc/machine-id` is created.
2. `machine-id: empty` 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. `machine-id: 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 this
stage has to run *after* the rpm stage.

See also the discussion in PR#960.

Thanks to Tom, Christian for the PR and the background.
  • Loading branch information
mvo5 committed Nov 14, 2023
1 parent d52738d commit 38ccba0
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 0 deletions.
47 changes: 47 additions & 0 deletions stages/org.osbuild.machine-id
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/python3
"""
Deal with /etc/machine-id
Explicitly define what should happen to /etc/machine-id.
Note that this stage *must* run after the rpm stage to be reliable.
"""

import pathlib
import sys

import osbuild.api

SCHEMA = """
"additionalProperties": false,
"required": ["machine-id"],
"properties": {
"machine-id": {
"enum": ["no", "empty", "preserve"],
"description": "Ensure the state of the /etc/machine-id file in the tree"
}
}
"""


def main(tree, options):
mode = options["machine-id"]

# use "match" here once we are on py3.10
machine_id_file = pathlib.Path(f"{tree}/etc/machine-id")
if mode == "no":
machine_id_file.unlink(missing_ok=True)
elif mode == "empty":
with open(machine_id_file, "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
return 0


if __name__ == '__main__':
args = osbuild.api.arguments()
r = main(args["tree"], args["options"])
sys.exit(r)
81 changes: 81 additions & 0 deletions stages/test/test_machine-id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/python3

import os
import pathlib
import mock
import subprocess

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(f"{test_dir}/../org.osbuild.{stage_name}")
return import_module_from_path("stage", os.fspath(stage_path))


@pytest.fixture
def machine_id_path(tmp_path):
machine_id_path = pathlib.Path(f"{tmp_path}/etc/machine-id")
machine_id_path.parent.mkdir()
return machine_id_path


@pytest.mark.parametrize("already_has_etc_machine_id", [True, False])
@mock.patch("os.unlink", wraps=os.unlink)
def test_machine_id_no(mock_unlink, 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, {"machine-id": "no"})
assert not machine_id_path.exists()
mock_unlink.assert_called_with(machine_id_path)


@pytest.mark.parametrize("already_has_etc_machine_id", [True, False])
def test_machine_id_empty(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, {"machine-id": "empty"})
assert machine_id_path.stat().st_size == 0


@pytest.mark.parametrize("already_has_etc_machine_id", [True, False])
@mock.patch("builtins.print")
def test_machine_id_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, {"machine-id": "preserve"})
if already_has_etc_machine_id:
machine_id_path = os.path.join(tmp_path, "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", [
({"machine-id": "invalid-option"}, "'invalid-option' is not one of "),
])
def test_schema_validation(test_data, expected_err):
name = "org.osbuild.machine-id"
root = os.path.join(os.path.dirname(__file__), "../..")
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]

0 comments on commit 38ccba0

Please sign in to comment.