diff --git a/stages/org.osbuild.kickstart b/stages/org.osbuild.kickstart index 82f949c173..48a050f13a 100755 --- a/stages/org.osbuild.kickstart +++ b/stages/org.osbuild.kickstart @@ -170,6 +170,74 @@ SCHEMA = r""" "type": "boolean" } } + }, + "autopart": { + "description": "Automatically creates partitions", + "type": "object", + "anyOf": [ + { "required": ["pbkdf-time"], + "not": {"required": ["pbkdf-iterations"]} + }, + { "required": ["pbkdf-iterations"], + "not": {"required": ["pbkdf-time"]} + }, + { "not": {"required": ["pbkdf-iterations", "pbkdf-time"]}} + ], + "properties": { + "type": { + "description": "Selects one of the predefined automatic partitioning schemes you want to use", + "type": "string", + "enum": ["lvm", "btrfs", "plain", "thinp"] + }, + "fstype": { + "description": "Specify a supported file system (such as ext4 or xfs) to replace the default when doing automatic partitioning", + "type": "string" + }, + "nolvm": { + "description": "Do not use LVM or Btrfs for automatic partitioning. This option is equal to --type=plain", + "type": "boolean" + }, + "encrypted": { + "description": "Encrypts all partitions", + "type": "boolean" + }, + "passphrase": { + "description": "Provides a default system-wide passphrase for all encrypted devices", + "type": "string" + }, + "escrowcert": { + "description": "Stores data encryption keys of all encrypted volumes as files in /root, encrypted using the X.509 certificate from the URL specified", + "type": "string" + }, + "backuppassphrase": { + "description": "Adds a randomly-generated passphrase to each encrypted volume", + "type": "string" + }, + "cipher": { + "description": "Specifies which type of encryption will be used if the Anaconda default aes-xts-plain64 is not satisfactory", + "type": "string" + }, + "luks-version": { + "description": "Specifies which version of LUKS should be used to encrypt the system", + "type": "string" + }, + "pbkdf": { + "description": "Sets Password-Based Key Derivation Function (PBKDF) algorithm for the LUKS keyslot", + "type": "string" + }, + "pbkdf-memory": { + "description": "Sets the memory cost for PBKDF", + "type": "integer" + }, + "pbkdf-time": { + "description": "Sets the number of milliseconds to spend with PBKDF passphrase processing", + "type": "integer" + }, + "pbkdf-iterations": { + "description": "Sets the number of iterations for passphrase processing directly", + "type": "integer" + } + } } } """ @@ -259,6 +327,26 @@ def make_clearpart(options: Dict) -> str: return cmd +def make_autopart(options: Dict) -> str: + autopart = options.get("autopart") + if autopart is None: + return "" + cmd = "autopart" + # TODO: make pkdf-{time,iterations} mutally exclusive at the + # schema level + for key in ["type", "fstype", "nolvm", "encrypted", "passphrase", + "escrowcert", "backuppassphrase", "cipher", "luks-version", + "pbkdf", "pbkdf-memory", "pbkdf-time", "pbkdf-iterations"]: + if not key in autopart: + continue + val = autopart[key] + if type(val) is bool: + cmd += f" --{key}" + else: + cmd += f" --{key}={val}" + return cmd + + def main(tree, options): path = options["path"].lstrip("/") ostree = options.get("ostree") @@ -301,6 +389,9 @@ def main(tree, options): clearpart = make_clearpart(options) if clearpart: config += [clearpart] + autopart = make_autopart(options) + if autopart: + config += [autopart] target = os.path.join(tree, path) base = os.path.dirname(target) diff --git a/stages/test/test_kickstart.py b/stages/test/test_kickstart.py index 22a3b78ca3..7199d57981 100644 --- a/stages/test/test_kickstart.py +++ b/stages/test/test_kickstart.py @@ -5,7 +5,7 @@ import pytest from osbuild.testutil.imports import import_module_from_path - +import osbuild.meta @pytest.mark.parametrize("test_input,expected", [ ({"lang": "en_US.UTF-8"}, "lang en_US.UTF-8"), @@ -16,6 +16,10 @@ "timezone": "UTC", }, "lang en_US.UTF-8\nkeyboard us\ntimezone UTC"), + ({"autopart": {}},"autopart"), + ({"autopart": {"type": "plain"}},"autopart --type=plain"), + ({"autopart": {"type": "lvm"}},"autopart --type=lvm"), + ({"autopart": {"nolvm": True}},"autopart --nolvm"), ]) def test_kickstart(tmp_path, test_input, expected): ks_stage_path = os.path.join(os.path.dirname(__file__), "../org.osbuild.kickstart") @@ -30,3 +34,35 @@ def test_kickstart(tmp_path, test_input, expected): with open(os.path.join(tmp_path, ks_path), encoding="utf-8") as fp: ks_content = fp.read() assert ks_content == expected + "\n" + + +@pytest.mark.parametrize("test_data,expected_err", [ + ({"autopart": {"pbkdf-time": 1}}, ""), + ({"autopart": {"pbkdf-iterations": 1}}, ""), + ({"autopart": {"pbkdf-time": 1, "pbkdf-iterations": 2}}, " is not valid "), +]) +def test_schema_validation_smoke(test_data, expected_err): + name = "org.osbuild.kickstart" + 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.kickstart", + "options": { + "path": "some-path", + } + } + test_input["options"].update(test_data) + res = schema.validate(test_input) + + if expected_err == "": + # debug + if not res.valid: + print([e.as_dict() for e in res.errors]) + assert res.valid is True + else: + 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]