From 617bccba079f6f2e67217a6319edc0f475444674 Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Wed, 10 Jan 2024 14:49:59 +0100 Subject: [PATCH 1/6] Add RPM OSTree source from container (#2125655) RPM OSTree is getting new functionality to use containers as the base image. Thanks to that it's possible to use standard container repositories. This source enables to use these repositories. Related: RHEL-2250 --- anaconda.spec.in | 2 +- configure.ac | 1 + pyanaconda/core/constants.py | 1 + pyanaconda/core/kickstart/commands.py | 1 + .../modules/common/constants/interfaces.py | 5 + .../modules/common/structures/rpm_ostree.py | 74 ++++++++++- pyanaconda/modules/payloads/constants.py | 4 +- pyanaconda/modules/payloads/kickstart.py | 1 + .../modules/payloads/source/Makefile.am | 3 +- pyanaconda/modules/payloads/source/factory.py | 4 + .../payloads/source/rpm_ostree/rpm_ostree.py | 1 + .../source/rpm_ostree_container/Makefile.am | 22 ++++ .../source/rpm_ostree_container/__init__.py | 0 .../rpm_ostree_container.py | 92 +++++++++++++ .../rpm_ostree_container_interface.py | 57 +++++++++ .../test_source_rpm_ostree_container.py | 121 ++++++++++++++++++ .../modules/payloads/test_module_payloads.py | 1 + 17 files changed, 386 insertions(+), 4 deletions(-) create mode 100644 pyanaconda/modules/payloads/source/rpm_ostree_container/Makefile.am create mode 100644 pyanaconda/modules/payloads/source/rpm_ostree_container/__init__.py create mode 100644 pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container.py create mode 100644 pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container_interface.py create mode 100644 tests/unit_tests/pyanaconda_tests/modules/payloads/source/test_source_rpm_ostree_container.py diff --git a/anaconda.spec.in b/anaconda.spec.in index 9509393bd7c..fdd0782c927 100644 --- a/anaconda.spec.in +++ b/anaconda.spec.in @@ -35,7 +35,7 @@ Source0: %{name}-%{version}.tar.bz2 %define libxklavierver 5.4 %define mehver 0.23-1 %define nmver 1.0 -%define pykickstartver 3.32.10-1 +%define pykickstartver 3.32.11-1 %define pypartedver 2.5-2 %define pythonblivetver 1:3.6.0-4 %define rpmver 4.10.0 diff --git a/configure.ac b/configure.ac index 59f76bbe6b9..7d61cd0f6f4 100644 --- a/configure.ac +++ b/configure.ac @@ -182,6 +182,7 @@ AC_CONFIG_FILES([Makefile pyanaconda/modules/payloads/source/nfs/Makefile pyanaconda/modules/payloads/source/repo_files/Makefile pyanaconda/modules/payloads/source/rpm_ostree/Makefile + pyanaconda/modules/payloads/source/rpm_ostree_container/Makefile pyanaconda/modules/payloads/source/url/Makefile pyanaconda/modules/storage/Makefile pyanaconda/modules/storage/bootloader/Makefile diff --git a/pyanaconda/core/constants.py b/pyanaconda/core/constants.py index df360a0b8b8..1496f8c5272 100644 --- a/pyanaconda/core/constants.py +++ b/pyanaconda/core/constants.py @@ -430,6 +430,7 @@ class DisplayModes(Enum): SOURCE_TYPE_LIVE_OS_IMAGE = "LIVE_OS_IMAGE" SOURCE_TYPE_LIVE_IMAGE = "LIVE_IMAGE" SOURCE_TYPE_RPM_OSTREE = "RPM_OSTREE" +SOURCE_TYPE_RPM_OSTREE_CONTAINER = "RPM_OSTREE_CONTAINER" SOURCE_TYPE_HMC = "HMC" SOURCE_TYPE_CDROM = "CDROM" SOURCE_TYPE_CLOSEST_MIRROR = "CLOSEST_MIRROR" diff --git a/pyanaconda/core/kickstart/commands.py b/pyanaconda/core/kickstart/commands.py index e01b402163a..181045f4e90 100644 --- a/pyanaconda/core/kickstart/commands.py +++ b/pyanaconda/core/kickstart/commands.py @@ -55,6 +55,7 @@ from pykickstart.commands.nfs import FC6_NFS as NFS from pykickstart.commands.nvdimm import F28_Nvdimm as Nvdimm from pykickstart.commands.ostreesetup import RHEL9_OSTreeSetup as OSTreeSetup +from pykickstart.commands.ostreecontainer import RHEL9_OSTreeContainer as OSTreeContainer from pykickstart.commands.partition import RHEL9_Partition as Partition from pykickstart.commands.raid import RHEL9_Raid as Raid from pykickstart.commands.realm import F19_Realm as Realm diff --git a/pyanaconda/modules/common/constants/interfaces.py b/pyanaconda/modules/common/constants/interfaces.py index ec031b9e0c8..01c39474314 100644 --- a/pyanaconda/modules/common/constants/interfaces.py +++ b/pyanaconda/modules/common/constants/interfaces.py @@ -79,6 +79,11 @@ basename="RPMOSTree" ) +PAYLOAD_SOURCE_RPM_OSTREE_CONTAINER = DBusInterfaceIdentifier( + namespace=SOURCE_NAMESPACE, + basename="RPMOSTreeContainer" +) + PAYLOAD_SOURCE = DBusInterfaceIdentifier( namespace=SOURCE_NAMESPACE ) diff --git a/pyanaconda/modules/common/structures/rpm_ostree.py b/pyanaconda/modules/common/structures/rpm_ostree.py index 9a09b77e300..38db37dcc8c 100644 --- a/pyanaconda/modules/common/structures/rpm_ostree.py +++ b/pyanaconda/modules/common/structures/rpm_ostree.py @@ -20,7 +20,7 @@ from dasbus.structure import DBusData from dasbus.typing import Str, Bool -__all__ = ["RPMOSTreeConfigurationData"] +__all__ = ["RPMOSTreeConfigurationData", "RPMOSTreeContainerConfigurationData"] class RPMOSTreeConfigurationData(DBusData): @@ -33,6 +33,11 @@ def __init__(self): self._ref = "" self._gpg_verification_enabled = True + @staticmethod + def is_container(): + """Is this native container source?""" + return False + @property def osname(self) -> Str: """Management root for OS installation.""" @@ -77,3 +82,70 @@ def gpg_verification_enabled(self) -> Bool: @gpg_verification_enabled.setter def gpg_verification_enabled(self, value: Bool): self._gpg_verification_enabled = value + + +class RPMOSTreeContainerConfigurationData(DBusData): + """Structure to hold RPM OSTree from container configuration.""" + + def __init__(self): + self._stateroot = "" + self._remote = "" + self._transport = "" + self._url = "" + self._signature_verification_enabled = True + + @staticmethod + def is_container(): + """Is this native container source?""" + return True + + @property + def stateroot(self) -> Str: + """Name for the state directory, also known as "osname". + + This could be optional. + """ + return self._stateroot + + @stateroot.setter + def stateroot(self, value: Str): + self._stateroot = value + + @property + def transport(self) -> Str: + """Ostree transport protocol used. + + This could be optional (default will be 'repository'). + """ + return self._transport + + @transport.setter + def transport(self, value: Str): + self._transport = value + + @property + def remote(self) -> Str: + """Name of the OSTree remote.""" + return self._remote + + @remote.setter + def remote(self, value: Str): + self._remote = value + + @property + def url(self) -> Str: + """URL of the repository to install from.""" + return self._url + + @url.setter + def url(self, value: Str): + self._url = value + + @property + def signature_verification_enabled(self) -> Bool: + """Is the GPG key verification enabled?""" + return self._signature_verification_enabled + + @signature_verification_enabled.setter + def signature_verification_enabled(self, value: Bool): + self._signature_verification_enabled = value diff --git a/pyanaconda/modules/payloads/constants.py b/pyanaconda/modules/payloads/constants.py index aa9632aa772..43d3feeb4ac 100644 --- a/pyanaconda/modules/payloads/constants.py +++ b/pyanaconda/modules/payloads/constants.py @@ -22,7 +22,8 @@ PAYLOAD_TYPE_DNF, PAYLOAD_TYPE_LIVE_OS, PAYLOAD_TYPE_LIVE_IMAGE, \ SOURCE_TYPE_LIVE_OS_IMAGE, SOURCE_TYPE_HMC, SOURCE_TYPE_CDROM, SOURCE_TYPE_REPO_FILES, \ SOURCE_TYPE_NFS, SOURCE_TYPE_URL, SOURCE_TYPE_HDD, SOURCE_TYPE_CDN, \ - SOURCE_TYPE_CLOSEST_MIRROR, PAYLOAD_TYPE_RPM_OSTREE, SOURCE_TYPE_RPM_OSTREE, \ + SOURCE_TYPE_CLOSEST_MIRROR, PAYLOAD_TYPE_RPM_OSTREE, \ + SOURCE_TYPE_RPM_OSTREE, SOURCE_TYPE_RPM_OSTREE_CONTAINER, \ SOURCE_TYPE_LIVE_IMAGE @@ -48,6 +49,7 @@ class SourceType(Enum): LIVE_OS_IMAGE = SOURCE_TYPE_LIVE_OS_IMAGE LIVE_IMAGE = SOURCE_TYPE_LIVE_IMAGE RPM_OSTREE = SOURCE_TYPE_RPM_OSTREE + RPM_OSTREE_CONTAINER = SOURCE_TYPE_RPM_OSTREE_CONTAINER HMC = SOURCE_TYPE_HMC CDROM = SOURCE_TYPE_CDROM CLOSEST_MIRROR = SOURCE_TYPE_CLOSEST_MIRROR diff --git a/pyanaconda/modules/payloads/kickstart.py b/pyanaconda/modules/payloads/kickstart.py index af16f38249e..2b3d0838f22 100644 --- a/pyanaconda/modules/payloads/kickstart.py +++ b/pyanaconda/modules/payloads/kickstart.py @@ -66,6 +66,7 @@ class PayloadKickstartSpecification(KickstartSpecification): "hmc": COMMANDS.Hmc, "liveimg": COMMANDS.Liveimg, "nfs": COMMANDS.NFS, + "ostreecontainer": COMMANDS.OSTreeContainer, "ostreesetup": COMMANDS.OSTreeSetup, "url": COMMANDS.Url } diff --git a/pyanaconda/modules/payloads/source/Makefile.am b/pyanaconda/modules/payloads/source/Makefile.am index 582c9f66ec2..839d3b0109b 100644 --- a/pyanaconda/modules/payloads/source/Makefile.am +++ b/pyanaconda/modules/payloads/source/Makefile.am @@ -14,7 +14,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -SUBDIRS = live_os hmc cdrom repo_files nfs url harddrive cdn closest_mirror rpm_ostree live_image +SUBDIRS = live_os hmc cdrom repo_files nfs url harddrive cdn closest_mirror rpm_ostree \ + rpm_ostree_container live_image pkgpyexecdir = $(pyexecdir)/py$(PACKAGE_NAME) payload_moduledir = $(pkgpyexecdir)/modules/payloads/source diff --git a/pyanaconda/modules/payloads/source/factory.py b/pyanaconda/modules/payloads/source/factory.py index cc55815d55b..2b24b3f330b 100644 --- a/pyanaconda/modules/payloads/source/factory.py +++ b/pyanaconda/modules/payloads/source/factory.py @@ -70,6 +70,10 @@ def create_source(source_type: SourceType): from pyanaconda.modules.payloads.source.rpm_ostree.rpm_ostree import \ RPMOSTreeSourceModule return RPMOSTreeSourceModule() + elif source_type == SourceType.RPM_OSTREE_CONTAINER: + from pyanaconda.modules.payloads.source.rpm_ostree_container.rpm_ostree_container import \ + RPMOSTreeContainerSourceModule + return RPMOSTreeContainerSourceModule() raise ValueError("Unknown source type: {}".format(source_type)) diff --git a/pyanaconda/modules/payloads/source/rpm_ostree/rpm_ostree.py b/pyanaconda/modules/payloads/source/rpm_ostree/rpm_ostree.py index 9bcb7eb25e7..50f25528fb5 100644 --- a/pyanaconda/modules/payloads/source/rpm_ostree/rpm_ostree.py +++ b/pyanaconda/modules/payloads/source/rpm_ostree/rpm_ostree.py @@ -110,6 +110,7 @@ def setup_kickstart(self, data): data.ostreesetup.url = self.configuration.url data.ostreesetup.ref = self.configuration.ref data.ostreesetup.nogpg = not self.configuration.gpg_verification_enabled + data.ostreesetup.seen = True def set_up_with_tasks(self): """Set up the installation source for installation. diff --git a/pyanaconda/modules/payloads/source/rpm_ostree_container/Makefile.am b/pyanaconda/modules/payloads/source/rpm_ostree_container/Makefile.am new file mode 100644 index 00000000000..e7098184d21 --- /dev/null +++ b/pyanaconda/modules/payloads/source/rpm_ostree_container/Makefile.am @@ -0,0 +1,22 @@ +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +pkgpyexecdir = $(pyexecdir)/py$(PACKAGE_NAME) +payload_moduledir = $(pkgpyexecdir)/modules/payloads/source/rpm_ostree_container +payload_module_PYTHON = $(srcdir)/*.py + +MAINTAINERCLEANFILES = Makefile.in + diff --git a/pyanaconda/modules/payloads/source/rpm_ostree_container/__init__.py b/pyanaconda/modules/payloads/source/rpm_ostree_container/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container.py b/pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container.py new file mode 100644 index 00000000000..c83001144b4 --- /dev/null +++ b/pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container.py @@ -0,0 +1,92 @@ +# +# The RPM OSTree source module. +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +from pyanaconda.anaconda_loggers import get_module_logger +from pyanaconda.core.i18n import _ +from pyanaconda.modules.common.structures.rpm_ostree import RPMOSTreeContainerConfigurationData +from pyanaconda.modules.payloads.constants import SourceType +from pyanaconda.modules.payloads.source.rpm_ostree.rpm_ostree import \ + RPMOSTreeSourceModule +from pyanaconda.modules.payloads.source.rpm_ostree_container.rpm_ostree_container_interface import \ + RPMOSTreeContainerSourceInterface + +log = get_module_logger(__name__) + +__all__ = ["RPMOSTreeContainerSourceModule"] + + +class RPMOSTreeContainerSourceModule(RPMOSTreeSourceModule): + """The RPM OSTree from container source module.""" + + def __init__(self): + super().__init__() + self._configuration = RPMOSTreeContainerConfigurationData() + + @property + def type(self): + """Get type of this source.""" + return SourceType.RPM_OSTREE_CONTAINER + + @property + def description(self): + """Get description of this source.""" + return _("RPM OSTree Container") + + def for_publication(self): + """Return a DBus representation.""" + return RPMOSTreeContainerSourceInterface(self) + + @property + def network_required(self): + """Does the source require a network? + + :return: True or False + """ + # FIXME: Missing network detection logic based on the URL + return False + + def process_kickstart(self, data): + """Process the kickstart data.""" + configuration = RPMOSTreeContainerConfigurationData() + + configuration.stateroot = data.ostreecontainer.stateroot + configuration.url = data.ostreecontainer.url + configuration.remote = data.ostreecontainer.remote + configuration.transport = data.ostreecontainer.transport + configuration.signature_verification_enabled = not data.ostreecontainer.noSignatureVerification + + self.set_configuration(configuration) + + def setup_kickstart(self, data): + """Setup the kickstart data.""" + data.ostreecontainer.stateroot = self.configuration.stateroot + data.ostreecontainer.remote = self.configuration.remote + data.ostreecontainer.transport = self.configuration.transport + data.ostreecontainer.url = self.configuration.url + data.ostreecontainer.noSignatureVerification = not self.configuration.signature_verification_enabled + data.ostreecontainer.seen = True + + def __repr__(self): + """Return a string representation of the source.""" + return "Source(type='{}', stateroot='{}', transport='{}', url='{}')".format( + self.type.value, + self.configuration.stateroot, + self.configuration.transport, + self.configuration.url + ) diff --git a/pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container_interface.py b/pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container_interface.py new file mode 100644 index 00000000000..00811599151 --- /dev/null +++ b/pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container_interface.py @@ -0,0 +1,57 @@ +# +# DBus interface for the RPM OSTree source module. +# +# Copyright (C) 2023 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +from dasbus.server.property import emits_properties_changed +from dasbus.typing import * # pylint: disable=wildcard-import +from dasbus.server.interface import dbus_interface +from pyanaconda.modules.common.constants.interfaces import PAYLOAD_SOURCE_RPM_OSTREE_CONTAINER +from pyanaconda.modules.common.structures.rpm_ostree import RPMOSTreeContainerConfigurationData +from pyanaconda.modules.payloads.source.source_base_interface import PayloadSourceBaseInterface + +__all__ = ["RPMOSTreeContainerSourceInterface"] + + +@dbus_interface(PAYLOAD_SOURCE_RPM_OSTREE_CONTAINER.interface_name) +class RPMOSTreeContainerSourceInterface(PayloadSourceBaseInterface): + """DBus interface for the RPM OSTree source module.""" + + def connect_signals(self): + """Connect the signals.""" + super().connect_signals() + self.watch_property("Configuration", self.implementation.configuration_changed) + + @property + def Configuration(self) -> Structure: + """The source configuration. + + :return: a structure of the type RPMOSTreeConfigurationData + """ + return RPMOSTreeContainerConfigurationData.to_structure( + self.implementation.configuration + ) + + @emits_properties_changed + def SetConfiguration(self, data: Structure): + """Set the source configuration. + + :param data: a structure of the type RPMOSTreeConfigurationData + """ + self.implementation.set_configuration( + RPMOSTreeContainerConfigurationData.from_structure(data) + ) diff --git a/tests/unit_tests/pyanaconda_tests/modules/payloads/source/test_source_rpm_ostree_container.py b/tests/unit_tests/pyanaconda_tests/modules/payloads/source/test_source_rpm_ostree_container.py new file mode 100644 index 00000000000..936a2473675 --- /dev/null +++ b/tests/unit_tests/pyanaconda_tests/modules/payloads/source/test_source_rpm_ostree_container.py @@ -0,0 +1,121 @@ +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +import unittest + +from pyanaconda.core.constants import SOURCE_TYPE_RPM_OSTREE_CONTAINER +from pyanaconda.modules.common.constants.interfaces import PAYLOAD_SOURCE_RPM_OSTREE_CONTAINER +from pyanaconda.modules.payloads.constants import SourceType, SourceState +from pyanaconda.modules.common.structures.rpm_ostree import RPMOSTreeContainerConfigurationData +from pyanaconda.modules.payloads.source.rpm_ostree_container.rpm_ostree_container import \ + RPMOSTreeContainerSourceModule +from pyanaconda.modules.payloads.source.rpm_ostree_container.rpm_ostree_container_interface \ + import RPMOSTreeContainerSourceInterface + +from tests.unit_tests.pyanaconda_tests import check_dbus_property + + +class OSTreeContainerSourceInterfaceTestCase(unittest.TestCase): + """Test the DBus interface of the OSTree source.""" + + def setUp(self): + self.module = RPMOSTreeContainerSourceModule() + self.interface = RPMOSTreeContainerSourceInterface(self.module) + + def _check_dbus_property(self, *args, **kwargs): + check_dbus_property( + PAYLOAD_SOURCE_RPM_OSTREE_CONTAINER, + self.interface, + *args, **kwargs + ) + + def test_type(self): + """Test the Type property.""" + assert SOURCE_TYPE_RPM_OSTREE_CONTAINER == self.interface.Type + + def test_description(self): + """Test the Description property.""" + assert "RPM OSTree Container" == self.interface.Description + + def test_configuration(self): + """Test the configuration property.""" + data = RPMOSTreeContainerConfigurationData() + data.stateroot = "fedora-coreos" + data.remote = "fcos-28" + data.transport = "registry" + data.url = "quay.io/fedora/coreos:stable" + data._signature_verification_enabled = False + + self._check_dbus_property( + "Configuration", + RPMOSTreeContainerConfigurationData.to_structure(data) + ) + + +class OSTreeContainerSourceTestCase(unittest.TestCase): + """Test the OSTree source module.""" + + def setUp(self): + self.module = RPMOSTreeContainerSourceModule() + + def test_type(self): + """Test the type property.""" + assert SourceType.RPM_OSTREE_CONTAINER == self.module.type + + def test_network_required(self): + """Test the network_required property.""" + assert self.module.network_required is False + + def test_required_space(self): + """Test the required_space property.""" + assert self.module.required_space == 0 + + def test_get_state(self): + """Test the source state.""" + assert SourceState.NOT_APPLICABLE == self.module.get_state() + + def test_set_up_with_tasks(self): + """Test the set-up tasks.""" + assert self.module.set_up_with_tasks() == [] + + def test_tear_down_with_tasks(self): + """Test the tear-down tasks.""" + assert self.module.tear_down_with_tasks() == [] + + def test_repr(self): + """Test the string representation.""" + assert repr(self.module) == str( + "Source(" + "type='RPM_OSTREE_CONTAINER', " + "stateroot='', " + "transport='', " + "url=''" + ")" + ) + + self.module.configuration.stateroot = "fcos" + self.module.configuration.transport = "registry" + self.module.configuration.url = "quay.io/fedora/coreos:stable" + + assert repr(self.module) == str( + "Source(" + "type='RPM_OSTREE_CONTAINER', " + "stateroot='fcos', " + "transport='registry', " + "url='quay.io/fedora/coreos:stable'" + ")" + ) diff --git a/tests/unit_tests/pyanaconda_tests/modules/payloads/test_module_payloads.py b/tests/unit_tests/pyanaconda_tests/modules/payloads/test_module_payloads.py index 4c97929ad7e..3d53b9b2136 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/payloads/test_module_payloads.py +++ b/tests/unit_tests/pyanaconda_tests/modules/payloads/test_module_payloads.py @@ -59,6 +59,7 @@ def test_kickstart_properties(self): "hmc", "liveimg", "nfs", + "ostreecontainer", "ostreesetup", "url" ] From efd9e4b4d97ac1b586f767b64c5cfcc438a3880a Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Thu, 11 Jan 2024 11:29:50 +0100 Subject: [PATCH 2/6] Enable RPM OSTree from container source in payload (#2125655) We have a new RPMOStreeContainer source now which enables us to use containers as installation source. In this commit we adapt the current rpm_ostree to be able to consume this new container source. Add a lot of rpm ostree tasks tests for the container source. Resolves: RHEL-2250 --- .../modules/payloads/payload/factory.py | 2 +- .../payload/rpm_ostree/installation.py | 109 +++++++++++++++--- .../payloads/payload/rpm_ostree/rpm_ostree.py | 3 +- pyanaconda/modules/payloads/source/factory.py | 2 + pyanaconda/payload/rpmostreepayload.py | 30 +++-- .../payloads/payload/test_rpm_ostree.py | 5 +- 6 files changed, 121 insertions(+), 30 deletions(-) diff --git a/pyanaconda/modules/payloads/payload/factory.py b/pyanaconda/modules/payloads/payload/factory.py index c32c9ebd311..c6097287af2 100644 --- a/pyanaconda/modules/payloads/payload/factory.py +++ b/pyanaconda/modules/payloads/payload/factory.py @@ -58,7 +58,7 @@ def get_type_for_kickstart(cls, data): :param data: a kickstart data :return: a payload type """ - if data.ostreesetup.seen: + if data.ostreesetup.seen or data.ostreecontainer.seen: return PayloadType.RPM_OSTREE if data.liveimg.seen: diff --git a/pyanaconda/modules/payloads/payload/rpm_ostree/installation.py b/pyanaconda/modules/payloads/payload/rpm_ostree/installation.py index 63959a5c631..d995a4d8353 100644 --- a/pyanaconda/modules/payloads/payload/rpm_ostree/installation.py +++ b/pyanaconda/modules/payloads/payload/rpm_ostree/installation.py @@ -51,6 +51,60 @@ def safe_exec_with_redirect(cmd, argv, successful_return_codes=(0,), **kwargs): ) +def _get_ref(data): + """Get ref or name based on source. + + OSTree container don't have ref because it's specified by the container. In that case let's + return just url for reporting. + + :param data: OSTree source structure + :return str: ref or name based on source + """ + # Variable substitute the ref: https://pagure.io/atomic-wg/issue/299 + if data.is_container(): + # we don't have ref with container; there are not multiple references in one container + return data.url + else: + return RpmOstree.varsubst_basearch(data.ref) + + +def _get_stateroot(data): + """Get stateroot. + + The OSTree renamed old osname to stateroot for containers. + + :param data: OSTree source structure + :return str: stateroot or osname value based on source + """ + if data.is_container(): + # osname was renamed to stateroot so let's use the new name + if data.stateroot: + return data.stateroot + else: + # The stateroot doesn't have to be defined + # https://github.com/ostreedev/ostree-rs-ext/pull/462/files + # However, it's working just for a subset of calls now. + # TODO: Remove this when all ostree commands undestarstands this + return "default" + else: + return data.osname + + +def _get_verification_enabled(data): + """Find out if source has enabled verification. + + OSTree sources has different names for enabled verification. This helper function + will make the access consistent. + + :param data: OSTree source structure + :return bool: True if verification is enabled + """ + if data.is_container(): + return data.signature_verification_enabled + else: + return data.gpg_verification_enabled + + class PrepareOSTreeMountTargetsTask(Task): """Task to prepare OSTree mount targets.""" @@ -119,7 +173,10 @@ def _handle_var_mount_point(self, existing_mount_points): :param [] existing_mount_points: a list of existing mount points """ - var_root = '/ostree/deploy/' + self._source_config.osname + '/var' + # osname was used for ostreesetup but ostreecontainer renamed it to stateroot + stateroot = _get_stateroot(self._source_config) + + var_root = '/ostree/deploy/' + stateroot + '/var' if existing_mount_points.get("/var") is None: self._setup_internal_bindmount(var_root, dest='/var', recurse=False) else: @@ -325,7 +382,7 @@ def run(self): remote_options = {} - if not self._data.gpg_verification_enabled: + if not _get_verification_enabled(self._data): remote_options['gpg-verify'] = Variant('b', False) if not conf.payload.verify_ssl: @@ -336,9 +393,12 @@ def run(self): else: root = None + # Remote is set or it should be named as stateroot is + remote = self._data.remote or _get_stateroot(self._data) + repo.remote_change(root, OSTree.RepoRemoteChange.ADD_IF_NOT_EXISTS, - self._data.remote, + remote, self._data.url, Variant('a{sv}', remote_options), cancellable) @@ -413,7 +473,8 @@ def name(self): def run(self): # Variable substitute the ref: https://pagure.io/atomic-wg/issue/299 - ref = RpmOstree.varsubst_basearch(self._data.ref) + ref = _get_ref(self._data) + stateroot = _get_stateroot(self._data) self.report_progress(_("Deployment starting: {}").format(ref)) @@ -422,21 +483,39 @@ def run(self): ["admin", "--sysroot=" + self._sysroot, "os-init", - self._data.osname] + stateroot] ) - log.info("ostree admin deploy starting") + if self._data.is_container(): + log.info("ostree image deploy starting") - safe_exec_with_redirect( - "ostree", - ["admin", - "--sysroot=" + self._sysroot, - "deploy", - "--os=" + self._data.osname, - self._data.remote + ':' + ref] - ) + args = ["container", "image", "deploy", + "--sysroot=" + self._sysroot, + "--image=" + ref] + + if self._data.transport: + args.append("--transport=" + self._data.transport) + if self._data.stateroot: + args.append("--stateroot=" + self._data.stateroot) + if not self._data.signature_verification_enabled: + args.append("--no-signature-verification") + + safe_exec_with_redirect( + "ostree", + args + ) + else: + log.info("ostree admin deploy starting") + safe_exec_with_redirect( + "ostree", + ["admin", + "--sysroot=" + self._sysroot, + "deploy", + "--os=" + stateroot, + self._data.remote + ':' + ref] + ) - log.info("ostree admin deploy complete") + log.info("ostree deploy complete") self.report_progress(_("Deployment complete: {}").format(ref)) diff --git a/pyanaconda/modules/payloads/payload/rpm_ostree/rpm_ostree.py b/pyanaconda/modules/payloads/payload/rpm_ostree/rpm_ostree.py index 5d4758ccf82..024753d4215 100644 --- a/pyanaconda/modules/payloads/payload/rpm_ostree/rpm_ostree.py +++ b/pyanaconda/modules/payloads/payload/rpm_ostree/rpm_ostree.py @@ -45,7 +45,8 @@ def type(self): def supported_source_types(self): """Get list of sources supported by the RPM OSTree module.""" return [ - SourceType.RPM_OSTREE + SourceType.RPM_OSTREE, + SourceType.RPM_OSTREE_CONTAINER, ] def process_kickstart(self, data): diff --git a/pyanaconda/modules/payloads/source/factory.py b/pyanaconda/modules/payloads/source/factory.py index 2b24b3f330b..898f75eb55c 100644 --- a/pyanaconda/modules/payloads/source/factory.py +++ b/pyanaconda/modules/payloads/source/factory.py @@ -108,6 +108,8 @@ def get_rpm_ostree_type_for_kickstart(ks_data): :param ks_data: kickstart data from DNF payload :return: SourceType value """ + if ks_data.ostreecontainer.seen: + return SourceType.RPM_OSTREE_CONTAINER if ks_data.ostreesetup.seen: return SourceType.RPM_OSTREE diff --git a/pyanaconda/payload/rpmostreepayload.py b/pyanaconda/payload/rpmostreepayload.py index f2fe7ee1537..b929396f3ed 100644 --- a/pyanaconda/payload/rpmostreepayload.py +++ b/pyanaconda/payload/rpmostreepayload.py @@ -20,8 +20,10 @@ from subprocess import CalledProcessError -from pyanaconda.core.constants import PAYLOAD_TYPE_RPM_OSTREE, SOURCE_TYPE_RPM_OSTREE -from pyanaconda.modules.common.structures.rpm_ostree import RPMOSTreeConfigurationData +from pyanaconda.core.constants import PAYLOAD_TYPE_RPM_OSTREE, SOURCE_TYPE_RPM_OSTREE, \ + SOURCE_TYPE_RPM_OSTREE_CONTAINER +from pyanaconda.modules.common.structures.rpm_ostree import RPMOSTreeConfigurationData, \ + RPMOSTreeContainerConfigurationData from pyanaconda.progress import progressQ from pyanaconda.payload.base import Payload from pyanaconda.payload import utils as payload_utils @@ -62,13 +64,18 @@ def source_type(self): def _get_source_configuration(self): """Get the configuration of the RPM OSTree source. - :return: an instance of RPMOSTreeConfigurationData + :return: an instance of RPMOSTreeConfigurationData or RPMOSTreeContainerConfigurationData """ source_proxy = self.get_source_proxy() - return RPMOSTreeConfigurationData.from_structure( - source_proxy.Configuration - ) + if self.source_type == SOURCE_TYPE_RPM_OSTREE_CONTAINER: + return RPMOSTreeContainerConfigurationData.from_structure( + source_proxy.Configuration + ) + else: + return RPMOSTreeConfigurationData.from_structure( + source_proxy.Configuration + ) @property def kernel_version_list(self): @@ -124,11 +131,12 @@ def _install(self, data): ) task.run() - from pyanaconda.modules.payloads.payload.rpm_ostree.installation import \ - PullRemoteAndDeleteTask - task = PullRemoteAndDeleteTask(data) - task.progress_changed_signal.connect(self._progress_cb) - task.run() + if not data.is_container(): + from pyanaconda.modules.payloads.payload.rpm_ostree.installation import \ + PullRemoteAndDeleteTask + task = PullRemoteAndDeleteTask(data) + task.progress_changed_signal.connect(self._progress_cb) + task.run() from pyanaconda.modules.payloads.payload.rpm_ostree.installation import DeployOSTreeTask task = DeployOSTreeTask(data, conf.target.physical_root) diff --git a/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_rpm_ostree.py b/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_rpm_ostree.py index 7c4704187d2..28a759e847e 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_rpm_ostree.py +++ b/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_rpm_ostree.py @@ -17,7 +17,7 @@ # import unittest -from pyanaconda.core.constants import SOURCE_TYPE_RPM_OSTREE +from pyanaconda.core.constants import SOURCE_TYPE_RPM_OSTREE, SOURCE_TYPE_RPM_OSTREE_CONTAINER from pyanaconda.modules.payloads.constants import PayloadType from pyanaconda.modules.payloads.payload.rpm_ostree.rpm_ostree import RPMOSTreeModule from pyanaconda.modules.payloads.payload.rpm_ostree.rpm_ostree_interface import RPMOSTreeInterface @@ -47,7 +47,8 @@ def test_type(self): def test_supported_sources(self): """Test the SupportedSourceTypes property.""" assert self.interface.SupportedSourceTypes == [ - SOURCE_TYPE_RPM_OSTREE + SOURCE_TYPE_RPM_OSTREE, + SOURCE_TYPE_RPM_OSTREE_CONTAINER, ] From a84b07843acb14d263ed5b301df6210b4c9bf503 Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Fri, 12 Jan 2024 16:27:44 +0100 Subject: [PATCH 3/6] Add tests for RPM Ostree payload with source from container (#2125655) Related: RHEL-2250 --- .../payloads/payload/test_rpm_ostree.py | 23 ++ .../payloads/payload/test_rpm_ostree_tasks.py | 354 +++++++++++++++--- 2 files changed, 331 insertions(+), 46 deletions(-) diff --git a/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_rpm_ostree.py b/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_rpm_ostree.py index 28a759e847e..c25e926b18a 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_rpm_ostree.py +++ b/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_rpm_ostree.py @@ -86,6 +86,17 @@ def test_ostree_kickstart(self): self.shared_ks_tests.check_kickstart(ks_in, ks_out) self._check_properties(SOURCE_TYPE_RPM_OSTREE) + def test_ostree_container_kickstart(self): + ks_in = """ + ostreecontainer --stateroot="fedora-coreos" --transport="repository" --remote="fedora" --url="quay.io/fedora/coreos:stable" --no-signature-verification + """ + ks_out = """ + # OSTree container setup + ostreecontainer --stateroot="fedora-coreos" --remote="fedora" --no-signature-verification --transport="repository" --url="quay.io/fedora/coreos:stable" + """ + self.shared_ks_tests.check_kickstart(ks_in, ks_out) + self._check_properties(SOURCE_TYPE_RPM_OSTREE_CONTAINER) + def test_priority_kickstart(self): ks_in = """ ostreesetup --osname="fedora-iot" --url="https://compose/iot/" --ref="fedora/iot" @@ -97,3 +108,15 @@ def test_priority_kickstart(self): """ self.shared_ks_tests.check_kickstart(ks_in, ks_out) self._check_properties(SOURCE_TYPE_RPM_OSTREE) + + def test_ostreecontainer_priority_kickstart(self): + ks_in = """ + url --url="https://compose/Everything" + ostreecontainer --stateroot="fedora-coreos" --remote="fedora" --url="quay.io/fedora/coreos:stable" + """ + ks_out = """ + # OSTree container setup + ostreecontainer --stateroot="fedora-coreos" --remote="fedora" --url="quay.io/fedora/coreos:stable" + """ + self.shared_ks_tests.check_kickstart(ks_in, ks_out) + self._check_properties(SOURCE_TYPE_RPM_OSTREE_CONTAINER) diff --git a/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_rpm_ostree_tasks.py b/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_rpm_ostree_tasks.py index 9b8ef10068b..862f6c1f640 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_rpm_ostree_tasks.py +++ b/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_rpm_ostree_tasks.py @@ -23,7 +23,8 @@ from unittest.mock import patch, call, MagicMock from pyanaconda.core.glib import Variant, GError -from pyanaconda.modules.common.structures.rpm_ostree import RPMOSTreeConfigurationData +from pyanaconda.modules.common.structures.rpm_ostree import RPMOSTreeConfigurationData, \ + RPMOSTreeContainerConfigurationData from pyanaconda.payload.errors import PayloadInstallError from pyanaconda.modules.payloads.payload.rpm_ostree.installation import \ @@ -46,17 +47,43 @@ def _make_config_data(): return data +def _make_container_config_data(): + """Create OSTree container configuration data for testing + + :return RPMOSTreeContainerConfigurationData: a data instance with all fields filled + """ + data = RPMOSTreeContainerConfigurationData() + data.url = "url" + data.stateroot = "osname" + data.signature_verification_enabled = True + data.transport = "oci" + data.remote = "remote" + return data + + class PrepareOSTreeMountTargetsTaskTestCase(unittest.TestCase): @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.execWithRedirect") def test_setup_internal_bindmount(self, exec_mock): - """Test OSTree mount target prepare task _setup_internal_bindmount()""" + """Test OSTree mount target prepare task _setup_internal_bindmount""" exec_mock.return_value = 0 data = _make_config_data() task = PrepareOSTreeMountTargetsTask("/sysroot", "/physroot", data) assert len(task._internal_mounts) == 0 + self._check_setup_internal_bindmount(task, exec_mock) + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.execWithRedirect") + def test_container_setup_internal_bindmount(self, exec_mock): + """Test OSTree mount target prepare task _setup_internal_bindmount with ostreecontainer""" + exec_mock.return_value = 0 + data = _make_container_config_data() + task = PrepareOSTreeMountTargetsTask("/sysroot", "/physroot", data) + assert len(task._internal_mounts) == 0 + self._check_setup_internal_bindmount(task, exec_mock) + + def _check_setup_internal_bindmount(self, task, exec_mock): # everything left out task._setup_internal_bindmount("/src") exec_mock.assert_called_once_with("mount", ["--rbind", "/physroot/src", "/sysroot/src"]) @@ -104,6 +131,19 @@ def test_run_with_var(self, storage_mock, mkdir_mock, exec_mock): exec_mock.return_value = 0 data = _make_config_data() + self._check_run_with_var(data, storage_mock, mkdir_mock, exec_mock) + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.execWithRedirect") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.mkdirChain") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.STORAGE") + def test_container_run_with_var(self, storage_mock, mkdir_mock, exec_mock): + """Test OSTree mount target prepare task run() with /var""" + exec_mock.return_value = 0 + + data = _make_container_config_data() + self._check_run_with_var(data, storage_mock, mkdir_mock, exec_mock) + + def _check_run_with_var(self, data, storage_mock, mkdir_mock, exec_mock): devicetree_mock = storage_mock.get_proxy() devicetree_mock.GetMountPoints.return_value = { @@ -159,6 +199,19 @@ def test_run_without_var(self, storage_mock, mkdir_mock, exec_mock): exec_mock.side_effect = [0] * 7 + [0, 65] * 5 + [0] * 3 data = _make_config_data() + self._check_run_without_var(data, storage_mock, mkdir_mock, exec_mock) + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.execWithRedirect") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.mkdirChain") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.STORAGE") + def test_container_run_without_var(self, storage_mock, mkdir_mock, exec_mock): + """Test OSTree mount target prepare task run() without /var""" + exec_mock.side_effect = [0] * 7 + [0, 65] * 5 + [0] * 3 + + data = _make_container_config_data() + self._check_run_without_var(data, storage_mock, mkdir_mock, exec_mock) + + def _check_run_without_var(self, data, storage_mock, mkdir_mock, exec_mock): devicetree_mock = storage_mock.get_proxy() devicetree_mock.GetMountPoints.return_value = { @@ -213,6 +266,19 @@ def test_run_failed(self, storage_mock, mkdir_mock, exec_mock): exec_mock.return_value = 1 data = _make_config_data() + self._check_run_failed(data, storage_mock, mkdir_mock, exec_mock) + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.execWithRedirect") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.mkdirChain") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.STORAGE") + def test_container_run_failed(self, storage_mock, mkdir_mock, exec_mock): + """Test the failed OSTree mount target prepare task.""" + exec_mock.return_value = 1 + + data = _make_container_config_data() + self._check_run_failed(data, storage_mock, mkdir_mock, exec_mock) + + def _check_run_failed(self, data, storage_mock, mkdir_mock, exec_mock): devicetree_mock = storage_mock.get_proxy() devicetree_mock.GetMountPoints.return_value = { @@ -351,57 +417,178 @@ def test_run(self, exec_mock): class ChangeOSTreeRemoteTaskTestCase(unittest.TestCase): + + def _get_repo(self, sysroot_cls): + """Create up the OSTree repo mock.""" + repo_mock = MagicMock() + sysroot_mock = sysroot_cls.new() + sysroot_mock.get_repo.return_value = [None, repo_mock] + return repo_mock + + def _check_remote_changed(self, repo, remote="remote", sysroot_file=None, options=None): + """Check the remote_changed method.""" + repo.remote_change.assert_called_once() + args, kwargs = repo.remote_change.call_args + + print(args) + assert len(args) == 6 + assert len(kwargs) == 0 + + assert args[0] == sysroot_file + assert args[2] == remote + assert args[3] == "url" + assert args[4].unpack() == (options or {}) + assert args[5] is None + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.Gio.File") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.OSTree.Sysroot") + def test_install(self, sysroot_cls, gio_file_cls): + """Test the ChangeOSTreeRemoteTask installation task.""" + data = _make_config_data() + repo = self._get_repo(sysroot_cls) + + task = ChangeOSTreeRemoteTask(data, False, "/physroot") + task.run() + + self._check_remote_changed(repo, sysroot_file=None) + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.Gio.File") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.OSTree.Sysroot") + def test_container_install(self, sysroot_cls, gio_file_cls): + """Test the ChangeOSTreeRemoteTask installation task with ostreecontainer.""" + data = _make_container_config_data() + repo = self._get_repo(sysroot_cls) + + task = ChangeOSTreeRemoteTask(data, False, "/physroot") + task.run() + + self._check_remote_changed(repo, sysroot_file=None) + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.Gio.File") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.OSTree.Sysroot") + def test_container_install_no_remote(self, sysroot_cls, gio_file_cls): + """Test the ChangeOSTreeRemoteTask task with ostreecontainer no remote.""" + data = _make_container_config_data() + data.remote = None + repo = self._get_repo(sysroot_cls) + + task = ChangeOSTreeRemoteTask(data, False, "/physroot") + task.run() + + # remote is taken from the stateroot value + self._check_remote_changed(repo, remote="osname", sysroot_file=None) + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.Gio.File") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.OSTree.Sysroot") + def test_container_install_no_remote_and_stateroot(self, sysroot_cls, gio_file_cls): + """Test the ChangeOSTreeRemoteTask task with ostreecontainer no remote + stateroot.""" + data = _make_container_config_data() + data.remote = None + data.stateroot = None + repo = self._get_repo(sysroot_cls) + + task = ChangeOSTreeRemoteTask(data, False, "/physroot") + task.run() + + # remote is taken from the stateroot value which when empty will be "default" + self._check_remote_changed(repo, remote="default", sysroot_file=None) + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.Gio.File") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.OSTree.Sysroot") + def test_post_install(self, sysroot_cls, gio_file_cls): + """Test the ChangeOSTreeRemoteTask post-installation task.""" + data = _make_config_data() + repo = self._get_repo(sysroot_cls) + sysroot_file = gio_file_cls.new_for_path("/sysroot") + + task = ChangeOSTreeRemoteTask(data, True, "/sysroot") + task.run() + + self._check_remote_changed(repo, sysroot_file=sysroot_file) + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.Gio.File") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.OSTree.Sysroot") + def test_container_post_install(self, sysroot_cls, gio_file_cls): + """Test the ChangeOSTreeRemoteTask post-installation task with ostreecontainer.""" + data = _make_container_config_data() + repo = self._get_repo(sysroot_cls) + sysroot_file = gio_file_cls.new_for_path("/sysroot") + + task = ChangeOSTreeRemoteTask(data, True, "/sysroot") + task.run() + + self._check_remote_changed(repo, sysroot_file=sysroot_file) + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.Gio.File") @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.OSTree.Sysroot") + def test_container_post_install_no_remote(self, sysroot_cls, gio_file_cls): + """Test the ChangeOSTreeRemoteTask post task with ostreecontainer no remote.""" + data = _make_container_config_data() + data.remote = None + repo = self._get_repo(sysroot_cls) + sysroot_file = gio_file_cls.new_for_path("/sysroot") + + task = ChangeOSTreeRemoteTask(data, True, "/sysroot") + task.run() + + # remote is taken from the stateroot value + self._check_remote_changed(repo, remote="osname", sysroot_file=sysroot_file) + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.Gio.File") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.OSTree.Sysroot") + def test_container_post_install_no_remote_and_stateroot(self, sysroot_cls, gio_file_cls): + """Test the ChangeOSTreeRemoteTask post task with ostreecontainer no remote + stateroot.""" + data = _make_container_config_data() + data.remote = None + data.stateroot = None + repo = self._get_repo(sysroot_cls) + sysroot_file = gio_file_cls.new_for_path("/sysroot") + + task = ChangeOSTreeRemoteTask(data, True, "/sysroot") + task.run() + + # remote is taken from the stateroot value which when empty will be "default" + self._check_remote_changed(repo, remote="default", sysroot_file=sysroot_file) + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.conf") @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.Gio.File") - def _execute_run_once(self, use_sysroot, gpg_verify, verify_ssl, - gio_file_mock, conf_mock, sysroot_mock): - new_mock = sysroot_mock.new() - repo_mock = MagicMock() - new_mock.get_repo.return_value = [None, repo_mock] - conf_mock.payload.verify_ssl = verify_ssl - path_mock = gio_file_mock.new_for_path() - - data = RPMOSTreeConfigurationData() - data.url = "url" - data.osname = "osname" - data.gpg_verification_enabled = gpg_verify - data.ref = "ref" - data.remote = "remote" - - task = ChangeOSTreeRemoteTask(data, use_sysroot, "/physroot") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.OSTree.Sysroot") + def test_options(self, sysroot_cls, gio_file_cls, conf_mock): + """Test the remote options of the ChangeOSTreeRemoteTask task.""" + options = { + "gpg-verify": False, + "tls-permissive": True, + } + + data = _make_config_data() + repo = self._get_repo(sysroot_cls) + conf_mock.payload.verify_ssl = False + data.gpg_verification_enabled = False + + task = ChangeOSTreeRemoteTask(data, False, "/physroot") task.run() - repo_mock.remote_change.assert_called_once() - the_call = repo_mock.remote_change.mock_calls[0] - name, args, kwargs = the_call - print(the_call, name, args, kwargs) - assert len(args) == 6 + self._check_remote_changed(repo, sysroot_file=None, options=options) - if use_sysroot: - assert args[0] == path_mock - else: - assert args[0] is None - assert args[2] == "remote" - assert args[3] == "url" + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.conf") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.Gio.File") + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.OSTree.Sysroot") + def test_container_options(self, sysroot_cls, gio_file_cls, conf_mock): + """Test the remote options of the ChangeOSTreeRemoteTask task with ostreecontainer.""" + options = { + "gpg-verify": False, + "tls-permissive": True, + } + + data = _make_container_config_data() + repo = self._get_repo(sysroot_cls) + conf_mock.payload.verify_ssl = False + data.signature_verification_enabled = False + + task = ChangeOSTreeRemoteTask(data, False, "/physroot") + task.run() - expected = {} - if not gpg_verify: - expected["gpg-verify"] = False - if not verify_ssl: - expected["tls-permissive"] = True - var = args[4] - assert type(var) == Variant - assert var.unpack() == expected - - def test_run(self): - """Test OSTree remote change task""" - # pylint: disable=no-value-for-parameter - # check all combinations of all inputs - for use_sysroot in (True, False): - for verify_ssl in (True, False): - for gpg_verify in (True, False): - self._execute_run_once(use_sysroot, gpg_verify, verify_ssl) + self._check_remote_changed(repo, sysroot_file=None, options=options) class ConfigureBootloaderTaskTestCase(unittest.TestCase): @@ -527,6 +714,81 @@ def test_run(self, exec_mock): ]) # no need to mock RpmOstree.varsubst_basearch(), since "ref" won't change + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.execWithRedirect") + def test_container_run(self, exec_mock): + """Test OSTree deploy task""" + exec_mock.return_value = 0 + data = _make_container_config_data() + + task = DeployOSTreeTask(data, "/sysroot") + task.run() + + exec_mock.assert_has_calls([ + call("ostree", ["admin", "--sysroot=/sysroot", "os-init", "osname"]), + call("ostree", ["container", "image", "deploy", + "--sysroot=/sysroot", + "--image=url", + "--transport=oci", + "--stateroot=osname"]), + ]) + # no need to mock RpmOstree.varsubst_basearch(), since "ref" won't change + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.execWithRedirect") + def test_container_run_with_no_stateroot(self, exec_mock): + """Test OSTree deploy task ostreecontainer without stateroot.""" + exec_mock.return_value = 0 + data = _make_container_config_data() + data.stateroot = None + + task = DeployOSTreeTask(data, "/sysroot") + task.run() + + exec_mock.assert_has_calls([ + call("ostree", ["admin", "--sysroot=/sysroot", "os-init", "default"]), + call("ostree", ["container", "image", "deploy", + "--sysroot=/sysroot", + "--image=url", + "--transport=oci"]), + ]) + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.execWithRedirect") + def test_container_run_with_no_transport(self, exec_mock): + """Test OSTree deploy task ostreecontainer without transport.""" + exec_mock.return_value = 0 + data = _make_container_config_data() + data.transport = None + + task = DeployOSTreeTask(data, "/sysroot") + task.run() + + exec_mock.assert_has_calls([ + call("ostree", ["admin", "--sysroot=/sysroot", "os-init", "osname"]), + call("ostree", ["container", "image", "deploy", + "--sysroot=/sysroot", + "--image=url", + "--stateroot=osname"]), + ]) + + @patch("pyanaconda.modules.payloads.payload.rpm_ostree.installation.execWithRedirect") + def test_container_run_with_no_verification(self, exec_mock): + """Test OSTree deploy task ostreecontainer without signature verification.""" + exec_mock.return_value = 0 + data = _make_container_config_data() + data.signature_verification_enabled = False + + task = DeployOSTreeTask(data, "/sysroot") + task.run() + + exec_mock.assert_has_calls([ + call("ostree", ["admin", "--sysroot=/sysroot", "os-init", "osname"]), + call("ostree", ["container", "image", "deploy", + "--sysroot=/sysroot", + "--image=url", + "--transport=oci", + "--stateroot=osname", + "--no-signature-verification"]), + ]) + class PullRemoteAndDeleteTaskTestCase(unittest.TestCase): # pylint: disable=unused-variable From d6d512a684465f963d7c932fb6543561c61abdad Mon Sep 17 00:00:00 2001 From: Jiri Konecny Date: Fri, 12 Jan 2024 16:45:07 +0100 Subject: [PATCH 4/6] Implement needs_network for rpm_ostree_container (#2125655) Based on my discussion and recommendation on the bug, the 'registry' transport should be the only one which needs network to work. Other ways of transport should be used for local management. Related: RHEL-2250 --- .../rpm_ostree_container/rpm_ostree_container.py | 6 +++++- .../source/test_source_rpm_ostree_container.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container.py b/pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container.py index c83001144b4..6f057371d6c 100644 --- a/pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container.py +++ b/pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container.py @@ -58,7 +58,11 @@ def network_required(self): :return: True or False """ - # FIXME: Missing network detection logic based on the URL + # the 'registry' transport value will most probably require network settings + # other transport ways shouldn't require that + if self._configuration.transport == "registry": + return True + return False def process_kickstart(self, data): diff --git a/tests/unit_tests/pyanaconda_tests/modules/payloads/source/test_source_rpm_ostree_container.py b/tests/unit_tests/pyanaconda_tests/modules/payloads/source/test_source_rpm_ostree_container.py index 936a2473675..e07d50d2bcf 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/payloads/source/test_source_rpm_ostree_container.py +++ b/tests/unit_tests/pyanaconda_tests/modules/payloads/source/test_source_rpm_ostree_container.py @@ -80,6 +80,20 @@ def test_network_required(self): """Test the network_required property.""" assert self.module.network_required is False + data = RPMOSTreeContainerConfigurationData() + + data.transport = "oci" + self.module.set_configuration(data) + assert self.module.network_required is False + + data.transport = "oci-archive" + self.module.set_configuration(data) + assert self.module.network_required is False + + data.transport = "registry" + self.module.set_configuration(data) + assert self.module.network_required is True + def test_required_space(self): """Test the required_space property.""" assert self.module.required_space == 0 From 2b2b9a77e1a22794f12590295f6e9081ec744053 Mon Sep 17 00:00:00 2001 From: Jiri Konecny Date: Fri, 12 Jan 2024 16:53:41 +0100 Subject: [PATCH 5/6] Move rpm-ostree deps from Lorax to Anaconda (#2125655) Our intention is to move dependencies specific for Anaconda execution from Lorax into Anaconda. Let's do that for rpm-ostree because we need to also add `skopeo` project to be able to download container images. Require rpm-ostree version which contains: - https://github.com/ostreedev/ostree-rs-ext/pull/464 (simplified syntax) - https://github.com/ostreedev/ostree-rs-ext/pull/462 (stateroot is not mandatory) Related: RHEL-2250 --- anaconda.spec.in | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/anaconda.spec.in b/anaconda.spec.in index fdd0782c927..d0f83b478d7 100644 --- a/anaconda.spec.in +++ b/anaconda.spec.in @@ -42,6 +42,7 @@ Source0: %{name}-%{version}.tar.bz2 %define simplelinever 1.8.3-1 %define subscriptionmanagerver 1.29.31 %define utillinuxver 2.15.1 +%define rpmostreever 2023.2 BuildRequires: audit-libs-devel BuildRequires: libtool @@ -100,6 +101,11 @@ Requires: python3-systemd Requires: python3-productmd Requires: python3-dasbus >= %{dasbusver} Requires: flatpak-libs +# dependencies for rpm-ostree payload module +Requires: rpm-ostree >= %{rpmostreever} +Requires: ostree +# used by ostree command for native containers +Requires: skopeo %if %{defined rhel} && %{undefined centos} Requires: subscription-manager >= %{subscriptionmanagerver} %endif From d20aa8f3fde9c410017baf0592f2e8bf6b07c4d5 Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Wed, 17 Jan 2024 08:56:42 +0100 Subject: [PATCH 6/6] Do not crash on default None values of ostreecontainer command Related: RHEL-2250 --- .../source/rpm_ostree_container/rpm_ostree_container.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container.py b/pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container.py index 6f057371d6c..d4c29171ff9 100644 --- a/pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container.py +++ b/pyanaconda/modules/payloads/source/rpm_ostree_container/rpm_ostree_container.py @@ -69,10 +69,10 @@ def process_kickstart(self, data): """Process the kickstart data.""" configuration = RPMOSTreeContainerConfigurationData() - configuration.stateroot = data.ostreecontainer.stateroot - configuration.url = data.ostreecontainer.url - configuration.remote = data.ostreecontainer.remote - configuration.transport = data.ostreecontainer.transport + configuration.stateroot = data.ostreecontainer.stateroot or "" + configuration.url = data.ostreecontainer.url or "" + configuration.remote = data.ostreecontainer.remote or "" + configuration.transport = data.ostreecontainer.transport or "" configuration.signature_verification_enabled = not data.ostreecontainer.noSignatureVerification self.set_configuration(configuration)