ostree: add convenience function for using default OSTree deployment

This adds a `default: true` option for all cases where OSTree
information is specified in schemas and allows for the information
to be picked up from the filesystem.

This is a safe operation because when building disk images there is
no known case where having two deployments makes sense. In the case
there ever were a case then the osname, ref, and serial options still
exist and can be used.

Co-authored-by: Luke Yang <luyang@redhat.com>
Co-authored-by: Michael Vogt <michael.vogt@gmail.com>
This commit is contained in:
Dusty Mabe 2024-01-25 13:57:51 -05:00
parent 2021b915f1
commit e1cbf92673
11 changed files with 264 additions and 49 deletions

View file

@ -41,7 +41,26 @@ SCHEMA_2 = """
"deployment": { "deployment": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["osname", "ref"], "oneOf": [
{
"properties": {
"default": {"enum": [false]}
},
"required": ["osname", "ref"]
},
{
"properties": {
"default": {"enum": [true]}
},
"not": {
"anyOf": [
{"required": ["osname"]},
{"required": ["ref"]},
{"required": ["serial"]}
]
}
}
],
"properties": { "properties": {
"osname": { "osname": {
"description": "Name of the stateroot to be used in the deployment", "description": "Name of the stateroot to be used in the deployment",
@ -55,6 +74,11 @@ SCHEMA_2 = """
"description": "The deployment serial (usually '0')", "description": "The deployment serial (usually '0')",
"type": "number", "type": "number",
"default": 0 "default": 0
},
"default": {
"description": "Find and use the default ostree deployment",
"type": "boolean",
"default": false
} }
} }
} }
@ -99,12 +123,9 @@ class OSTreeDeploymentMount(mounts.MountService):
tree = args["tree"] tree = args["tree"]
mountroot = args["root"] mountroot = args["root"]
options = args["options"] options = args["options"]
source = options.get("source", "tree") source = options.get("source", "tree")
deployment = options["deployment"] deployment = options["deployment"]
osname = deployment["osname"] osname, ref, serial = ostree.parse_deployment_option(tree, deployment)
ref = deployment["ref"]
serial = deployment.get("serial", 0)
# The user could specify either the tree or mountroot as the # The user could specify either the tree or mountroot as the
# place where we want the deployment to be mounted. # place where we want the deployment to be mounted.

View file

@ -1,13 +1,15 @@
import collections import collections
import contextlib import contextlib
import glob
import json import json
import os import os
import re
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import typing import typing
# pylint doesn't understand the string-annotation below # pylint doesn't understand the string-annotation below
from typing import Any, List # pylint: disable=unused-import from typing import Any, Dict, List, Tuple # pylint: disable=unused-import
from osbuild.util.rhsm import Subscriptions from osbuild.util.rhsm import Subscriptions
@ -214,7 +216,43 @@ def parse_input_commits(commits):
return commits["path"], data["refs"] return commits["path"], data["refs"]
def deployment_path(root: PathLike, osname: str, ref: str, serial: int): def parse_deployment_option(root: PathLike, deployment: Dict) -> Tuple[str, str, str]:
"""Parse the deployment option and return the osname, ref, and serial
The `deployment` arg contains the following sub fields:
- osname: Name of the stateroot used in the deployment (ie. fedora-coreos)
- ref: OStree ref to used for the deployment (ie. fedora/aarch64/coreos/next)
- serial: The deployment serial (ie. 0)
- default: Boolean to determine whether the default ostree deployment should be used
"""
default_deployment = deployment.get("default")
if default_deployment:
filenames = glob.glob(os.path.join(root, 'ostree/deploy/*/deploy/*.0'))
if len(filenames) < 1:
raise ValueError("Could not find deployment")
if len(filenames) > 1:
raise ValueError(f"More than one deployment found: {filenames}")
# We pick up the osname, commit, and serial from the filesystem
# here. We'll return the detected commit as the ref in this
# since it's a valid substitute for all subsequent uses in
# the code base.
f = re.search("/ostree/deploy/(.*)/deploy/(.*)\\.([0-9])", filenames[0])
if not f:
raise ValueError("cannot find ostree deployment in {filenames[0]}")
osname = f.group(1)
commit = f.group(2)
serial = f.group(3)
return osname, commit, serial
osname = deployment["osname"]
ref = deployment["ref"]
serial = deployment.get("serial", 0)
return osname, ref, serial
def deployment_path(root: PathLike, osname: str = "", ref: str = "", serial: int = 0):
"""Return the path to a deployment given the parameters""" """Return the path to a deployment given the parameters"""
base = os.path.join(root, "ostree") base = os.path.join(root, "ostree")

View file

@ -32,7 +32,26 @@ SCHEMA_2 = r"""
"deployment": { "deployment": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["osname", "ref"], "oneOf": [
{
"properties": {
"default": {"enum": [false]}
},
"required": ["osname", "ref"]
},
{
"properties": {
"default": {"enum": [true]}
},
"not": {
"anyOf": [
{"required": ["osname"]},
{"required": ["ref"]},
{"required": ["serial"]}
]
}
}
],
"properties": { "properties": {
"osname": { "osname": {
"description": "Name of the stateroot to be used in the deployment", "description": "Name of the stateroot to be used in the deployment",
@ -46,6 +65,11 @@ SCHEMA_2 = r"""
"description": "The deployment serial (usually '0')", "description": "The deployment serial (usually '0')",
"type": "number", "type": "number",
"default": 0 "default": 0
},
"default": {
"description": "Find and use the default ostree deployment",
"type": "boolean",
"default": false
} }
} }
}, },
@ -101,9 +125,7 @@ def main(args, options):
# we'll call ostree.deployment_path() helper to find it for us. # we'll call ostree.deployment_path() helper to find it for us.
root = mounts root = mounts
if deployment: if deployment:
osname = deployment["osname"] osname, ref, serial = ostree.parse_deployment_option(mounts, deployment)
ref = deployment["ref"]
serial = deployment.get("serial", 0)
root = ostree.deployment_path(mounts, osname, ref, serial) root = ostree.deployment_path(mounts, osname, ref, serial)
bootupd_args = [] bootupd_args = []

View file

@ -29,7 +29,26 @@ SCHEMA = """
"deployment": { "deployment": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["osname","ref"], "oneOf": [
{
"properties": {
"default": {"enum": [false]}
},
"required": ["osname", "ref"]
},
{
"properties": {
"default": {"enum": [true]}
},
"not": {
"anyOf": [
{"required": ["osname"]},
{"required": ["ref"]},
{"required": ["serial"]}
]
}
}
],
"properties": { "properties": {
"osname": { "osname": {
"description": "Name of the stateroot to be used in the deployment", "description": "Name of the stateroot to be used in the deployment",
@ -43,6 +62,11 @@ SCHEMA = """
"description": "The deployment serial (usually '0')", "description": "The deployment serial (usually '0')",
"type": "number", "type": "number",
"default": 0 "default": 0
},
"default": {
"description": "Find and use the default ostree deployment",
"type": "boolean",
"default": false
} }
} }
} }
@ -118,9 +142,7 @@ def main(tree, options):
if ostree_options: if ostree_options:
deployment = ostree_options["deployment"] deployment = ostree_options["deployment"]
osname = deployment["osname"] osname, ref, serial = ostree.parse_deployment_option(tree, deployment)
ref = deployment["ref"]
serial = deployment.get("serial", 0)
root = ostree.deployment_path(tree, osname, ref, serial) root = ostree.deployment_path(tree, osname, ref, serial)

View file

@ -19,6 +19,7 @@ COREOS_ALEPH_FILENAME = ".coreos-aleph-version.json"
SCHEMA_2 = """ SCHEMA_2 = """
"options": { "options": {
"additionalProperties": false, "additionalProperties": false,
"required": ["deployment"],
"properties": { "properties": {
"coreos_compat": { "coreos_compat": {
"description": "boolean to allow for CoreOS aleph version backwards compatibility", "description": "boolean to allow for CoreOS aleph version backwards compatibility",
@ -26,7 +27,26 @@ SCHEMA_2 = """
}, },
"deployment": { "deployment": {
"additionalProperties": false, "additionalProperties": false,
"required": ["osname", "ref"], "oneOf": [
{
"properties": {
"default": {"enum": [false]}
},
"required": ["osname", "ref"]
},
{
"properties": {
"default": {"enum": [true]}
},
"not": {
"anyOf": [
{"required": ["osname"]},
{"required": ["ref"]},
{"required": ["serial"]}
]
}
}
],
"properties": { "properties": {
"osname": { "osname": {
"description": "Name of the stateroot to be used in the deployment", "description": "Name of the stateroot to be used in the deployment",
@ -40,6 +60,11 @@ SCHEMA_2 = """
"description": "The deployment serial (usually '0')", "description": "The deployment serial (usually '0')",
"type": "number", "type": "number",
"default": 0 "default": 0
},
"default": {
"description": "Find and use the default ostree deployment",
"type": "boolean",
"default": false
} }
} }
} }
@ -131,9 +156,7 @@ def construct_aleph_json(tree, origin):
def main(tree, options): def main(tree, options):
coreos_compat = options.get("coreos_compat", False) coreos_compat = options.get("coreos_compat", False)
dep = options["deployment"] dep = options["deployment"]
osname = dep["osname"] osname, ref, serial = ostree.parse_deployment_option(tree, dep)
ref = dep["ref"]
serial = dep.get("serial", 0)
origin = ostree.deployment_path(tree, osname, ref, serial) + ".origin" origin = ostree.deployment_path(tree, osname, ref, serial) + ".origin"
data = construct_aleph_json(tree, origin) data = construct_aleph_json(tree, origin)

View file

@ -18,7 +18,26 @@ SCHEMA = """
"properties": { "properties": {
"deployment": { "deployment": {
"additionalProperties": false, "additionalProperties": false,
"required": ["osname", "ref"], "oneOf": [
{
"properties": {
"default": {"enum": [false]}
},
"required": ["osname", "ref"]
},
{
"properties": {
"default": {"enum": [true]}
},
"not": {
"anyOf": [
{"required": ["osname"]},
{"required": ["ref"]},
{"required": ["serial"]}
]
}
}
],
"properties": { "properties": {
"osname": { "osname": {
"description": "Name of the stateroot to be used in the deployment", "description": "Name of the stateroot to be used in the deployment",
@ -32,6 +51,11 @@ SCHEMA = """
"description": "The deployment serial (usually '0')", "description": "The deployment serial (usually '0')",
"type": "number", "type": "number",
"default": 0 "default": 0
},
"default": {
"description": "Find and use the default ostree deployment",
"type": "boolean",
"default": false
} }
} }
} }
@ -71,9 +95,7 @@ def populate_var(sysroot):
def main(tree, options): def main(tree, options):
dep = options["deployment"] dep = options["deployment"]
osname = dep["osname"] osname, ref, serial = ostree.parse_deployment_option(tree, dep)
ref = dep["ref"]
serial = dep.get("serial", 0)
deployment = ostree.deployment_path(tree, osname, ref, serial) deployment = ostree.deployment_path(tree, osname, ref, serial)
var = os.path.join(tree, "ostree", "deploy", osname, "var") var = os.path.join(tree, "ostree", "deploy", osname, "var")

View file

@ -21,7 +21,26 @@ SCHEMA = """
"properties": { "properties": {
"deployment": { "deployment": {
"additionalProperties": false, "additionalProperties": false,
"required": ["osname", "ref"], "oneOf": [
{
"properties": {
"default": {"enum": [false]}
},
"required": ["osname", "ref"]
},
{
"properties": {
"default": {"enum": [true]}
},
"not": {
"anyOf": [
{"required": ["osname"]},
{"required": ["ref"]},
{"required": ["serial"]}
]
}
}
],
"properties": { "properties": {
"osname": { "osname": {
"description": "Name of the stateroot to be used in the deployment", "description": "Name of the stateroot to be used in the deployment",
@ -35,6 +54,11 @@ SCHEMA = """
"description": "The deployment serial (usually '0')", "description": "The deployment serial (usually '0')",
"type": "number", "type": "number",
"default": 0 "default": 0
},
"default": {
"description": "Find and use the default ostree deployment",
"type": "boolean",
"default": false
} }
} }
} }
@ -44,9 +68,7 @@ SCHEMA = """
def main(tree, options): def main(tree, options):
dep = options["deployment"] dep = options["deployment"]
osname = dep["osname"] osname, ref, serial = ostree.parse_deployment_option(tree, dep)
ref = dep["ref"]
serial = dep.get("serial", 0)
# this created a state root at `osname` # this created a state root at `osname`
stateroot = f"{tree}/ostree/deploy/{osname}" stateroot = f"{tree}/ostree/deploy/{osname}"

View file

@ -12,10 +12,17 @@ STAGE_NAME = "org.osbuild.bootupd"
@pytest.mark.parametrize("test_data,expected_err", [ @pytest.mark.parametrize("test_data,expected_err", [
# bad # bad
({"deployment": "must-be-object"}, "'must-be-object' is not of type 'object'"), ({"deployment": "must-be-object"}, "'must-be-object' is not of type 'object'"),
({"deployment": {"osname": "some-os"}}, "'ref' is a required property"), ({"deployment": {"osname": "some-os"}}, "{'osname': 'some-os'} is not valid under any of the given schemas"),
({"deployment": {"ref": "some-ref"}}, "'osname' is a required property"), ({"deployment": {"ref": "some-ref"}}, "{'ref': 'some-ref'} is not valid under any of the given schemas"),
({"deployment": {"osname": "some-os", "ref": "some-ref", "serial": "must-be-number"}}, ({"deployment": {"osname": "some-os", "ref": "some-ref", "serial": "must-be-number"}},
"'must-be-number' is not of type 'number'"), "'must-be-number' is not of type 'number'"),
({"deployment": {"default": False}}, "{'default': False} is not valid under any of the given schemas"),
({"deployment": {
"osname": "some-os",
"ref": "some-ref",
"serial": 0,
"default": True}
}, "{'osname': 'some-os', 'ref': 'some-ref', 'serial': 0, 'default': True} is not valid under any of the given schemas"),
({"random": "property"}, "Additional properties are not allowed"), ({"random": "property"}, "Additional properties are not allowed"),
({"bios": {}}, "'device' is a required property"), ({"bios": {}}, "'device' is a required property"),
({"bios": "must-be-object"}, "'must-be-object' is not of type 'object'"), ({"bios": "must-be-object"}, "'must-be-object' is not of type 'object'"),
@ -33,6 +40,17 @@ STAGE_NAME = "org.osbuild.bootupd"
{ {
"device": "/dev/sda", "device": "/dev/sda",
}, },
}, ""),
({
"deployment":
{
"default": True,
},
"static-configs": True,
"bios":
{
"device": "/dev/sda",
},
}, "") }, "")
]) ])

View file

@ -518,8 +518,7 @@
"options": { "options": {
"coreos_compat": true, "coreos_compat": true,
"deployment": { "deployment": {
"ref": "ostree/1/1/0", "default": true
"osname": "fedora-coreos"
} }
} }
}, },
@ -527,8 +526,7 @@
"type": "org.osbuild.ostree.selinux", "type": "org.osbuild.ostree.selinux",
"options": { "options": {
"deployment": { "deployment": {
"ref": "ostree/1/1/0", "default": true
"osname": "fedora-coreos"
} }
} }
} }
@ -705,8 +703,7 @@
}, },
"static-configs": true, "static-configs": true,
"deployment": { "deployment": {
"ref": "ostree/1/1/0", "default": true
"osname": "fedora-coreos"
} }
}, },
"devices": { "devices": {
@ -956,8 +953,7 @@
"options": { "options": {
"static-configs": true, "static-configs": true,
"deployment": { "deployment": {
"ref": "ostree/1/1/0", "default": true
"osname": "fedora-coreos"
} }
}, },
"devices": { "devices": {

View file

@ -129,15 +129,11 @@ pipelines:
options: options:
coreos_compat: true coreos_compat: true
deployment: deployment:
ref: ostree/1/1/0 default: true
osname:
mpp-format-string: '{osname}'
- type: org.osbuild.ostree.selinux - type: org.osbuild.ostree.selinux
options: options:
deployment: deployment:
ref: ostree/1/1/0 default: true
osname:
mpp-format-string: '{osname}'
- name: raw-image - name: raw-image
build: name:build build: name:build
stages: stages:
@ -242,9 +238,7 @@ pipelines:
device: disk device: disk
static-configs: true static-configs: true
deployment: deployment:
ref: ostree/1/1/0 default: true
osname:
mpp-format-string: '{osname}'
devices: devices:
disk: disk:
type: org.osbuild.loopback type: org.osbuild.loopback
@ -408,9 +402,7 @@ pipelines:
options: options:
static-configs: true static-configs: true
deployment: deployment:
ref: ostree/1/1/0 default: true
osname:
mpp-format-string: '{osname}'
devices: devices:
disk: disk:
type: org.osbuild.loopback type: org.osbuild.loopback

View file

@ -11,6 +11,7 @@ import unittest
import pytest import pytest
from osbuild.testutil import make_fake_tree
from osbuild.util import ostree from osbuild.util import ostree
from .. import test from .. import test
@ -208,3 +209,41 @@ class TestPasswdLike(unittest.TestCase):
check.read_from(file) check.read_from(file)
assert subids.db == check.db assert subids.db == check.db
def test_parse_default_deployment_happy(tmp_path):
deployment = {
"default": True,
}
make_fake_tree(tmp_path, {
"ostree/deploy/fedora-coreos/deploy/72f807.0": "",
})
osname, ref, serial = ostree.parse_deployment_option(tmp_path, deployment)
assert osname == "fedora-coreos"
assert ref == "72f807"
assert serial == "0"
def test_parse_default_deployment_sad(tmp_path):
deployment = {
"default": True,
}
make_fake_tree(tmp_path, {
"ostree/deploy/fedora-coreos/deploy/72f807.0": "",
"ostree/deploy/fedora-coreos/deploy/123456.0": "",
})
with pytest.raises(ValueError) as exp:
ostree.parse_deployment_option(tmp_path, deployment)
assert "More than one deployment found: [" in str(exp.value)
def test_parse_deployment_happy(tmp_path):
deployment = {
"osname": "fedora-coreos",
"ref": "some-ref",
"serial": "0",
}
osname, ref, serial = ostree.parse_deployment_option(tmp_path, deployment)
assert osname == "fedora-coreos"
assert ref == "some-ref"
assert serial == "0"