diff --git a/anaconda.spec.in b/anaconda.spec.in
index e99be22fa12..ea4ad18f6f4 100644
--- a/anaconda.spec.in
+++ b/anaconda.spec.in
@@ -42,7 +42,7 @@ Source0: https://github.com/rhinstaller/%{name}/releases/download/%{name}-%{vers
%define pythonblivetver 1:3.9.0-1
%define rpmver 4.15.0
%define simplelinever 1.9.0-1
-%define subscriptionmanagerver 1.26
+%define subscriptionmanagerver 1.29.31
%define utillinuxver 2.15.1
%define rpmostreever 2023.2
%define s390utilscorever 2.31.0
diff --git a/pyanaconda/errors.py b/pyanaconda/errors.py
index ded2804ab35..7afa805d50c 100644
--- a/pyanaconda/errors.py
+++ b/pyanaconda/errors.py
@@ -23,6 +23,7 @@
InsightsClientMissingError, InsightsConnectError
from pyanaconda.modules.common.errors.payload import SourceSetupError
from pyanaconda.modules.common.errors.storage import UnusableStorageError
+from pyanaconda.modules.common.errors.subscription import SatelliteProvisioningError
class ScriptError(Exception):
@@ -110,6 +111,10 @@ def _get_default_mapping(self):
InsightsClientMissingError.__name__: self._insightsErrorHandler,
InsightsConnectError.__name__: self._insightsErrorHandler,
"KickstartRegistrationError": self._kickstartRegistrationErrorHandler,
+ "SubscriptionTokenTransferError": self._subscriptionTokenTransferErrorHandler,
+
+ # Satellite
+ SatelliteProvisioningError.__name__: self._target_satellite_provisioning_error_handler,
# General installation errors.
NonCriticalInstallationError.__name__: self._non_critical_error_handler,
@@ -175,6 +180,13 @@ def _bootloader_error_handler(self, exn):
else:
return ERROR_RAISE
+ def _target_satellite_provisioning_error_handler(self, exn):
+ message = _("Failed to provision the target system for Satellite.")
+ details = str(exn)
+
+ self.ui.showDetailedError(message, details)
+ return ERROR_RAISE
+
def _non_critical_error_handler(self, exn):
message = _("The following error occurred during the installation:"
"\n\n{details}\n\nWould you like to ignore this and "
@@ -210,6 +222,21 @@ def _kickstartRegistrationErrorHandler(self, exn):
else:
return ERROR_RAISE
+ def _subscriptionTokenTransferErrorHandler(self, exn):
+ message = _("Failed to enable Red Hat subscription on the "
+ "installed system."
+ "\n\n"
+ "Your Red Hat subscription might be invalid "
+ "(such as due to an expired developer subscription)."
+ "\n\n"
+ "Would you like to ignore this and continue with "
+ "installation?")
+
+ if self.ui.showYesNoQuestion(message):
+ return ERROR_CONTINUE
+ else:
+ return ERROR_RAISE
+
def cb(self, exn):
"""This method is the callback that all error handling should pass
through. The return value is one of the ERROR_* constants defined
diff --git a/pyanaconda/modules/common/constants/objects.py b/pyanaconda/modules/common/constants/objects.py
index 2deb5890dcb..18e26a23344 100644
--- a/pyanaconda/modules/common/constants/objects.py
+++ b/pyanaconda/modules/common/constants/objects.py
@@ -140,16 +140,6 @@
basename="Unregister"
)
-RHSM_ATTACH = DBusObjectIdentifier(
- namespace=RHSM_NAMESPACE,
- basename="Attach"
-)
-
-RHSM_ENTITLEMENT = DBusObjectIdentifier(
- namespace=RHSM_NAMESPACE,
- basename="Entitlement"
-)
-
RHSM_SYSPURPOSE = DBusObjectIdentifier(
namespace=RHSM_NAMESPACE,
basename="Syspurpose"
diff --git a/pyanaconda/modules/common/errors/installation.py b/pyanaconda/modules/common/errors/installation.py
index 08832f7bc4e..197691b0bad 100644
--- a/pyanaconda/modules/common/errors/installation.py
+++ b/pyanaconda/modules/common/errors/installation.py
@@ -103,3 +103,9 @@ class InsightsConnectError(InstallationError):
class SubscriptionTokenTransferError(InstallationError):
"""Exception for errors during subscription token transfer."""
pass
+
+
+@dbus_error("TargetSatelliteProvisioningError", namespace=ANACONDA_NAMESPACE)
+class TargetSatelliteProvisioningError(InstallationError):
+ """Exception for errors when provisioning target system for Satellite."""
+ pass
diff --git a/pyanaconda/modules/common/errors/subscription.py b/pyanaconda/modules/common/errors/subscription.py
index a1f9cce43a1..6612e16f2de 100644
--- a/pyanaconda/modules/common/errors/subscription.py
+++ b/pyanaconda/modules/common/errors/subscription.py
@@ -33,7 +33,13 @@ class UnregistrationError(AnacondaError):
pass
-@dbus_error("SubscriptionError", namespace=ANACONDA_NAMESPACE)
-class SubscriptionError(AnacondaError):
- """Subscription attempt failed."""
+@dbus_error("SatelliteProvisioningError", namespace=ANACONDA_NAMESPACE)
+class SatelliteProvisioningError(AnacondaError):
+ """Failed to provision the installation environment for Satellite."""
+ pass
+
+
+@dbus_error("MultipleOrganizationsError", namespace=ANACONDA_NAMESPACE)
+class MultipleOrganizationsError(AnacondaError):
+ """Account is member of more than one organization."""
pass
diff --git a/pyanaconda/modules/common/structures/subscription.py b/pyanaconda/modules/common/structures/subscription.py
index 862c4d6ba05..fd31edc2fa2 100644
--- a/pyanaconda/modules/common/structures/subscription.py
+++ b/pyanaconda/modules/common/structures/subscription.py
@@ -23,8 +23,7 @@
from pyanaconda.modules.common.structures.secret import SecretData, SecretDataList
-__all__ = ["SystemPurposeData", "SubscriptionRequest", "AttachedSubscription"]
-
+__all__ = ["SystemPurposeData", "SubscriptionRequest"]
class SystemPurposeData(DBusData):
"""System purpose data."""
@@ -141,6 +140,7 @@ def __init__(self):
# need to be set
self._organization = ""
self._redhat_account_username = ""
+ self._redhat_account_organization = ""
# Candlepin instance
self._server_hostname = ""
# CDN base url
@@ -228,6 +228,27 @@ def account_username(self) -> Str:
def account_username(self, account_username: Str):
self._redhat_account_username = account_username
+ @property
+ def account_organization(self) -> Str:
+ """Red Hat account organization for subscription purposes.
+
+ In case the account for the given username is member
+ of multiple organizations, organization id needs to
+ be specified as well or else the registration attempt
+ will not be successful. This account dependent organization
+ id is deliberately separate from the org + key org id
+ to avoid collisions and issues in the GUI when switching
+ between authentication types.
+
+ :return: Red Hat account organization id
+ :rtype: str
+ """
+ return self._redhat_account_organization
+
+ @account_organization.setter
+ def account_organization(self, account_organization: Str):
+ self._redhat_account_organization = account_organization
+
@property
def server_hostname(self) -> Str:
"""Subscription server hostname.
@@ -392,145 +413,43 @@ def server_proxy_password(self, password: SecretData):
self._server_proxy_password = password
-class AttachedSubscription(DBusData):
- """Data for a single attached subscription."""
+class OrganizationData(DBusData):
+ """Data about a single organization in the Red Hat account system.
+
+ A Red Hat account is expected to be member of an organization,
+ with some accounts being members of more than one organization.
+ """
def __init__(self):
+ self._id = ""
self._name = ""
- self._service_level = ""
- self._sku = ""
- self._contract = ""
- self._start_date = ""
- self._end_date = ""
- # we can expect at least one entitlement
- # to be consumed per attached subscription
- self._consumed_entitlement_count = 1
-
- @property
- def name(self) -> Str:
- """Name of the attached subscription.
-
- Example: "Red Hat Beta Access"
-
- :return: subscription name
- :rtype: str
- """
- return self._name
-
- @name.setter
- def name(self, name: Str):
- self._name = name
-
- @property
- def service_level(self) -> Str:
- """Service level of the attached subscription.
-
- Example: "Premium"
-
- :return: service level
- :rtype: str
- """
- return self._service_level
-
- @service_level.setter
- def service_level(self, service_level: Str):
- self._service_level = service_level
@property
- def sku(self) -> Str:
- """SKU id of the attached subscription.
+ def id(self) -> Str:
+ """Id of the organization.
- Example: "MBT8547"
+ Example: "abc123efg456"
- :return: SKU id
- :rtype: str
- """
- return self._sku
-
- @sku.setter
- def sku(self, sku: Str):
- self._sku = sku
-
- @property
- def contract(self) -> Str:
- """Contract identifier.
-
- Example: "32754658"
-
- :return: contract identifier
- :rtype: str
- """
- return self._contract
-
- @contract.setter
- def contract(self, contract: Str):
- self._contract = contract
-
- @property
- def start_date(self) -> Str:
- """Subscription start date.
-
- We do not guarantee fixed date format,
- but we aim for the date to look good
- when displayed in a GUI and be human
- readable.
-
- For context see the following bug, that
- illustrates the issues we are having with
- the source date for this property, that
- prevent us from providing a consistent
- date format:
- https://bugzilla.redhat.com/show_bug.cgi?id=1793501
-
- Example: "Nov 04, 2019"
-
- :return: start date of the subscription
+ :return: organization id
:rtype: str
"""
- return self._start_date
+ return self._id
- @start_date.setter
- def start_date(self, start_date: Str):
- self._start_date = start_date
+ @id.setter
+ def id(self, organization_id: Str):
+ self._id = organization_id
@property
- def end_date(self) -> Str:
- """Subscription end date.
-
- We do not guarantee fixed date format,
- but we aim for the date to look good
- when displayed in a GUI and be human
- readable.
-
- For context see the following bug, that
- illustrates the issues we are having with
- the source date for this property, that
- prevent us from providing a consistent
- date format:
- https://bugzilla.redhat.com/show_bug.cgi?id=1793501
+ def name(self) -> Str:
+ """Name of the organization.
- Example: "Nov 04, 2020"
+ Example: "Foo Organization"
- :return: end date of the subscription
+ :return: organization name
:rtype: str
"""
- return self._end_date
-
- @end_date.setter
- def end_date(self, end_date: Str):
- self._end_date = end_date
-
- @property
- def consumed_entitlement_count(self) -> Int:
- """Number of consumed entitlements for this subscription.
-
- Example: "1"
-
- :return: consumed entitlement number
- :rtype: int
- """
- return self._consumed_entitlement_count
+ return self._name
- @consumed_entitlement_count.setter
- def consumed_entitlement_count(self, consumed_entitlement_count: Int):
- self._consumed_entitlement_count = consumed_entitlement_count
+ @name.setter
+ def name(self, organization_name: Str):
+ self._name = organization_name
diff --git a/pyanaconda/modules/subscription/installation.py b/pyanaconda/modules/subscription/installation.py
index 108c9ce28c3..ca8ee177859 100644
--- a/pyanaconda/modules/subscription/installation.py
+++ b/pyanaconda/modules/subscription/installation.py
@@ -29,6 +29,8 @@
from pyanaconda.modules.common.task import Task
from pyanaconda.modules.common.errors.installation import InsightsConnectError, \
InsightsClientMissingError, SubscriptionTokenTransferError
+from pyanaconda.modules.common.errors.subscription import SatelliteProvisioningError
+from pyanaconda.modules.subscription import satellite
from pyanaconda.anaconda_loggers import get_module_logger
log = get_module_logger(__name__)
@@ -245,3 +247,58 @@ def run(self):
# transfer the RHSM config file
self._transfer_file(self.RHSM_CONFIG_FILE_PATH, "RHSM config file")
+
+
+class ProvisionTargetSystemForSatelliteTask(Task):
+ """Provision target system for communication with Satellite.
+
+ If the System gets registered to Satellite at installation time,
+ the provisioning is applied only to the installation environment.
+ This task makes sure it is applied also on the target system.
+
+ Run the appropriate Satellite provisioning script on the target system.
+
+ This should assure the target system has all the needed certificates
+ installed and rhsm.conf tweaks applied.
+ """
+
+ def __init__(self, provisioning_script):
+ """Create a new task.
+
+ :param str provisioning_script: Satellite provisioning script in string form
+ """
+ super().__init__()
+ self._provisioning_script = provisioning_script
+
+ @property
+ def name(self):
+ return "Provisioning target system for Satellite"
+
+ def run(self):
+ """Provision target system for Satellite.
+
+ First check if we are actually registered to a Satellite instance
+ by checking if we got a provisioning script.
+
+ If not, do nothing.
+
+ If we are registered to a Satellite instance, run the Satellite
+ provisioning script that has been downloaded from the instance previously.
+
+ """
+ if self._provisioning_script:
+ log.debug("subscription: provisioning target system for Satellite")
+ provisioning_success = satellite.run_satellite_provisioning_script(
+ provisioning_script=self._provisioning_script,
+ run_on_target_system=True
+
+ )
+ if provisioning_success:
+ log.debug("subscription: target system successfully provisioned for Satellite")
+ else:
+ raise SatelliteProvisioningError("Satellite provisioning script failed.")
+ else:
+ # lets assume here that no provisioning script == not registered to Satellite
+ log.debug(
+ "subscription: not registered to Satellite, skipping Satellite provisioning."
+ )
diff --git a/pyanaconda/modules/subscription/runtime.py b/pyanaconda/modules/subscription/runtime.py
index a996bec7048..6bfd1309aec 100644
--- a/pyanaconda/modules/subscription/runtime.py
+++ b/pyanaconda/modules/subscription/runtime.py
@@ -17,23 +17,31 @@
#
import os
import json
-import datetime
from collections import namedtuple
-from dasbus.typing import get_variant, Str
+from dasbus.typing import get_variant, Str, Bool, get_native
from dasbus.connection import MessageBus
from dasbus.error import DBusError
from pyanaconda.core.i18n import _
+from pyanaconda.core.constants import SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD, \
+ SUBSCRIPTION_REQUEST_TYPE_ORG_KEY
+from pyanaconda.core.payload import ProxyString
+from pyanaconda.core import service
+from pyanaconda.ui.lib.subscription import username_password_sufficient, org_keys_sufficient
from pyanaconda.modules.common.task import Task
from pyanaconda.modules.common.constants.services import RHSM
-from pyanaconda.modules.common.constants.objects import RHSM_REGISTER
+from pyanaconda.modules.common.constants.objects import RHSM_REGISTER, RHSM_REGISTER_SERVER, \
+ RHSM_UNREGISTER, RHSM_CONFIG, RHSM_SYSPURPOSE
from pyanaconda.modules.common.errors.subscription import RegistrationError, \
- UnregistrationError, SubscriptionError
-from pyanaconda.modules.common.structures.subscription import AttachedSubscription, \
- SystemPurposeData
-from pyanaconda.modules.subscription import system_purpose
-from pyanaconda.modules.subscription.constants import SERVER_HOSTNAME_NOT_SATELLITE_PREFIX
+ UnregistrationError, SatelliteProvisioningError, MultipleOrganizationsError
+from pyanaconda.modules.common.structures.subscription import SystemPurposeData, OrganizationData
+from pyanaconda.modules.subscription import system_purpose, satellite
+from pyanaconda.modules.subscription.constants import RHSM_SERVICE_NAME, \
+ SERVER_HOSTNAME_NOT_SATELLITE_PREFIX
+from pyanaconda.modules.subscription.subscription_interface import \
+ RetrieveOrganizationsTaskInterface
+from pyanaconda.modules.subscription.utils import flatten_rhsm_nested_dict
from pyanaconda.anaconda_loggers import get_module_logger
import gi
@@ -81,7 +89,7 @@ def _get_connection(self):
SystemSubscriptionData = namedtuple("SystemSubscriptionData",
- ["attached_subscriptions", "system_purpose_data"])
+ ["system_purpose_data"])
class SystemPurposeConfigurationTask(Task):
@@ -219,7 +227,7 @@ def run(self):
class RegisterWithUsernamePasswordTask(Task):
"""Register the system via username + password."""
- def __init__(self, rhsm_register_server_proxy, username, password):
+ def __init__(self, rhsm_register_server_proxy, username, password, organization):
"""Create a new registration task.
It is assumed the username and password have been
@@ -228,11 +236,13 @@ def __init__(self, rhsm_register_server_proxy, username, password):
:param rhsm_register_server_proxy: DBus proxy for the RHSM RegisterServer object
:param str username: Red Hat account username
:param str password: Red Hat account password
+ :param str organization: organization id
"""
super().__init__()
self._rhsm_register_server_proxy = rhsm_register_server_proxy
self._username = username
self._password = password
+ self._organization = organization
@property
def name(self):
@@ -242,23 +252,48 @@ def run(self):
"""Register the system with Red Hat account username and password.
:raises: RegistrationError if calling the RHSM DBus API returns an error
+ :return: JSON string describing registration state
+ :rtype: str
"""
+ if not self._organization:
+ # If no organization id is specified check if the account is member of more than
+ # one organization.
+ # If it is member of just one organization, this is fine and we can proceed
+ # with the registration attempt.
+ # If it is member of 2 or more organizations, this is an invalid state as without
+ # an organization id being specified RHSM will not know what organization to register
+ # the machine. In this throw raise a specific exception so that the GUI can react
+ # accordingly and help the user fix the issue.
+
+ org_data_task = RetrieveOrganizationsTask(
+ rhsm_register_server_proxy=self._rhsm_register_server_proxy,
+ username=self._username,
+ password=self._password,
+ reset_cache=True
+ )
+ org_list = org_data_task.run()
+ if len(org_list) > 1:
+ raise MultipleOrganizationsError(
+ _("Please select an organization for your account and try again.")
+ )
+
log.debug("subscription: registering with username and password")
with RHSMPrivateBus(self._rhsm_register_server_proxy) as private_bus:
try:
locale = os.environ.get("LANG", "")
+
private_register_proxy = private_bus.get_proxy(RHSM.service_name,
RHSM_REGISTER.object_path)
- # We do not yet support setting organization for username & password
- # registration, so organization is blank for now.
- organization = ""
- private_register_proxy.Register(organization,
- self._username,
- self._password,
- {},
- {},
- locale)
+ registration_data = private_register_proxy.Register(
+ self._organization,
+ self._username,
+ self._password,
+ {"enable_content": get_variant(Bool, True)},
+ {},
+ locale
+ )
log.debug("subscription: registered with username and password")
+ return registration_data
except DBusError as e:
log.debug("subscription: failed to register with username and password: %s",
str(e))
@@ -293,6 +328,8 @@ def run(self):
"""Register the system with organization name and activation key.
:raises: RegistrationError if calling the RHSM DBus API returns an error
+ :return: JSON string describing registration state
+ :rtype: str
"""
log.debug("subscription: registering with organization and activation key")
with RHSMPrivateBus(self._rhsm_register_server_proxy) as private_bus:
@@ -300,12 +337,15 @@ def run(self):
locale = os.environ.get("LANG", "")
private_register_proxy = private_bus.get_proxy(RHSM.service_name,
RHSM_REGISTER.object_path)
- private_register_proxy.RegisterWithActivationKeys(self._organization,
- self._activation_keys,
- {},
- {},
- locale)
+ registration_data = private_register_proxy.RegisterWithActivationKeys(
+ self._organization,
+ self._activation_keys,
+ {},
+ {},
+ locale
+ )
log.debug("subscription: registered with organization and activation key")
+ return registration_data
except DBusError as e:
log.debug("subscription: failed to register with organization & key: %s", str(e))
# RHSM exception contain details as JSON due to DBus exception handling limitations
@@ -318,13 +358,17 @@ def run(self):
class UnregisterTask(Task):
"""Unregister the system."""
- def __init__(self, rhsm_unregister_proxy):
+ def __init__(self, rhsm_observer, registered_to_satellite, rhsm_configuration):
"""Create a new unregistration task.
- :param rhsm_unregister_proxy: DBus proxy for the RHSM Unregister object
+ :param rhsm_observer: DBus service observer for talking to RHSM
+ :param dict rhsm_configuration: flat "clean" RHSM configuration dict to restore
+ :param bool registered_to_satellite: were we registered to Satellite ?
"""
super().__init__()
- self._rhsm_unregister_proxy = rhsm_unregister_proxy
+ self._rhsm_observer = rhsm_observer
+ self._registered_to_satellite = registered_to_satellite
+ self._rhsm_configuration = rhsm_configuration
@property
def name(self):
@@ -332,180 +376,46 @@ def name(self):
def run(self):
"""Unregister the system."""
- log.debug("subscription: unregistering the system")
+ log.debug("registration attempt: unregistering the system")
try:
locale = os.environ.get("LANG", "")
- self._rhsm_unregister_proxy.Unregister({}, locale)
+ rhsm_unregister_proxy = self._rhsm_observer.get_proxy(RHSM_UNREGISTER)
+ rhsm_unregister_proxy.Unregister({}, locale)
log.debug("subscription: the system has been unregistered")
except DBusError as e:
- log.exception("subscription: failed to unregister: %s", str(e))
+ log.error("registration attempt: failed to unregister: %s", str(e))
exception_dict = json.loads(str(e))
# return a generic error message in case the RHSM provided error message
# is missing
message = exception_dict.get("message", _("Unregistration failed."))
- raise UnregistrationError(message) from None
-
-
-class AttachSubscriptionTask(Task):
- """Attach a subscription."""
-
- def __init__(self, rhsm_attach_proxy, sla):
- """Create a new subscription task.
-
- :param rhsm_attach_proxy: DBus proxy for the RHSM Attach object
- :param str sla: organization name for subscription purposes
- """
- super().__init__()
- self._rhsm_attach_proxy = rhsm_attach_proxy
- self._sla = sla
-
- @property
- def name(self):
- return "Attach a subscription"
-
- def run(self):
- """Attach a subscription to the installation environment.
-
- This subscription will be used for CDN access during the
- installation and then transferred to the target system
- via separate DBus task.
-
- :raises: SubscriptionError if RHSM API DBus call fails
- """
- log.debug("subscription: auto-attaching a subscription")
- try:
- locale = os.environ.get("LANG", "")
- self._rhsm_attach_proxy.AutoAttach(self._sla, {}, locale)
- log.debug("subscription: auto-attached a subscription")
- except DBusError as e:
- log.debug("subscription: auto-attach failed: %s", str(e))
- exception_dict = json.loads(str(e))
- # return a generic error message in case the RHSM provided error message
- # is missing
- message = exception_dict.get("message", _("Failed to attach subscription."))
- raise SubscriptionError(message) from None
+ raise UnregistrationError(message) from e
+
+ # in case we were Registered to Satellite, roll back Satellite provisioning as well
+ if self._registered_to_satellite:
+ log.debug("registration attempt: rolling back Satellite provisioning")
+ rollback_task = RollBackSatelliteProvisioningTask(
+ rhsm_config_proxy=self._rhsm_observer.get_proxy(RHSM_CONFIG),
+ rhsm_configuration=self._rhsm_configuration
+ )
+ rollback_task.run()
+ log.debug("registration attempt: Satellite provisioning rolled back")
-class ParseAttachedSubscriptionsTask(Task):
+class ParseSubscriptionDataTask(Task):
"""Parse data about subscriptions attached to the installation environment."""
- def __init__(self, rhsm_entitlement_proxy, rhsm_syspurpose_proxy):
+ def __init__(self, rhsm_syspurpose_proxy):
"""Create a new attached subscriptions parsing task.
- :param rhsm_entitlement_proxy: DBus proxy for the RHSM Entitlement object
:param rhsm_syspurpose_proxy: DBus proxy for the RHSM Syspurpose object
"""
super().__init__()
- self._rhsm_entitlement_proxy = rhsm_entitlement_proxy
self._rhsm_syspurpose_proxy = rhsm_syspurpose_proxy
@property
def name(self):
return "Parse attached subscription data"
- @staticmethod
- def _pretty_date(date_from_json):
- """Return pretty human readable date based on date from the input JSON."""
- # fallback in case of the parsing fails
- date_string = date_from_json
- # try to parse the date as ISO 8601 first
- try:
- date = datetime.datetime.strptime(date_from_json, "%Y-%m-%d")
- # get a nice human readable date
- return date.strftime("%b %d, %Y")
- except ValueError:
- pass
- try:
- # The start/end date in GetPools() output seems to be formatted as
- # "Locale's appropriate date representation.".
- # See bug 1793501 for possible issues with RHSM provided date parsing.
- date = datetime.datetime.strptime(date_from_json, "%m/%d/%y")
- # get a nice human readable date
- date_string = date.strftime("%b %d, %Y")
- except ValueError:
- log.warning("subscription: date parsing failed: %s", date_from_json)
- return date_string
-
- @classmethod
- def _parse_subscription_json(cls, subscription_json):
- """Parse the JSON into list of AttachedSubscription instances.
-
- The expected JSON is at top level a list of rather complex dictionaries,
- with each dictionary describing a single subscription that has been attached
- to the system.
-
- :param str subscription_json: JSON describing what subscriptions have been attached
- :return: list of attached subscriptions
- :rtype: list of AttachedSubscription instances
- """
- attached_subscriptions = []
- try:
- subscriptions = json.loads(subscription_json)
- except json.decoder.JSONDecodeError:
- log.warning("subscription: failed to parse GetPools() JSON output")
- # empty attached subscription list is better than an installation
- # ending crash
- return []
- # find the list of subscriptions
- consumed_subscriptions = subscriptions.get("consumed", [])
- log.debug("subscription: parsing %d attached subscriptions",
- len(consumed_subscriptions))
- # split the list of subscriptions into separate subscription dictionaries
- for subscription_info in consumed_subscriptions:
- attached_subscription = AttachedSubscription()
- # user visible product name
- attached_subscription.name = subscription_info.get(
- "subscription_name",
- _("product name unknown")
- )
-
- # subscription support level
- # - this does *not* seem to directly correlate to system purpose SLA attribute
- attached_subscription.service_level = subscription_info.get(
- "service_level",
- _("unknown")
- )
-
- # SKU
- # - looks like productId == SKU in this JSON output
- attached_subscription.sku = subscription_info.get(
- "sku",
- _("unknown")
- )
-
- # contract number
- attached_subscription.contract = subscription_info.get(
- "contract",
- _("not available")
- )
-
- # subscription start date
- # - convert the raw date data from JSON to something more readable
- start_date = subscription_info.get(
- "starts",
- _("unknown")
- )
- attached_subscription.start_date = cls._pretty_date(start_date)
-
- # subscription end date
- # - convert the raw date data from JSON to something more readable
- end_date = subscription_info.get(
- "ends",
- _("unknown")
- )
- attached_subscription.end_date = cls._pretty_date(end_date)
-
- # consumed entitlements
- # - this seems to correspond to the toplevel "quantity" key,
- # not to the pool-level "consumed" key for some reason
- # *or* the pool-level "quantity" key
- quantity_string = int(subscription_info.get("quantity_used", 1))
- attached_subscription.consumed_entitlement_count = quantity_string
- # add attached subscription to the list
- attached_subscriptions.append(attached_subscription)
- # return the list of attached subscriptions
- return attached_subscriptions
-
@staticmethod
def _parse_system_purpose_json(final_syspurpose_json):
"""Parse the JSON into a SystemPurposeData instance.
@@ -557,34 +467,544 @@ def run(self):
in system purpose data being different after registration.
"""
locale = os.environ.get("LANG", "")
- # fetch subscription status data
- subscription_json = self._rhsm_entitlement_proxy.GetPools(
- {"pool_subsets": get_variant(Str, "consumed")},
- {},
- locale
- )
- subscription_data_length = 0
- # Log how much subscription data we got for debugging purposes.
- # By only logging length, we should be able to debug cases of no
- # or incomplete data being logged, without logging potentially
- # sensitive subscription status detail into the installation logs
- # stored on the target system.
- if subscription_json:
- subscription_data_length = len(subscription_json)
- log.debug("subscription: fetched subscription status data: %d characters",
- subscription_data_length)
- else:
- log.warning("subscription: fetched empty subscription status data")
-
# fetch final system purpose data
log.debug("subscription: fetching final syspurpose data")
final_syspurpose_json = self._rhsm_syspurpose_proxy.GetSyspurpose(locale)
log.debug("subscription: final syspurpose data: %s", final_syspurpose_json)
# parse the JSON strings
- attached_subscriptions = self._parse_subscription_json(subscription_json)
system_purpose_data = self._parse_system_purpose_json(final_syspurpose_json)
# return the DBus structures as a named tuple
- return SystemSubscriptionData(attached_subscriptions=attached_subscriptions,
- system_purpose_data=system_purpose_data)
+ return SystemSubscriptionData(system_purpose_data=system_purpose_data)
+
+
+class DownloadSatelliteProvisioningScriptTask(Task):
+ """Download the provisioning script from a Satellite instance."""
+
+ def __init__(self, satellite_url, proxy_url):
+ """Create a new Satellite related task.
+
+ :param str satellite_url: URL to Satellite instace to download from
+ :param str proxy_url: proxy URL for the download attempt
+ """
+ super().__init__()
+ self._satellite_url = satellite_url
+ self._proxy_url = proxy_url
+
+ @property
+ def name(self):
+ return "Download Satellite provisioning script"
+
+ def run(self):
+ log.debug("subscription: downloading Satellite provisioning script")
+ return satellite.download_satellite_provisioning_script(
+ satellite_url=self._satellite_url,
+ proxy_url=self._proxy_url
+ )
+
+
+class RunSatelliteProvisioningScriptTask(Task):
+ """Run the provisioning script we downloaded from a Satellite instance."""
+
+ def __init__(self, provisioning_script):
+ """Create a new Satellite related task.
+
+ :param str provisioning_script: Satellite provisioning script in string form
+ """
+ super().__init__()
+ self._provisioning_script = provisioning_script
+
+ @property
+ def name(self):
+ return "Run Satellite provisioning script"
+
+ def run(self):
+ log.debug("subscription: running Satellite provisioning script"
+ " in installation environment")
+
+ provisioning_success = satellite.run_satellite_provisioning_script(
+ provisioning_script=self._provisioning_script,
+ run_on_target_system=False
+ )
+
+ if provisioning_success:
+ log.debug("subscription: Satellite provisioning script executed successfully")
+ else:
+ message = "Failed to run Satellite provisioning script."
+ raise SatelliteProvisioningError(message)
+
+
+class BackupRHSMConfBeforeSatelliteProvisioningTask(Task):
+ """Backup the RHSM configuration state before the Satellite provisioning script is run.
+
+ The Satellite provisioning script sets arbitrary RHSM configuration options, which
+ we might need to roll back in case the user decides to unregister and then register
+ to a different Satellite instance or back to Hosted Candlepin.
+
+ So backup the RHSM configuration state just before we run the Satellite provisioning
+ script that changes the config file. This gives us a config snapshot we can then use
+ to restore the RHSM configuration to a "clean" state as needed.
+ """
+
+ def __init__(self, rhsm_config_proxy):
+ """Create a new Satellite related task.
+
+ :param rhsm_config_proxy: DBus proxy for the RHSM Config object
+ """
+ super().__init__()
+ self._rhsm_config_proxy = rhsm_config_proxy
+
+ @property
+ def name(self):
+ return "Save RHSM configuration before Satellite provisioning"
+
+ def run(self):
+ # retrieve a snapshot of "clean" RHSM configuration and return it
+ return get_native(self._rhsm_config_proxy.GetAll(""))
+
+
+class RollBackSatelliteProvisioningTask(Task):
+ """Roll back relevant parts of Satellite provisioning.
+
+ The current Anaconda GUI makes it possible to unregister and
+ change the Satellite URL as well as switch back from Satellite
+ to registration on Hosted Candlepin.
+
+ Due to this we need to be able to roll back changes to the RHSM
+ configuration done by the Satellite provisioning script.
+
+ To make this possible we first save a "clean" snapshot of the RHSM
+ config state so that this task can then restore the snapshot as
+ needed.
+
+ We don't actually uninstall the certs added by the provisioning
+ script, but they should not interfere with another run of a different
+ script & will be gone after the installation environment restarts.
+ """
+
+ def __init__(self, rhsm_config_proxy, rhsm_configuration):
+ """Create a new Satellite related task.
+
+ :param rhsm_config_proxy: DBus proxy for the RHSM Config object
+ :param dict rhsm_configuration: flat "clean" RHSM configuration dict to restore
+ """
+ super().__init__()
+ self._rhsm_config_proxy = rhsm_config_proxy
+ self._rhsm_configuration = rhsm_configuration
+
+ @property
+ def name(self):
+ return "Restore RHSM configuration after Satellite provisioning"
+
+ def run(self):
+ """Restore the full RHSM configuration back to clean values."""
+ # the SetAll() RHSM DBus API requires a dict of variants
+ config_dict = {}
+ for key, value in self._rhsm_configuration.items():
+ # if value is present in request, use it
+ config_dict[key] = get_variant(Str, value)
+ self._rhsm_config_proxy.SetAll(config_dict, "")
+
+
+class RegisterAndSubscribeTask(Task):
+ """Register and subscribe the installation environment.
+
+ NOTE: A separate installation task make sure all the subscription related tokens
+ and configuration files are transferred to the target system, to keep
+ the machine subscribed also after installation.
+
+ In case of registration to a Satellite instance another installation task
+ makes sure the system stays registered to Satellite after installation.
+ """
+
+ def __init__(self, rhsm_observer, subscription_request, system_purpose_data,
+ registered_callback, registered_to_satellite_callback,
+ simple_content_access_callback, subscription_attached_callback,
+ subscription_data_callback, satellite_script_callback,
+ config_backup_callback):
+ """Create a register-and-subscribe task.
+
+ :param rhsm_observer: DBus service observer for talking to RHSM
+ :param subscription_request: subscription request DBus struct
+ :param system_purpose_data: system purpose DBus struct
+
+ :param registered_callback: called when registration tasks finishes successfully
+ :param registered_to_satellite_callback: called after successful Satellite provisioning
+ :param simple_content_access_callback: called when registration tasks finishes successfully
+ :param subscription_attached_callback: called after subscription is attached
+ :param subscription_data_callback: called after subscription data is parsed
+ :param satellite_script_callback: called after Satellite provisioning script
+ has been downloaded
+ :param config_backup_callback: called when RHSM config data is ready to be backed up
+
+ :raises: SatelliteProvisioningError if Satellite provisioning fails
+ :raises: RegistrationError if registration fails
+ :raises: MultipleOrganizationsError if account is multiorg but no org id specified
+ """
+ super().__init__()
+ self._rhsm_observer = rhsm_observer
+ self._subscription_request = subscription_request
+ self._system_purpose_data = system_purpose_data
+ self._rhsm_configuration = {}
+ # callback for nested tasks
+ self._registered_callback = registered_callback
+ self._registered_to_satellite_callback = registered_to_satellite_callback
+ self._simple_content_access_callback = simple_content_access_callback
+ self._subscription_attached_callback = subscription_attached_callback
+ self._subscription_data_callback = subscription_data_callback
+ self._satellite_script_downloaded_callback = satellite_script_callback
+ self._config_backup_callback = config_backup_callback
+
+ @property
+ def name(self):
+ return "Register and subscribe"
+
+ @staticmethod
+ def _get_proxy_url(subscription_request):
+ """Construct proxy URL from proxy data (if any) in subscription request.
+
+ :param subscription_request: subscription request DBus struct
+ :return: proxy URL string or None if subscription request contains no usable proxy data
+ :rtype: Str or None
+ """
+ proxy_url = None
+ # construct proxy URL needed by the task from the
+ # proxy data in subscription request (if any)
+ # (it is logical to use the same proxy for provisioning
+ # script download as for RHSM access)
+ if subscription_request.server_proxy_hostname:
+ proxy = ProxyString(host=subscription_request.server_proxy_hostname,
+ username=subscription_request.server_proxy_user,
+ password=subscription_request.server_proxy_password.value)
+ # only set port if valid in the struct (not -1):
+ if subscription_request.server_proxy_port != -1:
+ # ProxyString expects the port to be a string
+ proxy.port = str(subscription_request.server_proxy_port)
+ # refresh the ProxyString internal URL cache after setting the port number
+ proxy.parse_components()
+ proxy_url = str(proxy)
+ return proxy_url
+
+ @staticmethod
+ def _detect_sca_from_registration_data(registration_data_json):
+ """Detect SCA/entitlement mode from registration data.
+
+ This function checks JSON data describing registration state as returned
+ by the the Register() or RegisterWithActivationKeys() RHSM DBus methods.
+ Based on the value of the "contentAccessMode" key present in a dictionary available
+ under the "owner" top level key.
+
+ :param str registration_data_json: registration data in JSON format
+ :return: True if data inicates SCA enabled, False otherwise
+ """
+ # we can't try to detect SCA mode if we don't have any registration data
+ if not registration_data_json:
+ log.warning("no registraton data provided, skipping SCA mode detection attempt")
+ return False
+ registration_data = json.loads(registration_data_json)
+ owner_data = registration_data.get("owner")
+
+ if owner_data:
+ content_access_mode = owner_data.get("contentAccessMode")
+ if content_access_mode == "org_environment":
+ # SCA explicitely noted as enabled
+ return True
+ elif content_access_mode == "entitlement":
+ # SCA explicitely not enabled
+ return False
+ else:
+ log.warning("contentAccessMode mode not set to known value:")
+ log.warning(content_access_mode)
+ # unknown mode or missing data -> not SCA
+ return False
+ else:
+ # we have no data indicating SCA is enabled
+ return False
+
+ def _provision_system_for_satellite(self):
+ """Provision the installation environment for a Satellite instance.
+
+ This method is speculatively run if custom server hostname has been
+ set by the user. Only if the URL specified by the server hostname
+ contains Satellite provisioning artifacts then actually provisioning
+ of installation environment will take place.
+
+ """
+ # First check if the server_hostname has the not-satellite prefix.
+ # If it does have the prefix, log the fact and skip Satellite provisioning.
+ if self._subscription_request.server_hostname.startswith(
+ SERVER_HOSTNAME_NOT_SATELLITE_PREFIX
+ ):
+ log.debug("registration attempt: server hostname marked as not Satellite URL")
+ log.debug("registration attempt: skipping Satellite provisioning")
+ return
+
+ # create the download task
+ provisioning_script = None
+ download_task = DownloadSatelliteProvisioningScriptTask(
+ satellite_url=self._subscription_request.server_hostname,
+ proxy_url=self._get_proxy_url(self._subscription_request)
+ )
+
+ # run the download task
+ try:
+ log.debug("registration attempt: downloading Satellite provisioning script")
+ provisioning_script = download_task.run()
+ log.debug("registration attempt: downloaded Satellite provisioning script")
+ self._satellite_script_downloaded_callback(provisioning_script)
+ except SatelliteProvisioningError as e:
+ log.debug("registration attempt: failed to download Satellite provisioning script")
+ # Failing to download the Satellite provisioning script for a user provided
+ # server hostname is an unrecoverable error (wrong URL or incorrectly configured
+ # Satellite instance), so we end there.
+ raise e
+
+ # before running the Satellite provisioning script we back up the current RHSM config
+ # file state, so that we can restore it if Satellite provisioning rollback become necessary
+ rhsm_config_proxy = self._rhsm_observer.get_proxy(RHSM_CONFIG)
+ backup_task = BackupRHSMConfBeforeSatelliteProvisioningTask(
+ rhsm_config_proxy=rhsm_config_proxy
+ )
+ # Run the task and flatten the returned configuration
+ # (so that it can be fed to SetAll()) now, so we don't have to do that later.
+ flat_rhsm_configuration = {}
+ nested_rhsm_configuration = backup_task.run()
+ if nested_rhsm_configuration:
+ flat_rhsm_configuration = flatten_rhsm_nested_dict(nested_rhsm_configuration)
+ self._config_backup_callback(flat_rhsm_configuration)
+ # also store a copy in this task, in case we encounter an error and need to roll-back
+ # when this task is still running
+ self._rhsm_configuration = flat_rhsm_configuration
+
+ # now run the Satellite provisioning script we just downloaded, so that the installation
+ # environment can talk to the Satellite instance the user has specified via custom
+ # server hostname
+ run_script_task = RunSatelliteProvisioningScriptTask(
+ provisioning_script=provisioning_script
+ )
+ run_script_task.succeeded_signal.connect(
+ lambda: self._registered_to_satellite_callback(True)
+ )
+ try:
+ log.debug("registration attempt: running Satellite provisioning script")
+ run_script_task.run_with_signals()
+ log.debug("registration attempt: Satellite provisioning script has been run")
+ # unfortunately the RHSM service apparently does not pick up the changes done
+ # by the provisioning script to rhsm.conf, so we need to restart the RHSM systemd
+ # service, which will make it re-read the config file
+ service.restart_service(RHSM_SERVICE_NAME)
+
+ except SatelliteProvisioningError as e:
+ log.debug("registration attempt: Satellite provisioning script run failed")
+ # Failing to run the Satellite provisioning script successfully,
+ # which is an unrecoverable error, so we end there.
+ raise e
+
+ def _roll_back_satellite_provisioning(self):
+ """Something failed after we did Satellite provisioning - roll it back."""
+ log.debug("registration attempt: rolling back Satellite provisioning")
+ rollback_task = RollBackSatelliteProvisioningTask(
+ rhsm_config_proxy=self._rhsm_observer.get_proxy(RHSM_CONFIG),
+ rhsm_configuration=self._rhsm_configuration
+ )
+ rollback_task.run()
+ log.debug("registration attempt: Satellite provisioning rolled back")
+
+ def run(self):
+ """Try to register and subscribe the installation environment."""
+ provisioned_for_satellite = False
+ # check authentication method has been set and credentials seem to be
+ # sufficient (though not necessarily valid)
+ register_task = None
+ if self._subscription_request.type == SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD:
+ if username_password_sufficient(self._subscription_request):
+ username = self._subscription_request.account_username
+ password = self._subscription_request.account_password.value
+ organization = self._subscription_request.account_organization
+ register_server_proxy = self._rhsm_observer.get_proxy(RHSM_REGISTER_SERVER)
+ register_task = RegisterWithUsernamePasswordTask(
+ rhsm_register_server_proxy=register_server_proxy,
+ username=username,
+ password=password,
+ organization=organization
+ )
+ elif self._subscription_request.type == SUBSCRIPTION_REQUEST_TYPE_ORG_KEY:
+ if org_keys_sufficient(self._subscription_request):
+ organization = self._subscription_request.organization
+ activation_keys = self._subscription_request.activation_keys.value
+ register_server_proxy = self._rhsm_observer.get_proxy(RHSM_REGISTER_SERVER)
+ register_task = RegisterWithOrganizationKeyTask(
+ rhsm_register_server_proxy=register_server_proxy,
+ organization=organization,
+ activation_keys=activation_keys
+ )
+ if register_task:
+ # Now that we know we can do a registration attempt:
+ # 1) Connect task success callbacks.
+ register_task.succeeded_signal.connect(lambda: self._registered_callback(True))
+ # set SCA state based on data returned by the registration task
+ register_task.succeeded_signal.connect(
+ lambda: self._simple_content_access_callback(
+ self._detect_sca_from_registration_data(register_task.get_result())
+ )
+ )
+
+ # 2) Check if custom server hostname is set, which would indicate we are most
+ # likely talking to a Satellite instance. If so, provision the installation
+ # environment for that Satellite instance.
+ if self._subscription_request.server_hostname:
+ # if custom server hostname is set, attempt to provision the installation
+ # environment for Satellite
+ log.debug("registration attempt: provisioning system for Satellite")
+ self._provision_system_for_satellite()
+ provisioned_for_satellite = True
+ # if we got there without an exception being raised, it was a success!
+ log.debug("registration attempt: system provisioned for Satellite")
+
+ # run the registration task
+ try:
+ register_task.run_with_signals()
+ except (RegistrationError, MultipleOrganizationsError) as e:
+ log.debug("registration attempt: registration attempt failed: %s", e)
+ if provisioned_for_satellite:
+ self._roll_back_satellite_provisioning()
+ raise e
+ log.debug("registration attempt: registration succeeded")
+ else:
+ log.debug(
+ "registration attempt: credentials insufficient, skipping registration attempt"
+ )
+ if provisioned_for_satellite:
+ self._roll_back_satellite_provisioning()
+ raise RegistrationError(_("Registration failed due to insufficient credentials."))
+
+ # if we got this far without an exception then subscriptions have been attached
+ self._subscription_attached_callback(True)
+
+ # parse attached subscription data
+ log.debug("registration attempt: parsing attached subscription data")
+ rhsm_syspurpose_proxy = self._rhsm_observer.get_proxy(RHSM_SYSPURPOSE)
+ parse_task = ParseSubscriptionDataTask(rhsm_syspurpose_proxy=rhsm_syspurpose_proxy)
+ parse_task.succeeded_signal.connect(
+ lambda: self._subscription_data_callback(parse_task.get_result())
+ )
+ parse_task.run_with_signals()
+
+
+class RetrieveOrganizationsTask(Task):
+ """Obtain data about the organizations the given Red Hat account is a member of.
+
+ While it is apparently not possible for a Red Hat account account to be a member
+ of multiple organizations on the Red Hat run subscription infrastructure
+ (hosted candlepin), its is a regular occurrence for accounts used for customer
+ Satellite instances.
+ """
+
+ # the cache is used to serve last-known-good data if calling the GetOrgs()
+ # DBus method can't be called successfully in some scenarios
+ _org_data_list_cache = []
+
+ def __init__(self, rhsm_register_server_proxy, username, password, reset_cache=False):
+ """Create a new organization data parsing task.
+
+ :param rhsm_register_server_proxy: DBus proxy for the RHSM RegisterServer object
+ :param str username: Red Hat account username
+ :param str password: Red Hat account password
+ :param bool reset_cache: clear the cache before calling GetOrgs()
+ """
+ super().__init__()
+ self._rhsm_register_server_proxy = rhsm_register_server_proxy
+ self._username = username
+ self._password = password
+ self._reset_cache = reset_cache
+
+ @property
+ def name(self):
+ return "Retrieve organizations"
+
+ @staticmethod
+ def _parse_org_data_json(org_data_json):
+ """Parse JSON data about organizations this Red Hat account belongs to.
+
+ As an account might be a member of multiple organizations,
+ the JSON data is an array of dictionaries, with one dictionary per organization.
+
+ :param str org_data_json: JSON describing organizations the given account belongs to
+ :return: data about the organizations the account belongs to
+ :rtype: list of OrganizationData instances
+ """
+ try:
+ org_json = json.loads(org_data_json)
+ except json.decoder.JSONDecodeError:
+ log.warning("subscription: failed to parse GetOrgs() JSON output")
+ # empty system purpose data is better than an installation ending crash
+ return []
+
+ org_data_list = []
+ for single_org in org_json:
+ org_data = OrganizationData()
+ # machine readable organization id
+ org_data.id = single_org.get("key", "")
+ # human readable organization name
+ org_data.name = single_org.get("displayName", "")
+ # finally, append to the list
+ org_data_list.append(org_data)
+
+ return org_data_list
+
+ def run(self):
+ """Parse organization data for a Red Hat account username and password.
+
+ :raises: RegistrationError if calling the RHSM DBus API returns an error
+ """
+ # reset the data cache if requested
+ if self._reset_cache:
+ RetrieveOrganizationsTask._org_data_list_cache = []
+ log.debug("subscription: getting data about organizations")
+ with RHSMPrivateBus(self._rhsm_register_server_proxy) as private_bus:
+ try:
+ locale = os.environ.get("LANG", "")
+ private_register_proxy = private_bus.get_proxy(
+ RHSM.service_name,
+ RHSM_REGISTER.object_path
+ )
+
+ org_data_json = private_register_proxy.GetOrgs(
+ self._username,
+ self._password,
+ {},
+ locale
+ )
+
+ log.debug("subscription: got organization data (%d characters)",
+ len(org_data_json))
+
+ # parse the JSON strings into list of DBus data objects
+ org_data = self._parse_org_data_json(org_data_json)
+
+ log.debug("subscription: updating org data cache")
+ RetrieveOrganizationsTask._org_data_list_cache = org_data
+ # return the DBus structure list
+ return org_data
+ except DBusError as e:
+ # Errors returned by the RHSM DBus API for this call are unfortunately
+ # quite ambiguous (especially if Hosted Candlepin is used) and we can't
+ # really decide which are fatal and which are not.
+ # So just log the full error JSON from the message field of the returned
+ # DBus exception and return empty organization list.
+ # If there really is something wrong with the credentials or RHSM
+ # configuration it will prevent the next stage - registration - from
+ # working anyway.
+ log.debug("subscription: failed to get organization data")
+ # log the raw exception JSON payload for debugging purposes
+ log.debug(str(e))
+ # if we have something in cache, log the cache is being used,
+ # if there is nothing don't log anything as the cache is empty
+ if RetrieveOrganizationsTask._org_data_list_cache:
+ log.debug("subscription: using cached organization data after failure")
+ return RetrieveOrganizationsTask._org_data_list_cache
+
+ def for_publication(self):
+ """Return a DBus representation."""
+ return RetrieveOrganizationsTaskInterface(self)
diff --git a/pyanaconda/modules/subscription/satellite.py b/pyanaconda/modules/subscription/satellite.py
new file mode 100644
index 00000000000..e96605c998f
--- /dev/null
+++ b/pyanaconda/modules/subscription/satellite.py
@@ -0,0 +1,181 @@
+#
+# Satellite support purpose library.
+#
+# 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 os
+import tempfile
+
+from requests import RequestException
+
+from pyanaconda.core import constants, util
+
+from pyanaconda.core.payload import ProxyString, ProxyStringError
+from pyanaconda.core.configuration.anaconda import conf
+from pyanaconda.core.path import make_directories
+
+from pyanaconda.anaconda_loggers import get_module_logger
+log = get_module_logger(__name__)
+
+# the well-known path of the Satellite instance URL where
+# the provisioning script should be located
+PROVISIONING_SCRIPT_SUB_PATH = "/pub/katello-rhsm-consumer"
+
+
+def download_satellite_provisioning_script(satellite_url, proxy_url=None):
+ """Download provisioning script from a Red Hat Satellite instance.
+
+ Download the provisioning script from a Satellite instance and return
+ it as a string.
+
+ Satellite instances usually have self signed certificates and also some tweaks
+ are usually required in rhsm.conf to connect to a customer run Satellite instance
+ instead of to Hosted Candlepin for subscription purposes.
+
+ Each Satellite instance thus hosts a provisioning script available over plain
+ HTTP that client machines can download and execute. This script has minimal dependencies
+ and provisions the machine to be able to talk to the one given Satellite instance
+ by installing it's self signed certificates and adjusting rhsm.conf.
+
+ NOTE: As the script is downloaded over plain HTTP it is advised to ever only
+ provision machines from a Satellite instance on a trusted network, to
+ avoid the possibility of the provisioning script being tempered with
+ during transit.
+
+ :param str satellite_url: Satellite instance URL
+ :param proxy_url: proxy URL to use when fetching the script
+ :type proxy_url: str or None if not set
+ :returns: True on success, False otherwise
+ """
+ # make sure the URL starts with protocol
+ if not satellite_url.startswith("http"):
+ satellite_url = "http://" + satellite_url
+
+ # construct the URL pointing to the provisioning script
+ script_url = satellite_url + PROVISIONING_SCRIPT_SUB_PATH
+
+ log.debug("subscription: fetching Satellite provisioning script from: %s", script_url)
+
+ headers = {"user-agent": constants.USER_AGENT}
+ proxies = {}
+ provisioning_script = ""
+
+ # process proxy URL (if any)
+ if proxy_url is not None:
+ try:
+ proxy = ProxyString(proxy_url)
+ proxies = {"http": proxy.url,
+ "https": proxy.url}
+ except ProxyStringError as e:
+ log.info("subscription: failed to parse proxy when fetching Satellite"
+ " provisioning script %s: %s",
+ proxy_url, e)
+
+ with util.requests_session() as session:
+ try:
+ # NOTE: we explicitly don't verify SSL certificates while
+ # downloading the provisioning script as the Satellite
+ # instance will most likely have it's own self signed certs that
+ # will only be trusted once the provisioning script runs
+ result = session.get(script_url, headers=headers,
+ proxies=proxies, verify=False,
+ timeout=constants.NETWORK_CONNECTION_TIMEOUT)
+ if result.ok:
+ provisioning_script = result.text
+ result.close()
+ log.debug("subscription: Satellite provisioning script downloaded (%d characters)",
+ len(provisioning_script))
+ return provisioning_script
+ else:
+ log.debug("subscription: server returned %i code when downloading"
+ " Satellite provisioning script", result.status_code)
+ result.close()
+ return None
+ except RequestException as e:
+ log.debug("subscription: can't download Satellite provisioning script"
+ " from %s with proxy: %s. Error: %s", script_url, proxies, e)
+ return None
+
+
+def run_satellite_provisioning_script(provisioning_script=None, run_on_target_system=False):
+ """Run the Satellite provisioning script.
+
+ Each Satellite instance provides a provisioning script that will
+ enable the currently running environment to talk to the given
+ Satellite instance.
+
+ This means that the self-signed certificates of the given
+ Satellite instance will be installed to the system but also some
+ necessary changes will be done to rhsm.conf.
+
+ As we need to provision both the installation environment *and* the target system
+ to talk to Satellite we need to run the provisioning script twice.
+ - once in the installation environment
+ - and once on the target system.
+
+ This is achieved by running this function first in the installation environment
+ with run_on_target_system == False before a registration attempt.
+ And then in the installation phase with run_on_target_system == True.
+
+ Implementation wise we just always run the script from a tempfile.
+
+ That way we can easily run it in the installation environment as well
+ as in the target system chroot with minimum code needed to make sure
+ it exists where we need it
+
+ :param str provisioning_script: content of the Satellite provisioning script
+ or None if no script is available
+ :param str run_on_target_system: run in the target system chroot instead,
+ otherwise run in the installation environment
+ :return: True on success, False otherwise
+ :rtype: bool
+ """
+ # first check we actually have the script
+ if provisioning_script is None:
+ log.warning("subscription: satellite provisioning script not available")
+ return False
+
+ # now that we have something to run, check where to run it
+ if run_on_target_system:
+ # run in the target system chroot
+ sysroot = conf.target.system_root
+ else:
+ # run in installation environment
+ sysroot = "/"
+
+ # create the tempfile containing the script in the sysroot in /tmp, just in case
+ sysroot_tmp = util.join_paths(sysroot, "/tmp")
+ # make sure the path exists
+ make_directories(sysroot_tmp)
+ with tempfile.NamedTemporaryFile(mode="w+t", dir=sysroot_tmp, prefix="satellite-") as tf:
+ # write the provisioning script to the tempfile & flush any caches, just in case
+ tf.write(provisioning_script)
+ tf.flush()
+ # We always set root to the correct sysroot, so the script will always
+ # look like it is in /tmp. So just split the randomly generated file name
+ # and combine it with /tmp to get sysroot specific script path.
+ filename = os.path.basename(tf.name)
+ chroot_script_path = os.path.join("/tmp", filename)
+ # and execute it in the sysroot
+ rc = util.execWithRedirect("bash", argv=[chroot_script_path], root=sysroot)
+ if rc == 0:
+ log.debug("subscription: satellite provisioning script executed successfully")
+ return True
+ else:
+ log.debug("subscription: satellite provisioning script executed with error")
+ return False
diff --git a/pyanaconda/modules/subscription/subscription.py b/pyanaconda/modules/subscription/subscription.py
index 632958cd781..1fe3d91cf6d 100644
--- a/pyanaconda/modules/subscription/subscription.py
+++ b/pyanaconda/modules/subscription/subscription.py
@@ -34,8 +34,8 @@
from pyanaconda.core.dbus import DBus
from pyanaconda.modules.common.constants.services import SUBSCRIPTION
-from pyanaconda.modules.common.constants.objects import RHSM_CONFIG, RHSM_REGISTER_SERVER, \
- RHSM_UNREGISTER, RHSM_ATTACH, RHSM_ENTITLEMENT, RHSM_SYSPURPOSE
+from pyanaconda.modules.common.constants.objects import RHSM_CONFIG, RHSM_SYSPURPOSE, \
+ RHSM_REGISTER_SERVER
from pyanaconda.modules.common.containers import TaskContainer
from pyanaconda.modules.common.structures.requirement import Requirement
@@ -43,13 +43,14 @@
from pyanaconda.modules.subscription.kickstart import SubscriptionKickstartSpecification
from pyanaconda.modules.subscription.subscription_interface import SubscriptionInterface
from pyanaconda.modules.subscription.installation import ConnectToInsightsTask, \
- RestoreRHSMDefaultsTask, TransferSubscriptionTokensTask
+ RestoreRHSMDefaultsTask, TransferSubscriptionTokensTask, \
+ ProvisionTargetSystemForSatelliteTask
from pyanaconda.modules.subscription.initialization import StartRHSMTask
from pyanaconda.modules.subscription.runtime import SetRHSMConfigurationTask, \
- RegisterWithUsernamePasswordTask, RegisterWithOrganizationKeyTask, \
- UnregisterTask, AttachSubscriptionTask, SystemPurposeConfigurationTask, \
- ParseAttachedSubscriptionsTask
+ RegisterAndSubscribeTask, UnregisterTask, SystemPurposeConfigurationTask, \
+ RetrieveOrganizationsTask
from pyanaconda.modules.subscription.rhsm_observer import RHSMObserver
+from pyanaconda.modules.subscription.utils import flatten_rhsm_nested_dict
from pykickstart.errors import KickstartParseWarning
@@ -80,10 +81,6 @@ def __init__(self):
self._subscription_request = SubscriptionRequest()
self.subscription_request_changed = Signal()
- # attached subscriptions
- self._attached_subscriptions = []
- self.attached_subscriptions_changed = Signal()
-
# Insights
# What are the defaults for Red Hat Insights ?
@@ -98,10 +95,20 @@ def __init__(self):
self._connect_to_insights = False
self.connect_to_insights_changed = Signal()
+ # Satellite
+ self._satellite_provisioning_script = None
+ self.registered_to_satellite_changed = Signal()
+ self._registered_to_satellite = False
+ self._rhsm_conf_before_satellite_provisioning = {}
+
# registration status
self.registered_changed = Signal()
self._registered = False
+ # simple content access
+ self.simple_content_access_enabled_changed = Signal()
+ self._sca_enabled = False
+
# subscription status
self.subscription_attached_changed = Signal()
self._subscription_attached = False
@@ -362,32 +369,6 @@ def set_subscription_request(self, subscription_request):
self.subscription_request_changed.emit()
log.debug("A subscription request set: %s", str(self._subscription_request))
- @property
- def attached_subscriptions(self):
- """A list of attached subscriptions.
-
- The list holds DBus structures with each structure holding information about
- one attached subscription. A system that has been successfully registered and
- subscribed usually has one or more subscriptions attached.
-
- :return: list of DBus structures, one per attached subscription
- :rtype: list of AttachedSubscription instances
- """
- return self._attached_subscriptions
-
- def set_attached_subscriptions(self, attached_subscriptions):
- """Set the list of attached subscriptions.
-
- :param attached_subscriptions: list of attached subscriptions to be set
- :type attached_subscriptions: list of AttachedSubscription instances
- """
- self._attached_subscriptions = attached_subscriptions
- self.attached_subscriptions_changed.emit()
- # as there is no public setter in the DBus API, we need to emit
- # the properties changed signal here manually
- self.module_properties_changed.emit()
- log.debug("Attached subscriptions set: %s", str(self._attached_subscriptions))
-
def _replace_current_subscription_request(self, new_request):
"""Replace current subscription request without loosing sensitive data.
@@ -477,6 +458,31 @@ def set_registered(self, system_registered):
self.module_properties_changed.emit()
log.debug("System registered set to: %s", system_registered)
+ @property
+ def registered_to_satellite(self):
+ """Return True if the system has been registered to a Satellite instance.
+
+ :return: True if the system has been registered to Satellite, False otherwise
+ :rtype: bool
+ """
+ return self._registered_to_satellite
+
+ def set_registered_to_satellite(self, system_registered_to_satellite):
+ """Set if the system is registered to a Satellite instance.
+
+ If we are not registered to a Satellite instance it means we are registered
+ to Hosted Candlepin.
+
+ :param bool system_registered_to_satellite: True if system has been registered
+ to Satellite, False otherwise
+ """
+ self._registered_to_satellite = system_registered_to_satellite
+ self.registered_to_satellite_changed.emit()
+ # as there is no public setter in the DBus API, we need to emit
+ # the properties changed signal here manually
+ self.module_properties_changed.emit()
+ log.debug("System registered to Satellite set to: %s", system_registered_to_satellite)
+
# subscription status
@property
@@ -500,6 +506,28 @@ def set_subscription_attached(self, system_subscription_attached):
self.module_properties_changed.emit()
log.debug("Subscription attached set to: %s", system_subscription_attached)
+ # simple content access status
+ @property
+ def simple_content_access_enabled(self):
+ """Return True if the system has been registered with SCA enabled.
+
+ :return: True if the system has been registered in SCA mode, False otherwise
+ :rtype: bool
+ """
+ return self._sca_enabled
+
+ def set_simple_content_access_enabled(self, sca_enabled):
+ """Set if Simple Content Access is enabled.
+
+ :param bool sca_enabled: True if SCA is enabled, False otherwise
+ """
+ self._sca_enabled = sca_enabled
+ self.simple_content_access_enabled_changed.emit()
+ # as there is no public setter in the DBus API, we need to emit
+ # the properties changed signal here manually
+ self.module_properties_changed.emit()
+ log.debug("Simple Content Access enabled set to: %s", sca_enabled)
+
# tasks
def install_with_tasks(self):
@@ -510,6 +538,8 @@ def install_with_tasks(self):
the INFO log level in rhsm.conf or else target system will
end up with RHSM logging in DEBUG mode
- transfer subscription tokens
+ - apply Satellite provisioning on the target system,
+ in case we are registered to Satellite
- connect to insights, this can run only once subscription
tokens are in place on the target system or else it would
fail as Insights client needs the subscription tokens to
@@ -525,6 +555,9 @@ def install_with_tasks(self):
sysroot=conf.target.system_root,
transfer_subscription_tokens=self.subscription_attached
),
+ ProvisionTargetSystemForSatelliteTask(
+ provisioning_script=self._satellite_provisioning_script,
+ ),
ConnectToInsightsTask(
sysroot=conf.target.system_root,
subscription_attached=self.subscription_attached,
@@ -551,26 +584,6 @@ def rhsm_observer(self):
"""
return self._rhsm_observer
- def _flatten_rhsm_nested_dict(self, nested_dict):
- """Convert the GetAll() returned nested dict into a flat one.
-
- RHSM returns a nested dict with categories on top
- and category keys & values inside. This is not convenient
- for setting keys based on original values, so
- let's normalize the dict to the flat key based
- structure similar to what's used by SetAll().
-
- :param dict nested_dict: the nested dict returned by GetAll()
- :return: flat key/value dictionary, similar to format used by SetAll()
- :rtype: dict
- """
- flat_dict = {}
- for category_key, category_dict in nested_dict.items():
- for key, value in category_dict.items():
- flat_key = "{}.{}".format(category_key, key)
- flat_dict[flat_key] = value
- return flat_dict
-
def get_rhsm_config_defaults(self):
"""Return RHSM config default values.
@@ -600,7 +613,7 @@ def get_rhsm_config_defaults(self):
# turn the variant into a dict with get_native()
nested_dict = get_native(proxy.GetAll(""))
# flatten the nested dict
- flat_dict = self._flatten_rhsm_nested_dict(nested_dict)
+ flat_dict = flatten_rhsm_nested_dict(nested_dict)
self._rhsm_config_defaults = flat_dict
return self._rhsm_config_defaults
@@ -618,105 +631,99 @@ def set_rhsm_config_with_task(self):
subscription_request=self._subscription_request)
return task
- def register_username_password_with_task(self):
- """Register with username and password based on current subscription request.
+ def unregister_with_task(self):
+ """Unregister the system.
:return: a DBus path of an installation task
"""
- # NOTE: we access self._subscription_request directly
- # to avoid the sensitive data clearing happening
- # in the subscription_request property getter
- username = self._subscription_request.account_username
- password = self._subscription_request.account_password.value
- register_server_proxy = self.rhsm_observer.get_proxy(RHSM_REGISTER_SERVER)
- task = RegisterWithUsernamePasswordTask(rhsm_register_server_proxy=register_server_proxy,
- username=username,
- password=password)
- # if the task succeeds, it means the system has been registered
- task.succeeded_signal.connect(
- lambda: self.set_registered(True))
+ # the configuration backup is already flattened by the task that fetched it,
+ # we can directly feed it to SetAll()
+ task = UnregisterTask(rhsm_observer=self.rhsm_observer,
+ registered_to_satellite=self.registered_to_satellite,
+ rhsm_configuration=self._rhsm_conf_before_satellite_provisioning)
+ # apply state changes on success
+ task.succeeded_signal.connect(self._system_unregistered_callback)
return task
- def register_organization_key_with_task(self):
- """Register with organization and activation key(s) based on current subscription request.
+ def _system_unregistered_callback(self):
+ """Callback function run on success of the unregistration task.
+
+ The general aim is to set the various variables to reflect that
+ the installation environment is no longer registered.
+ """
+ # we are no longer registered and subscribed
+ self.set_registered(False)
+ self.set_subscription_attached(False)
+ # clear the Satellite registration status as well
+ self.set_registered_to_satellite(False)
+ # don't forget to also clear the Satellite provisioning
+ # script, or else it will be run by the target system
+ # provisioning task
+ self._set_satellite_provisioning_script(None)
+ # also when we are no longer registered then we are are
+ # thus no longer in Simple Content Access mode
+ self.set_simple_content_access_enabled(False)
- :return: a DBus path of an installation task
+ def _set_system_subscription_data(self, system_subscription_data):
+ """A helper method invoked in ParseSubscriptionDataTask completed signal.
+
+ :param system_subscription_data: a named tuple holding attached subscriptions
+ and final system purpose data
"""
- # NOTE: we access self._subscription_request directly
- # to avoid the sensitive data clearing happening
- # in the subscription_request property getter
- organization = self._subscription_request.organization
- activation_keys = self._subscription_request.activation_keys.value
- register_server_proxy = self.rhsm_observer.get_proxy(RHSM_REGISTER_SERVER)
- task = RegisterWithOrganizationKeyTask(rhsm_register_server_proxy=register_server_proxy,
- organization=organization,
- activation_keys=activation_keys)
- # if the task succeeds, it means the system has been registered
- task.succeeded_signal.connect(
- lambda: self.set_registered(True))
- return task
+ self.set_system_purpose_data(system_subscription_data.system_purpose_data)
- def unregister_with_task(self):
- """Unregister the system.
+ def _set_satellite_provisioning_script(self, provisioning_script):
+ """Set satellite provisioning script we just downloaded.
- :return: a DBus path of an installation task
+ :param str provisioning_script: Satellite provisioning script in string form
"""
- rhsm_unregister_proxy = self.rhsm_observer.get_proxy(RHSM_UNREGISTER)
- task = UnregisterTask(rhsm_unregister_proxy=rhsm_unregister_proxy)
- # we will no longer be registered and subscribed if the task is successful,
- # so set the corresponding properties appropriately
- task.succeeded_signal.connect(
- lambda: self.set_registered(False))
- task.succeeded_signal.connect(
- lambda: self.set_subscription_attached(False))
- # and clear attached subscriptions
- task.succeeded_signal.connect(
- lambda: self.set_attached_subscriptions([]))
- return task
-
- def attach_subscription_with_task(self):
- """Attach a subscription.
+ self._satellite_provisioning_script = provisioning_script
- This should only be run on a system that has been successfully registered.
- Attached subscription depends on system type, system purpose data
- and entitlements available for the account that has been used for registration.
+ def _set_pre_satellite_rhsm_conf_snapshot(self, config_data):
+ """A helper method for BackupRHSMConfBeforeSatelliteProvisioningTask completed signal.
- :return: a DBus path of an installation task
+ :param config_data: RHSM config content before Satellite provisioning
"""
- sla = self.system_purpose_data.sla
- rhsm_attach_proxy = self.rhsm_observer.get_proxy(RHSM_ATTACH)
- task = AttachSubscriptionTask(rhsm_attach_proxy=rhsm_attach_proxy,
- sla=sla)
- # if the task succeeds, it means a subscription has been attached
- task.succeeded_signal.connect(
- lambda: self.set_subscription_attached(True))
- return task
+ self._rhsm_conf_before_satellite_provisioning = config_data
- def _set_system_subscription_data(self, system_subscription_data):
- """A helper method invoked in ParseAttachedSubscritionsTask completed signal.
+ def register_and_subscribe_with_task(self):
+ """Register and subscribe the installation environment.
- :param system_subscription_data: a named tuple holding attached subscriptions
- and final system purpose data
+ Also handle Satellite provisioning and attached subscription parsing.
+
+ :return: a DBus path of a runtime task
"""
- self.set_attached_subscriptions(system_subscription_data.attached_subscriptions)
- self.set_system_purpose_data(system_subscription_data.system_purpose_data)
- def parse_attached_subscriptions_with_task(self):
- """Parse attached subscriptions with task.
+ task = RegisterAndSubscribeTask(
+ rhsm_observer=self.rhsm_observer,
+ subscription_request=self._subscription_request,
+ system_purpose_data=self.system_purpose_data,
+ registered_callback=self.set_registered,
+ registered_to_satellite_callback=self.set_registered_to_satellite,
+ simple_content_access_callback=self.set_simple_content_access_enabled,
+ subscription_attached_callback=self.set_subscription_attached,
+ subscription_data_callback=self._set_system_subscription_data,
+ satellite_script_callback=self._set_satellite_provisioning_script,
+ config_backup_callback=self._set_pre_satellite_rhsm_conf_snapshot
+ )
- Parse data about attached subscriptions and final system purpose data.
- This data is available as JSON strings via the RHSM DBus API.
+ return task
- :return: a DBus path of an installation task
+ def retrieve_organizations_with_task(self):
+ """Retrieve organization data with task.
+
+ Parse data about organizations the currently used Red Hat account is a member of.
+ :return: a runtime task
"""
- rhsm_entitlement_proxy = self.rhsm_observer.get_proxy(RHSM_ENTITLEMENT)
- rhsm_syspurpose_proxy = self.rhsm_observer.get_proxy(RHSM_SYSPURPOSE)
- task = ParseAttachedSubscriptionsTask(rhsm_entitlement_proxy=rhsm_entitlement_proxy,
- rhsm_syspurpose_proxy=rhsm_syspurpose_proxy)
- # if the task succeeds, set attached subscriptions and system purpose data
- task.succeeded_signal.connect(
- lambda: self._set_system_subscription_data(task.get_result())
- )
+ # NOTE: we access self._subscription_request directly
+ # to avoid the sensitive data clearing happening
+ # in the subscription_request property getter
+ username = self._subscription_request.account_username
+ password = self._subscription_request.account_password.value
+ register_server_proxy = self.rhsm_observer.get_proxy(RHSM_REGISTER_SERVER)
+ task = RetrieveOrganizationsTask(rhsm_register_server_proxy=register_server_proxy,
+ username=username,
+ password=password)
return task
def collect_requirements(self):
diff --git a/pyanaconda/modules/subscription/subscription_interface.py b/pyanaconda/modules/subscription/subscription_interface.py
index 26f18d823fc..edeb4226f6d 100644
--- a/pyanaconda/modules/subscription/subscription_interface.py
+++ b/pyanaconda/modules/subscription/subscription_interface.py
@@ -20,13 +20,31 @@
from pyanaconda.modules.common.constants.services import SUBSCRIPTION
from pyanaconda.modules.common.base import KickstartModuleInterface
from pyanaconda.modules.common.structures.subscription import SystemPurposeData, \
- SubscriptionRequest, AttachedSubscription
+ SubscriptionRequest, OrganizationData
from pyanaconda.modules.common.containers import TaskContainer
-from dasbus.server.interface import dbus_interface
+from pyanaconda.modules.common.task import TaskInterface
+from dasbus.server.interface import dbus_interface, dbus_class
from dasbus.server.property import emits_properties_changed
from dasbus.typing import * # pylint: disable=wildcard-import
+@dbus_class
+class RetrieveOrganizationsTaskInterface(TaskInterface):
+ """The interface for a organization data parsing task.
+
+ Such a task returns a list of organization data objects.
+ """
+ @staticmethod
+ def convert_result(value) -> Variant:
+ """Convert the list of org data DBus structs.
+
+ Convert list of org data DBus structs to variant.
+ :param value: a validation report
+ :return: a variant with the structure
+ """
+ return get_variant(List[Structure], OrganizationData.to_structure_list(value))
+
+
@dbus_interface(SUBSCRIPTION.interface_name)
class SubscriptionInterface(KickstartModuleInterface):
"""DBus interface for the Subscription service."""
@@ -37,12 +55,14 @@ def connect_signals(self):
self.implementation.system_purpose_data_changed)
self.watch_property("SubscriptionRequest",
self.implementation.subscription_request_changed)
- self.watch_property("AttachedSubscriptions",
- self.implementation.attached_subscriptions_changed)
self.watch_property("InsightsEnabled",
self.implementation.connect_to_insights_changed)
self.watch_property("IsRegistered",
self.implementation.registered_changed)
+ self.watch_property("IsRegisteredToSatellite",
+ self.implementation.registered_to_satellite_changed)
+ self.watch_property("IsSimpleContentAccessEnabled",
+ self.implementation.simple_content_access_enabled_changed)
self.watch_property("IsSubscriptionAttached",
self.implementation.subscription_attached_changed)
@@ -121,13 +141,6 @@ def SubscriptionRequest(self, subscription_request: Structure):
converted_data = SubscriptionRequest.from_structure(subscription_request)
self.implementation.set_subscription_request(converted_data)
- @property
- def AttachedSubscriptions(self) -> List[Structure]:
- """Return a list of DBus structures holding data about attached subscriptions."""
- return AttachedSubscription.to_structure_list(
- self.implementation.attached_subscriptions
- )
-
@property
def InsightsEnabled(self) -> Int:
"""Connect the target system to Red Hat Insights."""
@@ -147,6 +160,16 @@ def IsRegistered(self) -> Bool:
"""Report if the system is registered."""
return self.implementation.registered
+ @property
+ def IsRegisteredToSatellite(self) -> Bool:
+ """Report if the system is registered to a Satellite instance."""
+ return self.implementation.registered_to_satellite
+
+ @property
+ def IsSimpleContentAccessEnabled(self) -> Bool:
+ """Report if Simple Content Access is enabled."""
+ return self.implementation.simple_content_access_enabled
+
@property
def IsSubscriptionAttached(self) -> Bool:
"""Report if an entitlement has been successfully attached."""
@@ -161,24 +184,6 @@ def SetRHSMConfigWithTask(self) -> ObjPath:
self.implementation.set_rhsm_config_with_task()
)
- def RegisterUsernamePasswordWithTask(self) -> ObjPath:
- """Register with username & password using a runtime DBus task.
-
- :return: a DBus path of an installation task
- """
- return TaskContainer.to_object_path(
- self.implementation.register_username_password_with_task()
- )
-
- def RegisterOrganizationKeyWithTask(self) -> ObjPath:
- """Register with organization & keys(s) using a runtime DBus task.
-
- :return: a DBus path of an installation task
- """
- return TaskContainer.to_object_path(
- self.implementation.register_organization_key_with_task()
- )
-
def UnregisterWithTask(self) -> ObjPath:
"""Unregister using a runtime DBus task.
@@ -188,20 +193,20 @@ def UnregisterWithTask(self) -> ObjPath:
self.implementation.unregister_with_task()
)
- def AttachSubscriptionWithTask(self) -> ObjPath:
- """Attach subscription using a runtime DBus task.
+ def RegisterAndSubscribeWithTask(self) -> ObjPath:
+ """Register and subscribe with a runtime DBus task.
- :return: a DBus path of an installation task
+ :return: a DBus path of a runtime task
"""
return TaskContainer.to_object_path(
- self.implementation.attach_subscription_with_task()
+ self.implementation.register_and_subscribe_with_task()
)
- def ParseAttachedSubscriptionsWithTask(self) -> ObjPath:
- """Parse attached subscriptions using a runtime DBus task.
+ def RetrieveOrganizationsWithTask(self) -> ObjPath:
+ """Get organization data using a runtime DBus task.
- :return: a DBus path of an installation task
+ :return: a DBus path of a runtime task
"""
return TaskContainer.to_object_path(
- self.implementation.parse_attached_subscriptions_with_task()
+ self.implementation.retrieve_organizations_with_task()
)
diff --git a/pyanaconda/modules/subscription/utils.py b/pyanaconda/modules/subscription/utils.py
new file mode 100644
index 00000000000..b107d61bba3
--- /dev/null
+++ b/pyanaconda/modules/subscription/utils.py
@@ -0,0 +1,39 @@
+#
+# Utility functions for the subscription 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.
+#
+
+def flatten_rhsm_nested_dict(nested_dict):
+ """Convert the GetAll() returned nested dict into a flat one.
+
+ RHSM returns a nested dict with categories on top
+ and category keys & values inside. This is not convenient
+ for setting keys based on original values, so
+ let's normalize the dict to the flat key based
+ structure similar to what's used by SetAll().
+
+ :param dict nested_dict: the nested dict returned by GetAll()
+ :return: flat key/value dictionary, similar to format used by SetAll()
+ :rtype: dict
+ """
+ flat_dict = {}
+ for category_key, category_dict in nested_dict.items():
+ for key, value in category_dict.items():
+ flat_key = "{}.{}".format(category_key, key)
+ flat_dict[flat_key] = value
+ return flat_dict
diff --git a/pyanaconda/ui/gui/spokes/installation_source.py b/pyanaconda/ui/gui/spokes/installation_source.py
index 0c98e17ff27..3c5a76ba2f2 100644
--- a/pyanaconda/ui/gui/spokes/installation_source.py
+++ b/pyanaconda/ui/gui/spokes/installation_source.py
@@ -28,7 +28,7 @@
SOURCE_TYPE_CLOSEST_MIRROR, SOURCE_TYPE_CDN, PAYLOAD_STATUS_SETTING_SOURCE, \
PAYLOAD_STATUS_INVALID_SOURCE, PAYLOAD_STATUS_CHECKING_SOFTWARE, SOURCE_TYPE_REPO_PATH, \
DRACUT_REPO_DIR
-from pyanaconda.core.i18n import _, CN_
+from pyanaconda.core.i18n import _, CN_, C_
from pyanaconda.core.path import join_paths
from pyanaconda.core.payload import parse_nfs_url, create_nfs_url, parse_hdd_url
from pyanaconda.core.regexes import URL_PARSE, HOSTNAME_PATTERN_WITHOUT_ANCHORS
@@ -122,7 +122,7 @@ def apply(self):
# attached there is no need to refresh the installation source,
# as without the subscription tokens the refresh would fail anyway.
if cdn_source and not self.subscribed:
- log.debug("CDN source but no subscribtion attached - skipping payload restart.")
+ log.debug("CDN source but no subscription attached - skipping payload restart.")
elif source_changed or repo_changed or self._error:
payloadMgr.start(self.payload)
else:
@@ -311,6 +311,22 @@ def subscribed(self):
subscribed = subscription_proxy.IsSubscriptionAttached
return subscribed
+ @property
+ def registered_to_satellite(self):
+ """Report if the system is registered to a Satellite instance.
+
+ NOTE: This will be always False when the Subscription
+ module is not available.
+
+ :return: True if registered to Satellite, False otherwise
+ :rtype: bool
+ """
+ registered_to_satellite = False
+ if is_module_available(SUBSCRIPTION):
+ subscription_proxy = SUBSCRIPTION.get_proxy()
+ registered_to_satellite = subscription_proxy.IsRegisteredToSatellite
+ return registered_to_satellite
+
@property
def status(self):
# When CDN is selected as installation source and system
@@ -327,6 +343,11 @@ def status(self):
source_proxy = self.payload.get_source_proxy()
return source_proxy.Description
+ if cdn_source and self.subscribed and self.registered_to_satellite:
+ # override the regular CDN source name to make it clear Satellite
+ # provided repositories are being used
+ return _("Satellite")
+
if thread_manager.get(constants.THREAD_CHECK_SOFTWARE):
return _(PAYLOAD_STATUS_CHECKING_SOFTWARE)
@@ -725,6 +746,26 @@ def refresh(self):
# Update the URL entry validation now that we're done messing with sensitivites
self._update_url_entry_check()
+ # If subscription module is available we might need to refresh the label
+ # of the CDN/Satellite radio button, so that it properly describes what is providing
+ # the repositories available after registration.
+ #
+ # For registration to Red Hat hosted infrastructure (also called Hosted Candlepin) the
+ # global Red Hat CDN efficiently provides quick access to the repositories to customers
+ # across the world over the public Internet.
+ #
+ # If registered to a customer Satellite instance, it is the Satellite instance itself that
+ # provides the software repositories.
+ #
+ # This is an important distinction as Satellite instances are often used in environments
+ # not connected to the public Internet, so seeing the installation source being provided
+ # by Red Hat CDN which the machine might not be able to reach could be very confusing.
+ if is_module_available(SUBSCRIPTION):
+ if self.registered_to_satellite:
+ self._cdn_button.set_label(C_("GUI|Software Source", "_Satellite"))
+ else:
+ self._cdn_button.set_label(C_("GUI|Software Source", "Red Hat _CDN"))
+
# Show the info bar with an error message if any.
# This error message has the highest priority.
if self._error:
diff --git a/pyanaconda/ui/gui/spokes/lib/subscription.py b/pyanaconda/ui/gui/spokes/lib/subscription.py
index 47b96b57792..578eb6f0a12 100644
--- a/pyanaconda/ui/gui/spokes/lib/subscription.py
+++ b/pyanaconda/ui/gui/spokes/lib/subscription.py
@@ -17,11 +17,6 @@
# Red Hat, Inc.
#
-import gi
-gi.require_version("Gtk", "3.0")
-gi.require_version("Pango", "1.0")
-from gi.repository import Gtk, Pango
-
from collections import namedtuple
from pyanaconda.core.i18n import _
@@ -108,129 +103,3 @@ def fill_combobox(combobox, user_provided_value, valid_values):
# set the active id (what item should be selected in the combobox)
combobox.set_active_id(active_id)
-
-
-def add_attached_subscription_delegate(listbox, subscription, delegate_index):
- """Add delegate representing an attached subscription to the listbox.
-
- :param listbox: a listbox to add the delegate to
- :type listbox: GTK ListBox
- :param subscription: a subscription attached to the system
- :type: AttachedSubscription instance
- :param int delegate_index: index of the delegate in the listbox
- """
- log.debug("Subscription GUI: adding subscription to listbox: %s", subscription.name)
- # if we are not the first delegate, we should pre-pend a spacer, so that the
- # actual delegates are nicely delimited
- if delegate_index != 0:
- row = Gtk.ListBoxRow()
- row.set_name("subscriptions_listbox_row_spacer")
- row.set_margin_top(4)
- listbox.insert(row, -1)
-
- # construct delegate
- row = Gtk.ListBoxRow()
- # set a name so that the ListBoxRow instance can be styled via CSS
- row.set_name("subscriptions_listbox_row")
-
- main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
- main_vbox.set_margin_top(12)
- main_vbox.set_margin_bottom(12)
-
- name_label = Gtk.Label(label='{}'.format(subscription.name),
- use_markup=True, wrap=True, wrap_mode=Pango.WrapMode.WORD_CHAR,
- hexpand=True, xalign=0, yalign=0.5)
- name_label.set_margin_start(12)
- name_label.set_margin_bottom(12)
-
- # create the first details grid
- details_grid_1 = Gtk.Grid()
- details_grid_1.set_column_spacing(12)
- details_grid_1.set_row_spacing(12)
-
- # first column
- service_level_label = Gtk.Label(label="{}".format(_("Service level")),
- use_markup=True, xalign=0)
- service_level_status_label = Gtk.Label(label=subscription.service_level)
- sku_label = Gtk.Label(label="{}".format(_("SKU")),
- use_markup=True, xalign=0)
- sku_status_label = Gtk.Label(label=subscription.sku, xalign=0)
- contract_label = Gtk.Label(label="{}".format(_("Contract")),
- use_markup=True, xalign=0)
- contract_status_label = Gtk.Label(label=subscription.contract, xalign=0)
-
- # add first column to the grid
- details_grid_1.attach(service_level_label, 0, 0, 1, 1)
- details_grid_1.attach(service_level_status_label, 1, 0, 1, 1)
- details_grid_1.attach(sku_label, 0, 1, 1, 1)
- details_grid_1.attach(sku_status_label, 1, 1, 1, 1)
- details_grid_1.attach(contract_label, 0, 2, 1, 1)
- details_grid_1.attach(contract_status_label, 1, 2, 1, 1)
-
- # second column
- start_date_label = Gtk.Label(label="{}".format(_("Start date")),
- use_markup=True, xalign=0)
- start_date_status_label = Gtk.Label(label=subscription.start_date, xalign=0)
- end_date_label = Gtk.Label(label="{}".format(_("End date")),
- use_markup=True, xalign=0)
- end_date_status_label = Gtk.Label(label=subscription.end_date, xalign=0)
- entitlements_label = Gtk.Label(label="{}".format(_("Entitlements")),
- use_markup=True, xalign=0)
- entitlement_string = _("{} consumed").format(subscription.consumed_entitlement_count)
- entitlements_status_label = Gtk.Label(label=entitlement_string, xalign=0)
-
- # create the second details grid
- details_grid_2 = Gtk.Grid()
- details_grid_2.set_column_spacing(12)
- details_grid_2.set_row_spacing(12)
-
- # add second column to the grid
- details_grid_2.attach(start_date_label, 0, 0, 1, 1)
- details_grid_2.attach(start_date_status_label, 1, 0, 1, 1)
- details_grid_2.attach(end_date_label, 0, 1, 1, 1)
- details_grid_2.attach(end_date_status_label, 1, 1, 1, 1)
- details_grid_2.attach(entitlements_label, 0, 2, 1, 1)
- details_grid_2.attach(entitlements_status_label, 1, 2, 1, 1)
-
- details_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=16)
- details_hbox.pack_start(details_grid_1, True, True, 12)
- details_hbox.pack_start(details_grid_2, True, True, 0)
-
- main_vbox.pack_start(name_label, True, True, 0)
- main_vbox.pack_start(details_hbox, True, True, 0)
-
- row.add(main_vbox)
-
- # append delegate to listbox
- listbox.insert(row, -1)
-
-
-def populate_attached_subscriptions_listbox(listbox, attached_subscriptions):
- """Populate the attached subscriptions listbox with delegates.
-
- Unfortunately it does not seem to be possible to create delegate templates
- that could be reused for each data item in the listbox via Glade, so
- we need to construct them imperatively via Python GTK API.
-
- :param listbox: listbox to populate
- :type listbox: GTK ListBox
- :param attached_subscriptions: list of AttachedSubscription instances
- """
- log.debug("Subscription GUI: populating attached subscriptions listbox")
-
- # start by making sure the listbox is empty
- for child in listbox.get_children():
- listbox.remove(child)
- del(child)
-
- # add one delegate per attached subscription
- delegate_index = 0
- for subscription in attached_subscriptions:
- add_attached_subscription_delegate(listbox, subscription, delegate_index)
- delegate_index = delegate_index + 1
-
- # Make sure the delegates are actually visible after the listbox has been cleared.
- # Without show_all() nothing would be visible past first clear.
- listbox.show_all()
-
- log.debug("Subscription GUI: attached subscriptions listbox has been populated")
diff --git a/pyanaconda/ui/gui/spokes/subscription.glade b/pyanaconda/ui/gui/spokes/subscription.glade
index b3d4058f47e..925fc496191 100644
--- a/pyanaconda/ui/gui/spokes/subscription.glade
+++ b/pyanaconda/ui/gui/spokes/subscription.glade
@@ -1,26 +1,27 @@
-
+