forked from osbuild/osbuild
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
stages: add org.osbuild.machine-id stage
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
Showing
2 changed files
with
128 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |