From 0c6ebd7aaa319d4c594d4c7d6a6ee99af5c5392a Mon Sep 17 00:00:00 2001 From: Martin Kolman Date: Wed, 3 Jul 2024 17:01:14 +0200 Subject: [PATCH] WiP --- anaconda.spec.in | 2 +- pyanaconda/errors.py | 27 + .../modules/common/constants/objects.py | 10 - .../modules/common/errors/installation.py | 6 + .../modules/common/errors/subscription.py | 11 +- .../modules/common/structures/subscription.py | 173 +-- pyanaconda/modules/subscription/constants.py | 2 + .../modules/subscription/installation.py | 57 + pyanaconda/modules/subscription/runtime.py | 826 +++++++--- pyanaconda/modules/subscription/satellite.py | 182 +++ .../modules/subscription/subscription.py | 279 ++-- .../subscription/subscription_interface.py | 79 +- pyanaconda/modules/subscription/utils.py | 39 + .../ui/gui/spokes/installation_source.py | 60 +- pyanaconda/ui/gui/spokes/lib/subscription.py | 126 -- pyanaconda/ui/gui/spokes/subscription.glade | 601 +++++--- pyanaconda/ui/gui/spokes/subscription.py | 114 +- pyanaconda/ui/lib/subscription.py | 120 +- .../modules/subscription/test_satellite.py | 231 +++ .../modules/subscription/test_subscription.py | 376 ++--- .../subscription/test_subscription_tasks.py | 1342 +++++++++++++++-- .../modules/subscription/utils_test.py | 35 + .../test_subscription_helpers.py | 296 +--- 23 files changed, 3409 insertions(+), 1585 deletions(-) create mode 100644 pyanaconda/modules/subscription/satellite.py create mode 100644 pyanaconda/modules/subscription/utils.py create mode 100644 tests/unit_tests/pyanaconda_tests/modules/subscription/test_satellite.py create mode 100644 tests/unit_tests/pyanaconda_tests/modules/subscription/utils_test.py diff --git a/anaconda.spec.in b/anaconda.spec.in index c9fd87ccbdb..6723b03f820 100644 --- a/anaconda.spec.in +++ b/anaconda.spec.in @@ -43,7 +43,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 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..0f3f773e9bc 100644 --- a/pyanaconda/modules/common/errors/subscription.py +++ b/pyanaconda/modules/common/errors/subscription.py @@ -32,8 +32,13 @@ class UnregistrationError(AnacondaError): """Unregistration attempt failed.""" pass +@dbus_error("SatelliteProvisioningError", namespace=ANACONDA_NAMESPACE) +class SatelliteProvisioningError(AnacondaError): + """Failed to provision the installation environment for Satellite.""" + pass + -@dbus_error("SubscriptionError", namespace=ANACONDA_NAMESPACE) -class SubscriptionError(AnacondaError): - """Subscription attempt failed.""" +@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/constants.py b/pyanaconda/modules/subscription/constants.py index 8ccd146118f..01a6cb8f4b6 100644 --- a/pyanaconda/modules/subscription/constants.py +++ b/pyanaconda/modules/subscription/constants.py @@ -20,3 +20,5 @@ # name of the RHSM systemd unit RHSM_SERVICE_NAME = "rhsm.service" +# server hostname prefix marking the URL as not Satellite +SERVER_HOSTNAME_NOT_SATELLITE_PREFIX = "not-satellite:" 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 47647a9b186..f3f702c5e0a 100644 --- a/pyanaconda/modules/subscription/runtime.py +++ b/pyanaconda/modules/subscription/runtime.py @@ -20,19 +20,29 @@ import datetime from collections import namedtuple -from dasbus.typing import get_variant, Str +from dasbus.typing import get_variant, Str, 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 import util +from pyanaconda.core.payload import ProxyString +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 + 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 @@ -80,7 +90,7 @@ def _get_connection(self): SystemSubscriptionData = namedtuple("SystemSubscriptionData", - ["attached_subscriptions", "system_purpose_data"]) + ["system_purpose_data"]) class SystemPurposeConfigurationTask(Task): @@ -176,8 +186,15 @@ def run(self): # - all values need to be string variants # - proxy password is stored in SecretData instance and we need to retrieve # its value + # - server host name might have a prefix indicating the given URL is not + # a Satellite URL, drop that prefix before setting the value to RHSM + + # drop the not-satellite prefix, if any + server_hostname = self._request.server_hostname.removeprefix( + SERVER_HOSTNAME_NOT_SATELLITE_PREFIX + ) property_key_map = { - self.CONFIG_KEY_SERVER_HOSTNAME: self._request.server_hostname, + self.CONFIG_KEY_SERVER_HOSTNAME: server_hostname, self.CONFIG_KEY_SERVER_PROXY_HOSTNAME: self._request.server_proxy_hostname, self.CONFIG_KEY_SERVER_PROXY_PORT: str(self._request.server_proxy_port), self.CONFIG_KEY_SERVER_PROXY_USER: self._request.server_proxy_user, @@ -211,7 +228,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 @@ -220,11 +237,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): @@ -234,23 +253,47 @@ 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)) @@ -285,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: @@ -292,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 @@ -310,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): @@ -324,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. @@ -549,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 + util.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..5a3620e72a9 --- /dev/null +++ b/pyanaconda/modules/subscription/satellite.py @@ -0,0 +1,182 @@ +# +# 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.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" + +#TODO: handle also new-style URL once the new form of it is known + + +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 + util.mkdirChain(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 a2e12f57df7..6cc9c4fe4c0 100644 --- a/pyanaconda/modules/subscription/subscription.py +++ b/pyanaconda/modules/subscription/subscription.py @@ -39,8 +39,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 @@ -48,13 +48,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 @@ -85,10 +86,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 ? @@ -103,10 +100,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 @@ -367,32 +374,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. @@ -482,6 +463,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 @@ -505,6 +511,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): @@ -515,6 +543,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 @@ -530,6 +560,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, @@ -556,26 +589,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. @@ -605,7 +618,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 @@ -623,105 +636,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..3eaf1265799 --- /dev/null +++ b/pyanaconda/modules/subscription/utils.py @@ -0,0 +1,39 @@ +# +# Utility functions for network module +# +# Copyright (C) 2021 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 d6d55b1a43c..12718de4151 100644 --- a/pyanaconda/ui/gui/spokes/installation_source.py +++ b/pyanaconda/ui/gui/spokes/installation_source.py @@ -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,21 @@ 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 self._subscription_module: + registered_to_satellite = self._subscription_module.IsRegisteredToSatellite + return registered_to_satellite + @property def status(self): # When CDN is selected as installation source and system @@ -323,6 +338,29 @@ def status(self): source_proxy = self.payload.get_source_proxy() cdn_source = source_proxy.Type == SOURCE_TYPE_CDN +# FIXME: port to modular payload +# +# if cdn_source: +# if self.registered_to_satellite: +# # override the regular CDN source name to make it clear Satellite +# # provided repositories are being used +# return _("Satellite") +# else: +# source_proxy = self.payload.get_source_proxy() +# return source_proxy.Description +# elif threadMgr.get(constants.THREAD_CHECK_SOFTWARE): +# return _("Checking software dependencies...") +# elif not self.ready: +# return _(BASEREPO_SETUP_MESSAGE) +# elif not self.payload.base_repo: +# return _("Error setting up base repository") +# elif self._error: +# return _("Error setting up software source") +# elif not self.payload.is_complete(): +# return _("Nothing selected") +# else: + + if cdn_source and not self.subscribed: source_proxy = self.payload.get_source_proxy() return source_proxy.Description @@ -723,6 +761,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 self._subscription_module: + 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..444e3bbd250 100644 --- a/pyanaconda/ui/gui/spokes/lib/subscription.py +++ b/pyanaconda/ui/gui/spokes/lib/subscription.py @@ -108,129 +108,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 @@ - + - False - CONNECT TO RED HAT + False + CONNECT TO RED HAT - False + False vertical 6 - False + False + - False - 6 - 6 - 6 + False + 6 + 6 + 6 @@ -32,53 +33,54 @@ - False + False 0 - 12 - 48 - 48 - 48 + 12 + 48 + 48 + 48 - False + False vertical 8 True - False + False True - False - False + False + False True - True + True True - False + False center - none + none True - False + False center vertical 4 + True - False + False center center - 4 - 4 + 4 + 4 True - False + False end Authentication right @@ -87,23 +89,23 @@ - 0 - 0 + 0 + 0 True - False + False _Account True - True - False - True + True + False + True True - True + True @@ -116,10 +118,10 @@ Activation _Key True - True - False - True - True + True + False + True + True account_radio_button @@ -131,157 +133,217 @@ - 1 - 0 + 1 + 0 True - False - none - True + False + none + True + True - False + False True - 4 - 4 + 4 + 4 True - False + False start User name - 0 - 0 + 0 + 0 - 250 + 250 True - True + True True - 1 - 0 + 1 + 0 True - False + False start Password - 0 - 1 + 0 + 1 True - True + True True False - + - 1 - 1 + 1 + 1 + + + True + False + none + + + False + Organization ID + + + + + 0 + 2 + + + + + True + False + none + + + False + + + + + + 1 + 2 + + + + + + + + + + + - 1 - 1 + 1 + 1 True - False - none + False + none + True - False + False True - 4 - 4 + 4 + 4 True - False + False start - Organization + Organization ID - 0 - 0 + 0 + 0 True - False + False start Activation Key - 0 - 1 + 0 + 1 True - True + True True - 1 - 0 + 1 + 0 True - True + True True - key1,key2,... + key1,key2,... - 1 - 1 + 1 + 1 + + + + + + + + + + + + + + + - 1 - 2 + 1 + 2 True - False + False end Purpose right @@ -290,115 +352,125 @@ - 0 - 3 + 0 + 3 True - False + False + True - False - 4 - 4 + False + 4 + 4 True - False + False start Usage - 0 - 2 + 0 + 2 True - False + False start Role - 0 - 0 + 0 + 0 True - False + False start SLA - 0 - 1 + 0 + 1 True - False + False - 1 - 0 + 1 + 0 True - False + False - 1 - 1 + 1 + 1 True - False + False - 1 - 2 + 1 + 2 + + + + + + + + + - 1 - 4 + 1 + 4 Set System Purpose True - True - False - True + True + False + True - 1 - 3 + 1 + 3 True - False + False end Insights right @@ -407,24 +479,24 @@ - 0 - 5 + 0 + 5 Connect to Red Hat _Insights True - True - False - Red Hat Insights aims to increase your IT efficiency and speed across hybrid infrastructures by identifying and prioritizing risks, managing vulnerabilities, and compliance, and analyzing costs. For more information, visit the Red Hat Insights information page. - True + True + False + True + Red Hat Insights aims to increase your IT efficiency and speed across hybrid infrastructures by identifying and prioritizing risks, managing vulnerabilities, and compliance, and analyzing costs. For more information, visit the Red Hat Insights information page. True - True + True - 1 - 5 + 1 + 5 @@ -436,6 +508,24 @@ + + + + + + + + + + + + + + + + + + False @@ -446,181 +536,192 @@ True - True + True + True - False - 4 - 4 + False + 4 + 4 Custom base URL True - True - False - True + True + False + True - 1 - 4 + 1 + 4 True - False + False True - True + True - 1 - 5 + 1 + 5 True - False + False True - True + True - 1 - 3 + 1 + 3 - Custom server URL + Satellite URL True - True - False - True + True + False + True - 1 - 2 + 1 + 2 Use HTTP proxy True - True - False - True + True + False + True - 1 - 0 + 1 + 0 True - False + False + True - False - 4 - 4 + False + 4 + 4 True - False + False start Location - 0 - 0 + 0 + 0 True - False + False start User name - 0 - 1 + 0 + 1 True - False + False start Password - 0 - 2 + 0 + 2 True - True + True True - hostname:port + hostname:port - 1 - 0 + 1 + 0 - 250 + 250 True - True + True - 1 - 1 + 1 + 1 True - True + True False - + - 1 - 2 + 1 + 2 + + + + + + + + + - 1 - 1 + 1 + 1 @@ -641,16 +742,34 @@ + + + + + + + + + + + + + + + + + + True - False + False True - False + False Options @@ -671,9 +790,9 @@ True - False - 8 - 8 + False + 8 + 8 The system is currently not registered. @@ -690,10 +809,10 @@ _Register True False - True - True + True + True center - True + True @@ -714,26 +833,26 @@ True - False - 8 + False + 8 vertical 4 True - False + False vertical 4 True - False + False 4 - + True - False - The system has been properly subscribed + False + "" @@ -749,10 +868,10 @@ _Unregister True - True - True + True + True center - True + True @@ -770,18 +889,19 @@ + True - False + False start - 8 - 4 - 8 + 8 + 4 + 8 method_label True - False + False end Method right @@ -790,15 +910,15 @@ - 0 - 0 + 0 + 0 system_purpose_label True - False + False end System Purpose right @@ -807,51 +927,51 @@ - 0 - 1 + 0 + 1 True - False + False start lorem ipsum - 1 - 0 + 1 + 0 True - False + False start lorem ipsum - 1 - 1 + 1 + 1 True - False + False start lorem ipsum - 1 - 2 + 1 + 2 insights_label True - False + False end Insights right @@ -860,32 +980,32 @@ - 0 - 4 + 0 + 4 True - False + False start lorem ipsum - 1 - 4 + 1 + 4 True - False + False start lorem ipsum - 1 - 3 + 1 + 3 @@ -894,6 +1014,21 @@ + + + + + + + + + + + + + + + False @@ -911,10 +1046,10 @@ True - False + False start - 16 - 4 + 16 + 4 No subscriptions have been attached to the system @@ -930,18 +1065,18 @@ True - True + True True True - False - none + False + none True - False - none + False + none diff --git a/pyanaconda/ui/gui/spokes/subscription.py b/pyanaconda/ui/gui/spokes/subscription.py index e8cf44249dc..7b2c273371e 100644 --- a/pyanaconda/ui/gui/spokes/subscription.py +++ b/pyanaconda/ui/gui/spokes/subscription.py @@ -19,6 +19,8 @@ from enum import IntEnum +from dasbus.typing import unwrap_variant + from pyanaconda.flags import flags from pyanaconda.core.threads import thread_manager @@ -34,13 +36,13 @@ from pyanaconda.modules.common.constants.services import SUBSCRIPTION, NETWORK from pyanaconda.modules.common.structures.subscription import SystemPurposeData, \ - SubscriptionRequest, AttachedSubscription + SubscriptionRequest, OrganizationData +from pyanaconda.modules.common.errors.subscription import MultipleOrganizationsError from pyanaconda.modules.common.util import is_module_available -from pyanaconda.modules.common.task import sync_run_task +from pyanaconda.modules.common.task import sync_run_task, async_run_task from pyanaconda.ui.gui.spokes import NormalSpoke -from pyanaconda.ui.gui.spokes.lib.subscription import fill_combobox, \ - populate_attached_subscriptions_listbox +from pyanaconda.ui.gui.spokes.lib.subscription import fill_combobox from pyanaconda.ui.gui.utils import set_password_visibility from pyanaconda.ui.categories.software import SoftwareCategory from pyanaconda.ui.communication import hubQ @@ -315,6 +317,9 @@ def on_activation_key_radio_button_toggled(self, radio): def on_username_entry_changed(self, editable): self.subscription_request.account_username = editable.get_text() self._update_registration_state() + # changes to username can invalidate the organization list, + # so hide it if the username changes + self._disable_org_selection_for_account() def on_password_entry_changed(self, editable): entered_text = editable.get_text() @@ -337,6 +342,11 @@ def on_password_entry_map(self, entry): """ set_password_visibility(entry, False) + def on_select_organization_combobox_changed(self, combobox): + log.debug("Subscription GUI: organization selected for account: %s", + combobox.get_active_id()) + self.subscription_request.account_organization = combobox.get_active_id() + def on_organization_entry_changed(self, editable): self.subscription_request.organization = editable.get_text() self._update_registration_state() @@ -561,6 +571,17 @@ def initialize(self): self._username_entry = self.builder.get_object("username_entry") self._password_entry = self.builder.get_object("password_entry") + # authentication - account - org selection + self._select_organization_label_revealer = self.builder.get_object( + "select_organization_label_revealer" + ) + self._select_organization_combobox_revealer = self.builder.get_object( + "select_organization_combobox_revealer" + ) + self._select_organization_combobox = self.builder.get_object( + "select_organization_combobox" + ) + # authentication - activation key self._activation_key_revealer = self.builder.get_object("activation_key_revealer") self._organization_entry = self.builder.get_object("organization_entry") @@ -626,6 +647,7 @@ def initialize(self): # * the subscription status tab * # # general status + self._subscription_status_label = self.builder.get_object("subscription_status_label") self._method_status_label = self.builder.get_object("method_status_label") self._role_status_label = self.builder.get_object("role_status_label") self._sla_status_label = self.builder.get_object("sla_status_label") @@ -968,18 +990,62 @@ def _subscription_progress_callback(self, phase): def _subscription_error_callback(self, error_message): log.debug("Subscription GUI: registration & attach failed") # store the error message - self.registration_error = error_message + self.registration_error = str(error_message) # even if we fail, we are technically done, # so clear the phase self.registration_phase = None # update registration and subscription parts of the spoke self._update_registration_state() self._update_subscription_state() + # if the error is an instance of multi-org error, + # fetch organization list & enable org selection + # checkbox + if isinstance(error, MultipleOrganizationsError): + task_path = self._subscription_module.RetrieveOrganizationsWithTask() + task_proxy = SUBSCRIPTION.get_proxy(task_path) + async_run_task(task_proxy, self._process_org_list) # re-enable controls, so user can try again self.set_registration_controls_sensitive(True) # notify hub hubQ.send_ready(self.__class__.__name__) + def _process_org_list(self, task_proxy): + """Process org listing for account. + + Called as an async callback of the organization listing runtime task. + + :param task_proxy: a task + """ + # finish the task + task_proxy.Finish() + # process the organization list + org_struct_list = unwrap_variant(task_proxy.GetResult()) + org_list = OrganizationData.from_structure_list(org_struct_list) + # fill the combobox + self._select_organization_combobox.remove_all() + # also add a placeholder and make it the active item so it is visible + self._select_organization_combobox.append("", _("Not Specified")) + self._select_organization_combobox.set_active_id("") + for org in org_list: + self._select_organization_combobox.append(org.id, org.name) + # show the combobox + self._enable_org_selection_for_account() + + def _enable_org_selection_for_account(self): + self._select_organization_label_revealer.set_reveal_child(True) + self._select_organization_combobox_revealer.set_reveal_child(True) + + def _disable_org_selection_for_account(self): + """Disable the org selection combobox. + + And also wipe the last used organization id or else it might be used + for the next registration attempt with a different username, + triggering confusing authetication failures. + """ + self._subscription_request.account_organization = "" + self._select_organization_label_revealer.set_reveal_child(False) + self._select_organization_combobox_revealer.set_reveal_child(False) + def _get_status_message(self): """Get status message describing current spoke state. @@ -1003,7 +1069,10 @@ def _get_status_message(self): elif self.registration_error: return _("Registration failed.") elif self.subscription_attached: - return _("Registered.") + if self._subscription_module.IsRegisteredToSatellite: + return _("Registered to Satellite.") + else: + return _("Registered.") else: return _("Not registered.") @@ -1034,6 +1103,16 @@ def _update_subscription_state(self): Update state of the part of the spoke, that shows data about the currently attached subscriptions. """ + # top level status label + if self._subscription_module.IsRegisteredToSatellite: + self._subscription_status_label.set_text( + _("The system is registered to a Satellite instance.") + ) + else: + self._subscription_status_label.set_text( + _("The system is registered.") + ) + # authentication method if self.authentication_method == AuthenticationMethod.USERNAME_PASSWORD: method_string = _("Registered with account {}").format( @@ -1069,30 +1148,9 @@ def _update_subscription_state(self): insights_string = _("Not connected to Red Hat Insights") self._insights_status_label.set_text(insights_string) - # get attached subscriptions as a list of structs - attached_subscriptions = self._subscription_module.AttachedSubscriptions - # turn the structs to more useful AttachedSubscription instances - attached_subscriptions = AttachedSubscription.from_structure_list(attached_subscriptions) - - # check how many we have & set the subscription status string accordingly - subscription_count = len(attached_subscriptions) - if subscription_count == 0: - subscription_string = _("No subscriptions are attached to the system") - elif subscription_count == 1: - subscription_string = _("1 subscription attached to the system") - else: - subscription_string = _("{} subscriptions attached to the system").format( - subscription_count - ) - + subscription_string = _("Subscribed in Simple Content Access mode.") self._attached_subscriptions_label.set_text(subscription_string) - # populate the attached subscriptions listbox - populate_attached_subscriptions_listbox( - self._subscriptions_listbox, - attached_subscriptions - ) - def _check_connectivity(self): """Check network connectivity is available. diff --git a/pyanaconda/ui/lib/subscription.py b/pyanaconda/ui/lib/subscription.py index b23c73798ad..5ea334ac4bd 100644 --- a/pyanaconda/ui/lib/subscription.py +++ b/pyanaconda/ui/lib/subscription.py @@ -30,8 +30,10 @@ from pyanaconda.modules.common import task from pyanaconda.modules.common.structures.subscription import SubscriptionRequest from pyanaconda.modules.common.util import is_module_available +from pyanaconda.modules.common.structures.secret import SECRET_TYPE_HIDDEN, \ + SECRET_TYPE_TEXT from pyanaconda.modules.common.errors.subscription import RegistrationError, \ - UnregistrationError, SubscriptionError + UnregistrationError, SatelliteProvisioningError, MultipleOrganizationsError from pyanaconda.payload.manager import payloadMgr from pyanaconda.core.threads import thread_manager from pyanaconda.ui.lib.payload import create_source, set_source, tear_down_sources @@ -53,6 +55,21 @@ class SubscriptionPhase(Enum): # temporary methods for Subscription/CDN related source switching +def _tear_down_existing_source(payload): + """Tear down existing payload, so we can set a new one. + + FIXME: does this work with modern payload module ? + + :param payload: Anaconda payload instance + """ + source_proxy = payload.get_source_proxy() + + if source_proxy.Type == SOURCE_TYPE_HDD and source_proxy.Partition: + unmark_protected_device(source_proxy.Partition) + + tear_down_sources(payload.proxy) + + def switch_source(payload, source_type): """Switch to an installation source. @@ -145,6 +162,17 @@ def noop(*args, **kwargs): pass +def dummy_error_callback(error): + """Dummy error reporting function used if no custom callback is set.""" + pass + + +# auth-data-sufficient checks +# +# NOTE: As those are used also in the GUI we can't just put them to a private methods +# in one of the tasks. + + def org_keys_sufficient(subscription_request=None): """Report if sufficient credentials are set for org & keys registration attempt. @@ -188,7 +216,7 @@ def register_and_subscribe(payload, progress_callback=noop, error_callback=noop, :param payload: Anaconda payload instance :param progress_callback: progress callback function, takes one argument, subscription phase :type progress_callback: callable(subscription_phase) - :param error_callback: error callback function, takes one argument, the error message + :param error_callback: error callback function, takes one argument, the error instance :type error_callback: callable(error_message) :param bool restart_payload: should payload restart be attempted if it appears necessary ? @@ -237,70 +265,49 @@ def register_and_subscribe(payload, progress_callback=noop, error_callback=noop, # registered system, as a registration attempt on # an already registered system would fail. if subscription_proxy.IsRegistered: - log.debug("subscription thread: system already registered, unregistering") + log.debug("registration attempt: system already registered, unregistering") progress_callback(SubscriptionPhase.UNREGISTER) task_path = subscription_proxy.UnregisterWithTask() task_proxy = SUBSCRIPTION.get_proxy(task_path) try: task.sync_run_task(task_proxy) except UnregistrationError as e: - log.debug("subscription thread: unregistration failed: %s", e) + log.debug("registration attempt: unregistration failed: %s", e) # Failing to unregister the system is an unrecoverable error, # so we end there. - error_callback(str(e)) + error_callback(e) return log.debug("Subscription GUI: unregistration succeeded") - # Try to register. + # Try to register and subscribe # # If we got this far the system was either not registered # or was unregistered successfully. - log.debug("subscription thread: attempting to register") + log.debug("registration attempt: attempting to register") progress_callback(SubscriptionPhase.REGISTER) - # check authentication method has been set and credentials seem to be - # sufficient (though not necessarily valid) - subscription_request_struct = subscription_proxy.SubscriptionRequest - subscription_request = SubscriptionRequest.from_structure(subscription_request_struct) - task_path = None - if subscription_request.type == SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD: - if username_password_sufficient(): - task_path = subscription_proxy.RegisterUsernamePasswordWithTask() - elif subscription_request.type == SUBSCRIPTION_REQUEST_TYPE_ORG_KEY: - if org_keys_sufficient(): - task_path = subscription_proxy.RegisterOrganizationKeyWithTask() - - if task_path: - task_proxy = SUBSCRIPTION.get_proxy(task_path) - try: - task.sync_run_task(task_proxy) - except RegistrationError as e: - log.debug("subscription thread: registration attempt failed: %s", e) - log.debug("subscription thread: skipping auto attach due to registration error") - error_callback(str(e)) - return - log.debug("subscription thread: registration succeeded") - else: - log.debug("subscription thread: credentials insufficient, skipping registration attempt") - error_callback(_("Registration failed due to insufficient credentials.")) - return - # try to attach subscription - log.debug("subscription thread: attempting to auto attach an entitlement") - progress_callback(SubscriptionPhase.ATTACH_SUBSCRIPTION) - task_path = subscription_proxy.AttachSubscriptionWithTask() + # registration and subscription is handled by a combined tasks that also + # handles Satellite support and attached subscription parsing + task_path = subscription_proxy.RegisterAndSubscribeWithTask() + task_proxy = SUBSCRIPTION.get_proxy(task_path) try: task.sync_run_task(task_proxy) - except SubscriptionError as e: - log.debug("subscription thread: failed to attach subscription: %s", e) - error_callback(str(e)) + except SatelliteProvisioningError as e: + log.debug("registration attempt: Satellite provisioning failed: %s", e) + error_callback(e) + return + except MultipleOrganizationsError as e: + log.debug( + "registration attempt: please specify org id for current account and try again: %s", + e + ) + error_callback(e) + return + except RegistrationError as e: + log.debug("registration attempt: registration attempt failed: %s", e) + error_callback(e) return - - # parse attached subscription data - log.debug("subscription thread: parsing attached subscription data") - task_path = subscription_proxy.ParseAttachedSubscriptionsWithTask() - task_proxy = SUBSCRIPTION.get_proxy(task_path) - task.sync_run_task(task_proxy) # check if the current installation source should be overridden by # the CDN source we can now use @@ -308,7 +315,7 @@ def register_and_subscribe(payload, progress_callback=noop, error_callback=noop, source_proxy = payload.get_source_proxy() if payload.type == PAYLOAD_TYPE_DNF: if source_proxy.Type in SOURCE_TYPES_OVERRIDEN_BY_CDN: - log.debug("subscription thread: overriding current installation source by CDN") + log.debug("registration attempt: overriding current installation source by CDN") switch_source(payload, SOURCE_TYPE_CDN) # If requested, also restart the payload if CDN is the installation source # The CDN either already was the installation source or we just switched to it. @@ -317,11 +324,10 @@ def register_and_subscribe(payload, progress_callback=noop, error_callback=noop, # a source switch. source_proxy = payload.get_source_proxy() if restart_payload and source_proxy.Type == SOURCE_TYPE_CDN: - log.debug("subscription thread: restarting payload after registration") + log.debug("registration attempt: restarting payload after registration") _do_payload_restart(payload) - # and done, report attaching subscription was successful - log.debug("subscription thread: auto attach succeeded") + # and done, report subscription attempt was successful progress_callback(SubscriptionPhase.DONE) @@ -349,7 +355,7 @@ def unregister(payload, overridden_source_type, progress_callback=noop, error_ca subscription_proxy = SUBSCRIPTION.get_proxy() if subscription_proxy.IsRegistered: - log.debug("subscription thread: unregistering the system") + log.debug("registration attempt: unregistering the system") # Make sure to set RHSM config options to be in sync # with the current subscription request in the unlikely # case of someone doing a valid change in the subscription @@ -363,8 +369,8 @@ def unregister(payload, overridden_source_type, progress_callback=noop, error_ca try: task.sync_run_task(task_proxy) except UnregistrationError as e: - log.debug("subscription thread: unregistration failed: %s", e) - error_callback(str(e)) + log.debug("registration attempt: unregistration failed: %s", e) + error_callback(e) return # If the CDN overrode an installation source we should revert that @@ -374,7 +380,7 @@ def unregister(payload, overridden_source_type, progress_callback=noop, error_ca if payload.type == PAYLOAD_TYPE_DNF: if source_proxy.Type == SOURCE_TYPE_CDN and overridden_source_type: log.debug( - "subscription thread: rolling back CDN installation source override" + "registration attempt: rolling back CDN installation source override" ) switch_source(payload, overridden_source_type) switched_source = True @@ -385,12 +391,12 @@ def unregister(payload, overridden_source_type, progress_callback=noop, error_ca # after unregistration, so we need to refresh the Source # and Software spokes if restart_payload and (source_proxy.Type == SOURCE_TYPE_CDN or switched_source): - log.debug("subscription thread: restarting payload after unregistration") + log.debug("registration attempt: restarting payload after unregistration") _do_payload_restart(payload) - log.debug("Subscription GUI: unregistration succeeded") + log.debug("registration attempt: unregistration succeeded") progress_callback(SubscriptionPhase.DONE) else: - log.warning("subscription thread: not registered, so can't unregister") + log.warning("registration attempt: not registered, so can't unregister") progress_callback(SubscriptionPhase.DONE) return diff --git a/tests/unit_tests/pyanaconda_tests/modules/subscription/test_satellite.py b/tests/unit_tests/pyanaconda_tests/modules/subscription/test_satellite.py new file mode 100644 index 00000000000..aced61c82f6 --- /dev/null +++ b/tests/unit_tests/pyanaconda_tests/modules/subscription/test_satellite.py @@ -0,0 +1,231 @@ +# +# Copyright (C) 2021 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. +# +# Red Hat Author(s): Martin Kolman + +import unittest +from unittest.mock import patch +from requests import RequestException + +from pyanaconda.modules.subscription.satellite import download_satellite_provisioning_script, \ + run_satellite_provisioning_script, PROVISIONING_SCRIPT_SUB_PATH +from pyanaconda.core.constants import USER_AGENT, NETWORK_CONNECTION_TIMEOUT + + +class SatelliteLibraryTestCase(unittest.TestCase): + """Test the Satellite provisioning code.""" + # this code at the moment basically just downloads the Satellite provisioning + # script from the Satellite instance with one method, then runs it with the other + + @patch("pyanaconda.core.util.requests_session") + def test_script_download_no_prefix(self, get_session): + """Test the download_satellite_provisioning_script function - no prefix.""" + # mock the Python Request session + session = get_session.return_value.__enter__.return_value + result = session.get.return_value + result.ok = True + result.text = "foo script text" + # run the download method + script_text = download_satellite_provisioning_script("satellite.example.com") + # check script text was returned + assert "foo script text" == script_text + # check the session was called correctly + session.get.assert_called_once_with( + 'http://satellite.example.com' + PROVISIONING_SCRIPT_SUB_PATH, + headers={"user-agent": USER_AGENT}, + proxies={}, + verify=False, + timeout=NETWORK_CONNECTION_TIMEOUT + ) + result.close.assert_called_once() + + @patch("pyanaconda.core.util.requests_session") + def test_script_download_http(self, get_session): + """Test the download_satellite_provisioning_script function - http prefix.""" + # mock the Python Request session + session = get_session.return_value.__enter__.return_value + result = session.get.return_value + result.ok = True + result.text = "foo script text" + # run the download method + script_text = download_satellite_provisioning_script("http://satellite.example.com") + # check script text was returned + assert "foo script text" == script_text + # check the session was called correctly + session.get.assert_called_once_with( + 'http://satellite.example.com' + PROVISIONING_SCRIPT_SUB_PATH, + headers={"user-agent": USER_AGENT}, + proxies={}, + verify=False, + timeout=NETWORK_CONNECTION_TIMEOUT + ) + result.close.assert_called_once() + + @patch("pyanaconda.core.util.requests_session") + def test_script_download_https(self, get_session): + """Test the download_satellite_provisioning_script function - https prefix.""" + # mock the Python Request session + session = get_session.return_value.__enter__.return_value + result = session.get.return_value + result.ok = True + result.text = "foo script text" + # run the download method + script_text = download_satellite_provisioning_script("https://satellite.example.com") + # check script text was returned + assert "foo script text" == script_text + # check the session was called correctly + session.get.assert_called_once_with( + 'https://satellite.example.com' + PROVISIONING_SCRIPT_SUB_PATH, + headers={"user-agent": USER_AGENT}, + proxies={}, + verify=False, + timeout=NETWORK_CONNECTION_TIMEOUT + ) + result.close.assert_called_once() + + @patch("pyanaconda.core.util.requests_session") + def test_script_download_not_ok(self, get_session): + """Test the download_satellite_provisioning_script function - result not ok.""" + # mock the Python Request session + session = get_session.return_value.__enter__.return_value + result = session.get.return_value + result.ok = False + result.text = "foo script text" + # run the download method + script_text = download_satellite_provisioning_script("satellite.example.com") + # if result has ok == False, None should be returned instead of script text + assert script_text is None + # check the session was called correctly + session.get.assert_called_once_with( + 'http://satellite.example.com' + PROVISIONING_SCRIPT_SUB_PATH, + headers={"user-agent": USER_AGENT}, + proxies={}, + verify=False, + timeout=NETWORK_CONNECTION_TIMEOUT + ) + result.close.assert_called_once() + + @patch("pyanaconda.core.util.requests_session") + def test_script_download_exception(self, get_session): + """Test the download_satellite_provisioning_script function - exception.""" + # mock the Python Request session + session = get_session.return_value.__enter__.return_value + session.get.side_effect = RequestException() + # run the download method + script_text = download_satellite_provisioning_script("satellite.example.com") + # if requests throw an exception, None should be returned instead of script text + assert script_text is None + # check the session was called correctly + session.get.assert_called_once_with( + 'http://satellite.example.com' + PROVISIONING_SCRIPT_SUB_PATH, + headers={"user-agent": USER_AGENT}, + proxies={}, + verify=False, + timeout=NETWORK_CONNECTION_TIMEOUT + ) + + def test_run_satellite_provisioning_script_no_script(self): + """Test the run_satellite_provisioning_script function - no script.""" + # if no script is provided, False should be returned + assert run_satellite_provisioning_script(provisioning_script=None) is False + + @patch('pyanaconda.modules.subscription.satellite.util.mkdirChain') + @patch('tempfile.NamedTemporaryFile') + @patch('pyanaconda.core.util.execWithRedirect') + def test_run_satellite_provisioning_script_success(self, exec_with_redirect, + named_tempfile, mkdirChain): + """Test the run_satellite_provisioning_script function - success.""" + # simulate successful script run + exec_with_redirect.return_value = 0 + # get the actual file object + file_object = named_tempfile.return_value.__enter__.return_value + # fake a random name + file_object.name = "totally_random_name" + file_object = named_tempfile.return_value.__enter__.return_value + # successful run should return True + assert run_satellite_provisioning_script(provisioning_script="foo script") is True + # check temp directory was created successfully + mkdirChain.assert_called_once_with("/tmp") + # check the tempfile was created correctly + named_tempfile.assert_called_once_with(mode='w+t', dir='/tmp', prefix='satellite-') + # check the temp file was written out + file_object.write.assert_called_once_with("foo script") + # test the script was executed properly + exec_with_redirect.assert_called_once_with('bash', + argv=['/tmp/totally_random_name'], + root='/') + + @patch("pyanaconda.modules.subscription.satellite.conf") + @patch('pyanaconda.modules.subscription.satellite.util.mkdirChain') + @patch('tempfile.NamedTemporaryFile') + @patch('pyanaconda.core.util.execWithRedirect') + def test_run_satellite_provisioning_script_success_chroot(self, + exec_with_redirect, + named_tempfile, + mkdirChain, + patched_conf): + """Test the run_satellite_provisioning_script function - success in chroot.""" + # mock sysroot + patched_conf.target.system_root = "/foo/sysroot" + # simulate successful script run + exec_with_redirect.return_value = 0 + # get the actual file object + file_object = named_tempfile.return_value.__enter__.return_value + # fake a random name + file_object.name = "totally_random_name" + file_object = named_tempfile.return_value.__enter__.return_value + # successful run should return True + assert run_satellite_provisioning_script(provisioning_script="foo script", + run_on_target_system=True) is True + # check temp directory was created successfully + mkdirChain.assert_called_once_with("/foo/sysroot/tmp") + # check the tempfile was created correctly + named_tempfile.assert_called_once_with(mode='w+t', + dir='/foo/sysroot/tmp', + prefix='satellite-') + # check the temp file was written out + file_object.write.assert_called_once_with("foo script") + # test the script was executed properly + exec_with_redirect.assert_called_once_with('bash', + argv=['/tmp/totally_random_name'], + root='/foo/sysroot') + + @patch('pyanaconda.modules.subscription.satellite.util.mkdirChain') + @patch('tempfile.NamedTemporaryFile') + @patch('pyanaconda.core.util.execWithRedirect') + def test_run_satellite_provisioning_script_failure(self, exec_with_redirect, + named_tempfile, mkdirChain): + """Test the run_satellite_provisioning_script function - failure.""" + # simulate unsuccessful script run + exec_with_redirect.return_value = 1 + # get the actual file object + file_object = named_tempfile.return_value.__enter__.return_value + # fake a random name + file_object.name = "totally_random_name" + file_object = named_tempfile.return_value.__enter__.return_value + # failed run should return False + assert run_satellite_provisioning_script(provisioning_script="foo script") is False + # check temp directory was created successfully + mkdirChain.assert_called_once_with("/tmp") + # check the tempfile was created correctly + named_tempfile.assert_called_once_with(mode='w+t', dir='/tmp', prefix='satellite-') + # check the temp file was written out + file_object.write.assert_called_once_with("foo script") + # test the script was executed properly + exec_with_redirect.assert_called_once_with('bash', + argv=['/tmp/totally_random_name'], + root='/') diff --git a/tests/unit_tests/pyanaconda_tests/modules/subscription/test_subscription.py b/tests/unit_tests/pyanaconda_tests/modules/subscription/test_subscription.py index e7a8e6107b8..d8513c09724 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/subscription/test_subscription.py +++ b/tests/unit_tests/pyanaconda_tests/modules/subscription/test_subscription.py @@ -34,11 +34,10 @@ from pyanaconda.modules.subscription.subscription import SubscriptionService from pyanaconda.modules.subscription.subscription_interface import SubscriptionInterface from pyanaconda.modules.subscription.installation import ConnectToInsightsTask, \ - RestoreRHSMDefaultsTask, TransferSubscriptionTokensTask + RestoreRHSMDefaultsTask, TransferSubscriptionTokensTask, ProvisionTargetSystemForSatelliteTask from pyanaconda.modules.subscription.runtime import SetRHSMConfigurationTask, \ - RegisterWithUsernamePasswordTask, RegisterWithOrganizationKeyTask, \ - UnregisterTask, AttachSubscriptionTask, SystemPurposeConfigurationTask, \ - ParseAttachedSubscriptionsTask, SystemSubscriptionData + RegisterAndSubscribeTask, UnregisterTask, SystemPurposeConfigurationTask, \ + RetrieveOrganizationsTask from tests.unit_tests.pyanaconda_tests import check_kickstart_interface, check_dbus_property, \ PropertiesChangedCallback, patch_dbus_publish_object, check_task_creation_list, \ @@ -198,6 +197,7 @@ def test_subscription_request_data_defaults(self): "type": DEFAULT_SUBSCRIPTION_REQUEST_TYPE, "organization": "", "account-username": "", + "account-organization": "", "server-hostname": "", "rhsm-baseurl": "", "server-proxy-hostname": "", @@ -220,6 +220,7 @@ def test_subscription_request_data_full(self): full_request.type = SUBSCRIPTION_REQUEST_TYPE_ORG_KEY full_request.organization = "123456789" full_request.account_username = "foo_user" + full_request.account_organization = "foo_account_org" full_request.server_hostname = "candlepin.foo.com" full_request.rhsm_baseurl = "cdn.foo.com" full_request.server_proxy_hostname = "proxy.foo.com" @@ -233,6 +234,7 @@ def test_subscription_request_data_full(self): "type": SUBSCRIPTION_REQUEST_TYPE_ORG_KEY, "organization": "123456789", "account-username": "foo_user", + "account-organization": "foo_account_org", "server-hostname": "candlepin.foo.com", "rhsm-baseurl": "cdn.foo.com", "server-proxy-hostname": "proxy.foo.com", @@ -259,6 +261,7 @@ def test_subscription_request_data_full(self): "type": SUBSCRIPTION_REQUEST_TYPE_ORG_KEY, "organization": "123456789", "account-username": "foo_user", + "account-organization": "foo_account_org", "server-hostname": "candlepin.foo.com", "rhsm-baseurl": "cdn.foo.com", "server-proxy-hostname": "proxy.foo.com", @@ -286,6 +289,7 @@ def test_set_subscription_request_password(self): "type": get_variant(Str, SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD), "organization": get_variant(Str, ""), "account-username": get_variant(Str, "foo_user"), + "account-organization": get_variant(Str, ""), "server-hostname": get_variant(Str, ""), "rhsm-baseurl": get_variant(Str, ""), "server-proxy-hostname": get_variant(Str, ""), @@ -326,6 +330,7 @@ def test_set_subscription_request_activation_key(self): "type": get_variant(Str, SUBSCRIPTION_REQUEST_TYPE_ORG_KEY), "organization": get_variant(Str, "123456789"), "account-username": get_variant(Str, ""), + "account-organization": get_variant(Str, ""), "server-hostname": get_variant(Str, ""), "rhsm-baseurl": get_variant(Str, ""), "server-proxy-hostname": get_variant(Str, ""), @@ -367,6 +372,7 @@ def test_set_subscription_request_proxy(self): "type": get_variant(Str, SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD), "organization": get_variant(Str, ""), "account-username": get_variant(Str, ""), + "account-organization": get_variant(Str, ""), "server-hostname": get_variant(Str, ""), "rhsm-baseurl": get_variant(Str, ""), "server-proxy-hostname": get_variant(Str, "proxy.foo.bar"), @@ -402,6 +408,7 @@ def test_set_subscription_request_custom_urls(self): "type": get_variant(Str, SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD), "organization": get_variant(Str, ""), "account-username": get_variant(Str, ""), + "account-organization": get_variant(Str, ""), "server-hostname": get_variant(Str, "candlepin.foo.bar"), "rhsm-baseurl": get_variant(Str, "cdn.foo.bar"), "server-proxy-hostname": get_variant(Str, ""), @@ -450,6 +457,7 @@ def test_set_subscription_request_sensitive_data_wipe(self): "type": get_variant(Str, SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD), "organization": get_variant(Str, ""), "account-username": get_variant(Str, "foo_user"), + "account-organization": get_variant(Str, ""), "server-hostname": get_variant(Str, ""), "rhsm-baseurl": get_variant(Str, ""), "server-proxy-hostname": get_variant(Str, ""), @@ -499,6 +507,7 @@ def test_set_subscription_request_sensitive_data_wipe(self): "type": get_variant(Str, SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD), "organization": get_variant(Str, ""), "account-username": get_variant(Str, "foo_user"), + "account-organization": get_variant(Str, ""), "server-hostname": get_variant(Str, ""), "rhsm-baseurl": get_variant(Str, ""), "server-proxy-hostname": get_variant(Str, ""), @@ -548,6 +557,7 @@ def test_set_subscription_request_sensitive_data_keep(self): "type": get_variant(Str, SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD), "organization": get_variant(Str, ""), "account-username": get_variant(Str, "foo_user"), + "account-organization": get_variant(Str, ""), "server-hostname": get_variant(Str, ""), "rhsm-baseurl": get_variant(Str, ""), "server-proxy-hostname": get_variant(Str, ""), @@ -588,6 +598,7 @@ def test_set_subscription_request_sensitive_data_keep(self): subscription_request = { "type": get_variant(Str, SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD), "account-username": get_variant(Str, "foo_user"), + "account-organization": get_variant(Str, ""), "account-password": get_variant(Structure, {"type": get_variant(Str, "HIDDEN"), @@ -605,6 +616,7 @@ def test_set_subscription_request_sensitive_data_keep(self): "type": get_variant(Str, SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD), "organization": get_variant(Str, ""), "account-username": get_variant(Str, "foo_user"), + "account-organization": get_variant(Str, ""), "server-hostname": get_variant(Str, ""), "rhsm-baseurl": get_variant(Str, ""), "server-proxy-hostname": get_variant(Str, ""), @@ -640,53 +652,6 @@ def test_set_subscription_request_sensitive_data_keep(self): assert internal_request.server_proxy_password.value == \ "foo_proxy_password" - def test_attached_subscription_defaults(self): - """Test the AttachedSubscription DBus structure defaults.""" - - # create empty AttachedSubscription structure - empty_request = AttachedSubscription() - - # compare with expected default values - expected_default_dict = { - "name": get_variant(Str, ""), - "service-level": get_variant(Str, ""), - "sku": get_variant(Str, ""), - "contract": get_variant(Str, ""), - "start-date": get_variant(Str, ""), - "end-date": get_variant(Str, ""), - "consumed-entitlement-count": get_variant(Int, 1) - } - # compare the empty structure with expected default values - assert AttachedSubscription.to_structure(empty_request) == \ - expected_default_dict - - def test_attached_subscription_full(self): - """Test the AttachedSubscription DBus structure that is fully populated.""" - - # create empty AttachedSubscription structure - full_request = AttachedSubscription() - full_request.name = "Foo Bar Beta" - full_request.service_level = "really good" - full_request.sku = "ABCD1234" - full_request.contract = "87654321" - full_request.start_date = "Jan 01, 1970" - full_request.end_date = "Jan 19, 2038" - full_request.consumed_entitlement_count = 9001 - - # compare with expected values - expected_default_dict = { - "name": get_variant(Str, "Foo Bar Beta"), - "service-level": get_variant(Str, "really good"), - "sku": get_variant(Str, "ABCD1234"), - "contract": get_variant(Str, "87654321"), - "start-date": get_variant(Str, "Jan 01, 1970"), - "end-date": get_variant(Str, "Jan 19, 2038"), - "consumed-entitlement-count": get_variant(Int, 9001) - } - # compare the full structure with expected values - assert AttachedSubscription.to_structure(full_request) == \ - expected_default_dict - def test_insights_property(self): """Test the InsightsEnabled property.""" # should be False by default @@ -725,72 +690,51 @@ def custom_setter(value): # at the end the property should be True assert self.subscription_interface.IsRegistered - def test_subscription_attached_property(self): - """Test the IsSubscriptionAttached property.""" + def test_simple_content_access_property(self): + """Test the IsSimpleContentAccessEnabled property.""" # should be false by default - assert not self.subscription_interface.IsSubscriptionAttached + assert not self.subscription_interface.IsSimpleContentAccessEnabled # this property can't be set by client as it is set as the result of # subscription attempts, so we need to call the internal module interface # via a custom setter def custom_setter(value): - self.subscription_module.set_subscription_attached(value) + self.subscription_module.set_simple_content_access_enabled(value) # check the property is True and the signal was emitted # - we use fake setter as there is no public setter self._check_dbus_property( - "IsSubscriptionAttached", + "IsSimpleContentAccessEnabled", True, setter=custom_setter ) # at the end the property should be True - assert self.subscription_interface.IsSubscriptionAttached + assert self.subscription_interface.IsSimpleContentAccessEnabled + + def test_subscription_attached_property(self): + """Test the IsSubscriptionAttached property.""" + # should be false by default + assert not self.subscription_interface.IsSubscriptionAttached - def test_attached_subscriptions_property(self): - """Test the AttachedSubscriptions property.""" - # should return an empty list by default - assert self.subscription_interface.AttachedSubscriptions == [] # this property can't be set by client as it is set as the result of # subscription attempts, so we need to call the internal module interface # via a custom setter - def custom_setter(struct_list): - instance_list = AttachedSubscription.from_structure_list(struct_list) - self.subscription_module.set_attached_subscriptions(instance_list) - - # prepare some testing data - subscription_structs = [ - { - "name": get_variant(Str, "Foo Bar Beta"), - "service-level": get_variant(Str, "very good"), - "sku": get_variant(Str, "ABC1234"), - "contract": get_variant(Str, "12345678"), - "start-date": get_variant(Str, "May 12, 2020"), - "end-date": get_variant(Str, "May 12, 2021"), - "consumed-entitlement-count": get_variant(Int, 1) - }, - { - "name": get_variant(Str, "Foo Bar Beta NG"), - "service-level": get_variant(Str, "even better"), - "sku": get_variant(Str, "ABC4321"), - "contract": get_variant(Str, "87654321"), - "start-date": get_variant(Str, "now"), - "end-date": get_variant(Str, "never"), - "consumed-entitlement-count": get_variant(Int, 1000) - } - ] + def custom_setter(value): + self.subscription_module.set_subscription_attached(value) + # check the property is True and the signal was emitted # - we use fake setter as there is no public setter self._check_dbus_property( - "AttachedSubscriptions", - subscription_structs, + "IsSubscriptionAttached", + True, setter=custom_setter ) - # at the end the property should return the expected list - # of AttachedSubscription structures - assert self.subscription_interface.AttachedSubscriptions == subscription_structs + + # at the end the property should be True + assert self.subscription_interface.IsSubscriptionAttached @patch_dbus_publish_object def test_set_system_purpose_with_task(self, publisher): @@ -979,6 +923,7 @@ def test_set_rhsm_config_with_task(self, publisher): full_request.type = SUBSCRIPTION_REQUEST_TYPE_ORG_KEY full_request.organization = "123456789" full_request.account_username = "foo_user" + full_request.account_organization = "foo_account_organization" full_request.server_hostname = "candlepin.foo.com" full_request.rhsm_baseurl = "cdn.foo.com" full_request.server_proxy_hostname = "proxy.foo.com" @@ -1010,6 +955,7 @@ def test_set_rhsm_config_with_task(self, publisher): "type": SUBSCRIPTION_REQUEST_TYPE_ORG_KEY, "organization": "123456789", "account-username": "foo_user", + "account-organization": "foo_account_organization", "server-hostname": "candlepin.foo.com", "rhsm-baseurl": "cdn.foo.com", "server-proxy-hostname": "proxy.foo.com", @@ -1023,39 +969,41 @@ def test_set_rhsm_config_with_task(self, publisher): assert obj.implementation._rhsm_config_defaults == flat_default_config @patch_dbus_publish_object - def test_register_with_username_password(self, publisher): - """Test RegisterWithUsernamePasswordTask creation.""" - # prepare the module with dummy data - full_request = SubscriptionRequest() - full_request.type = SUBSCRIPTION_REQUEST_TYPE_ORG_KEY - full_request.organization = "123456789" - full_request.account_username = "foo_user" - full_request.server_hostname = "candlepin.foo.com" - full_request.rhsm_baseurl = "cdn.foo.com" - full_request.server_proxy_hostname = "proxy.foo.com" - full_request.server_proxy_port = 9001 - full_request.server_proxy_user = "foo_proxy_user" - full_request.account_password.set_secret("foo_password") - full_request.activation_keys.set_secret(["key1", "key2", "key3"]) - full_request.server_proxy_password.set_secret("foo_proxy_password") - - self.subscription_interface.SubscriptionRequest = \ - SubscriptionRequest.to_structure(full_request) - - # make sure the task gets dummy rhsm register server proxy - observer = Mock() - observer.get_proxy = Mock() - self.subscription_module._rhsm_observer = observer - register_server_proxy = Mock() - observer.get_proxy.return_value = register_server_proxy - + def test_register_and_subscribe(self, publisher): + """Test RegisterAndSubscribeTask creation - org + key.""" + # prepare dummy objects for the task + rhsm_observer = Mock() + self.subscription_module._rhsm_observer = rhsm_observer + subscription_request = Mock() + self.subscription_module._subscription_request = subscription_request + system_purpose_data = Mock() + self.subscription_module._system_purpose_data = system_purpose_data # check the task is created correctly - task_path = self.subscription_interface.RegisterUsernamePasswordWithTask() - obj = check_task_creation(task_path, publisher, RegisterWithUsernamePasswordTask) - # check all the data got propagated to the module correctly - assert obj.implementation._rhsm_register_server_proxy == register_server_proxy - assert obj.implementation._username == "foo_user" - assert obj.implementation._password == "foo_password" + task_path = self.subscription_interface.RegisterAndSubscribeWithTask() + obj = check_task_creation(task_path, publisher, RegisterAndSubscribeTask) + # check all the data got propagated to the task correctly + assert obj.implementation._rhsm_observer == rhsm_observer + assert obj.implementation._subscription_request == subscription_request + assert obj.implementation._system_purpose_data == system_purpose_data + # pylint: disable=comparison-with-callable + assert obj.implementation._registered_callback == self.subscription_module.set_registered + # pylint: disable=comparison-with-callable + assert obj.implementation._registered_to_satellite_callback == \ + self.subscription_module.set_registered_to_satellite + assert obj.implementation._simple_content_access_callback == \ + self.subscription_module.set_simple_content_access_enabled + # pylint: disable=comparison-with-callable + assert obj.implementation._subscription_attached_callback == \ + self.subscription_module.set_subscription_attached + # pylint: disable=comparison-with-callable + assert obj.implementation._subscription_data_callback == \ + self.subscription_module._set_system_subscription_data + # pylint: disable=comparison-with-callable + assert obj.implementation._satellite_script_downloaded_callback == \ + self.subscription_module._set_satellite_provisioning_script + # pylint: disable=comparison-with-callable + assert obj.implementation._config_backup_callback == \ + self.subscription_module._set_pre_satellite_rhsm_conf_snapshot # trigger the succeeded signal obj.implementation.succeeded_signal.emit() # check this set the registered property to True @@ -1105,106 +1053,56 @@ def test_unregister(self, publisher): """Test UnregisterTask creation.""" # simulate system being subscribed self.subscription_module.set_subscription_attached(True) - # make sure the task gets dummy rhsm unregister proxy - observer = Mock() - self.subscription_module._rhsm_observer = observer - rhsm_unregister_proxy = observer.get_proxy.return_value + # make sure the task gets dummy rhsm observer + rhsm_observer = Mock() + self.subscription_module._rhsm_observer = rhsm_observer # check the task is created correctly task_path = self.subscription_interface.UnregisterWithTask() obj = check_task_creation(task_path, publisher, UnregisterTask) # check all the data got propagated to the module correctly - assert obj.implementation._rhsm_unregister_proxy == rhsm_unregister_proxy + assert obj.implementation._rhsm_observer == rhsm_observer + assert obj.implementation._registered_to_satellite is False + assert obj.implementation._rhsm_configuration == {} # trigger the succeeded signal obj.implementation.succeeded_signal.emit() - # check this set the subscription-attached & registered properties to False - assert not self.subscription_interface.IsRegistered - assert not self.subscription_interface.IsSubscriptionAttached + # check unregistration set the subscription-attached, registered + # and SCA properties to False + assert self.subscription_interface.IsRegistered is False + assert self.subscription_interface.IsRegisteredToSatellite is False + assert self.subscription_interface.IsSimpleContentAccessEnabled is False + assert self.subscription_interface.IsSubscriptionAttached is False @patch_dbus_publish_object - def test_attach_subscription(self, publisher): - """Test AttachSubscriptionTask creation.""" - # create the SystemPurposeData structure - system_purpose_data = SystemPurposeData() - system_purpose_data.role = "foo" - system_purpose_data.sla = "bar" - system_purpose_data.usage = "baz" - system_purpose_data.addons = ["a", "b", "c"] - # feed it to the DBus interface - self.subscription_interface.SystemPurposeData = \ - SystemPurposeData.to_structure(system_purpose_data) - - # make sure system is not subscribed - assert not self.subscription_interface.IsSubscriptionAttached - # make sure the task gets dummy rhsm attach proxy - observer = Mock() - self.subscription_module._rhsm_observer = observer - rhsm_attach_proxy = observer.get_proxy.return_value - # check the task is created correctly - task_path = self.subscription_interface.AttachSubscriptionWithTask() - obj = check_task_creation(task_path, publisher, AttachSubscriptionTask) - # check all the data got propagated to the module correctly - assert obj.implementation._rhsm_attach_proxy == rhsm_attach_proxy - assert obj.implementation._sla == "bar" - # trigger the succeeded signal - obj.implementation.succeeded_signal.emit() - # check this set subscription_attached to True - assert self.subscription_interface.IsSubscriptionAttached - - @patch_dbus_publish_object - def test_parse_attached_subscriptions(self, publisher): - """Test ParseAttachedSubscriptionsTask creation.""" - # make sure the task gets dummy rhsm entitlement and syspurpose proxies - observer = Mock() - self.subscription_module._rhsm_observer = observer - rhsm_entitlement_proxy = Mock() - rhsm_syspurpose_proxy = Mock() - # yes, this can be done - observer.get_proxy.side_effect = [rhsm_entitlement_proxy, rhsm_syspurpose_proxy] + def test_unregister_satellite(self, publisher): + """Test UnregisterTask creation - system registered to Satellite.""" + # simulate system being subscribed & registered to Satellite + self.subscription_module.set_subscription_attached(True) + self.subscription_module._set_satellite_provisioning_script("foo script") + self.subscription_module.set_registered_to_satellite(True) + # lets also set SCA as enabled + self.subscription_module.set_simple_content_access_enabled(True) + # simulate RHSM config backup + self.subscription_module._rhsm_conf_before_satellite_provisioning = {"foo.bar": "baz"} + # make sure the task gets dummy rhsm unregister proxy + rhsm_observer = Mock() + self.subscription_module._rhsm_observer = rhsm_observer # check the task is created correctly - task_path = self.subscription_interface.ParseAttachedSubscriptionsWithTask() - obj = check_task_creation(task_path, publisher, ParseAttachedSubscriptionsTask) + task_path = self.subscription_interface.UnregisterWithTask() + obj = check_task_creation(task_path, publisher, UnregisterTask) # check all the data got propagated to the module correctly - assert obj.implementation._rhsm_entitlement_proxy == rhsm_entitlement_proxy - assert obj.implementation._rhsm_syspurpose_proxy == rhsm_syspurpose_proxy - # prepare some testing data - subscription_structs = [ - { - "name": get_variant(Str, "Foo Bar Beta"), - "service-level": get_variant(Str, "very good"), - "sku": get_variant(Str, "ABC1234"), - "contract": get_variant(Str, "12345678"), - "start-date": get_variant(Str, "May 12, 2020"), - "end-date": get_variant(Str, "May 12, 2021"), - "consumed-entitlement-count": get_variant(Int, 1) - }, - { - "name": get_variant(Str, "Foo Bar Beta NG"), - "service-level": get_variant(Str, "even better"), - "sku": get_variant(Str, "ABC4321"), - "contract": get_variant(Str, "87654321"), - "start-date": get_variant(Str, "now"), - "end-date": get_variant(Str, "never"), - "consumed-entitlement-count": get_variant(Int, 1000) - } - ] - system_purpose_struct = { - "role": get_variant(Str, "foo"), - "sla": get_variant(Str, "bar"), - "usage": get_variant(Str, "baz"), - "addons": get_variant(List[Str], ["a", "b", "c"]) - } - # make sure this data is returned by get_result() - return_tuple = SystemSubscriptionData( - attached_subscriptions=AttachedSubscription.from_structure_list(subscription_structs), - system_purpose_data=SystemPurposeData.from_structure(system_purpose_struct) - ) - obj.implementation.get_result = Mock() - obj.implementation.get_result.return_value = return_tuple + assert obj.implementation._registered_to_satellite is True + assert obj.implementation._rhsm_configuration == {"foo.bar": "baz"} + assert obj.implementation._rhsm_observer == rhsm_observer # trigger the succeeded signal obj.implementation.succeeded_signal.emit() - # check this set attached subscription and system purpose as expected - assert self.subscription_interface.AttachedSubscriptions == subscription_structs - assert self.subscription_interface.SystemPurposeData == system_purpose_struct + # check unregistration set the subscription-attached, registered + # and SCA properties to False + assert self.subscription_interface.IsRegistered is False + assert self.subscription_interface.IsRegisteredToSatellite is False + assert self.subscription_interface.IsSimpleContentAccessEnabled is False + assert self.subscription_interface.IsSubscriptionAttached is False + # check the provisioning scrip has been cleared + assert self.subscription_module._satellite_provisioning_script is None @patch_dbus_publish_object def test_install_with_tasks_default(self, publisher): @@ -1219,6 +1117,7 @@ def test_install_with_tasks_default(self, publisher): task_classes = [ RestoreRHSMDefaultsTask, TransferSubscriptionTokensTask, + ProvisionTargetSystemForSatelliteTask, ConnectToInsightsTask ] task_paths = self.subscription_interface.InstallWithTasks() @@ -1232,8 +1131,12 @@ def test_install_with_tasks_default(self, publisher): obj = task_objs[1] assert obj.implementation._transfer_subscription_tokens is False - # ConnectToInsightsTask + # ProvisionTargetSystemForSatelliteTask obj = task_objs[2] + assert obj.implementation._provisioning_script is None + + # ConnectToInsightsTask + obj = task_objs[3] assert obj.implementation._subscription_attached is False assert obj.implementation._connect_to_insights is False @@ -1241,8 +1144,11 @@ def test_install_with_tasks_default(self, publisher): def test_install_with_tasks_configured(self, publisher): """Test install tasks - Subscription module in configured state.""" - self.subscription_interface.InsightsEnabled = True + self.subscription_interface.SetInsightsEnabled(True) self.subscription_module.set_subscription_attached(True) + self.subscription_module.set_registered_to_satellite(True) + self.subscription_module.set_simple_content_access_enabled(True) + self.subscription_module._satellite_provisioning_script = "foo script" # mock the rhsm config proxy observer = Mock() @@ -1254,6 +1160,7 @@ def test_install_with_tasks_configured(self, publisher): task_classes = [ RestoreRHSMDefaultsTask, TransferSubscriptionTokensTask, + ProvisionTargetSystemForSatelliteTask, ConnectToInsightsTask ] task_paths = self.subscription_interface.InstallWithTasks() @@ -1267,8 +1174,12 @@ def test_install_with_tasks_configured(self, publisher): obj = task_objs[1] assert obj.implementation._transfer_subscription_tokens is True - # ConnectToInsightsTask + # ProvisionTargetSystemForSatelliteTask obj = task_objs[2] + assert obj.implementation._provisioning_script == "foo script" + + # ConnectToInsightsTask + obj = task_objs[3] assert obj.implementation._subscription_attached is True assert obj.implementation._connect_to_insights is True @@ -1480,3 +1391,40 @@ def test_ks_no_apply_syspurpose(self, mock_give_purpose): # the SystemPurposeConfigurationTask should have been called, # which calls give_the_system_purpose() mock_give_purpose.assert_not_called() + + @patch_dbus_publish_object + def test_parse_organization_data(self, publisher): + """Test ParseOrganizationDataTask creation.""" + # make sure the task gets dummy rhsm entitlement and syspurpose proxies + + # prepare the module with dummy data + full_request = SubscriptionRequest() + full_request.type = SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD + full_request.organization = "123456789" + full_request.account_username = "foo_user" + full_request.server_hostname = "candlepin.foo.com" + full_request.rhsm_baseurl = "cdn.foo.com" + full_request.server_proxy_hostname = "proxy.foo.com" + full_request.server_proxy_port = 9001 + full_request.server_proxy_user = "foo_proxy_user" + full_request.account_password.set_secret("foo_password") + full_request.activation_keys.set_secret(["key1", "key2", "key3"]) + full_request.server_proxy_password.set_secret("foo_proxy_password") + + self.subscription_interface.SetSubscriptionRequest( + SubscriptionRequest.to_structure(full_request) + ) + # make sure the task gets dummy rhsm register server proxy + observer = Mock() + observer.get_proxy = Mock() + self.subscription_module._rhsm_observer = observer + register_server_proxy = Mock() + observer.get_proxy.return_value = register_server_proxy + + # check the task is created correctly + task_path = self.subscription_interface.RetrieveOrganizationsWithTask() + obj = check_task_creation(task_path, publisher, RetrieveOrganizationsTask) + # check all the data got propagated to the module correctly + assert obj.implementation._rhsm_register_server_proxy == register_server_proxy + assert obj.implementation._username == "foo_user" + assert obj.implementation._password == "foo_password" diff --git a/tests/unit_tests/pyanaconda_tests/modules/subscription/test_subscription_tasks.py b/tests/unit_tests/pyanaconda_tests/modules/subscription/test_subscription_tasks.py index 3ad7f1f4c6a..123974c0762 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/subscription/test_subscription_tasks.py +++ b/tests/unit_tests/pyanaconda_tests/modules/subscription/test_subscription_tasks.py @@ -31,24 +31,29 @@ from pyanaconda.core.path import join_paths from pyanaconda.core.constants import SUBSCRIPTION_REQUEST_TYPE_ORG_KEY, \ - RHSM_SYSPURPOSE_FILE_PATH + RHSM_SYSPURPOSE_FILE_PATH, SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD from pyanaconda.modules.common.errors.installation import InsightsConnectError, \ InsightsClientMissingError, SubscriptionTokenTransferError from pyanaconda.modules.common.errors.subscription import RegistrationError, \ - SubscriptionError + SatelliteProvisioningError, MultipleOrganizationsError from pyanaconda.modules.common.structures.subscription import SystemPurposeData, \ - SubscriptionRequest, AttachedSubscription + SubscriptionRequest, OrganizationData 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_UNREGISTER, \ + RHSM_CONFIG from pyanaconda.modules.subscription.installation import ConnectToInsightsTask, \ - RestoreRHSMDefaultsTask, TransferSubscriptionTokensTask + RestoreRHSMDefaultsTask, TransferSubscriptionTokensTask, ProvisionTargetSystemForSatelliteTask from pyanaconda.modules.subscription.runtime import SetRHSMConfigurationTask, \ RHSMPrivateBus, RegisterWithUsernamePasswordTask, RegisterWithOrganizationKeyTask, \ - UnregisterTask, AttachSubscriptionTask, SystemPurposeConfigurationTask, \ - ParseAttachedSubscriptionsTask + UnregisterTask, SystemPurposeConfigurationTask, \ + ParseSubscriptionDataTask, DownloadSatelliteProvisioningScriptTask, \ + RunSatelliteProvisioningScriptTask, BackupRHSMConfBeforeSatelliteProvisioningTask, \ + RollBackSatelliteProvisioningTask, RegisterAndSubscribeTask, RetrieveOrganizationsTask +from pyanaconda.modules.subscription.constants import SERVER_HOSTNAME_NOT_SATELLITE_PREFIX, \ + RHSM_SERVICE_NAME import gi gi.require_version("Gio", "2.0") @@ -287,6 +292,50 @@ def test_set_rhsm_config_tast_restore_default_value(self): mock_config_proxy.SetAll.assert_called_once_with(expected_dict, "") + def test_set_rhsm_config_task_not_satellite(self): + """Test the SetRHSMConfigurationTask task - not-satellite prefix handling.""" + # if the subscription request has the no-satellite prefix, it should be stripped + # before the server hostname value is sent to RHSM + mock_config_proxy = Mock() + # RHSM config default values + default_config = { + SetRHSMConfigurationTask.CONFIG_KEY_SERVER_HOSTNAME: "server.example.com", + SetRHSMConfigurationTask.CONFIG_KEY_SERVER_PROXY_HOSTNAME: "proxy.example.com", + SetRHSMConfigurationTask.CONFIG_KEY_SERVER_PROXY_PORT: "1000", + SetRHSMConfigurationTask.CONFIG_KEY_SERVER_PROXY_USER: "foo_user", + SetRHSMConfigurationTask.CONFIG_KEY_SERVER_PROXY_PASSWORD: "foo_password", + SetRHSMConfigurationTask.CONFIG_KEY_RHSM_BASEURL: "cdn.example.com", + "key_anaconda_does_not_use_1": "foo1", + "key_anaconda_does_not_use_2": "foo2" + } + # a representative subscription request + request = SubscriptionRequest() + request.type = SUBSCRIPTION_REQUEST_TYPE_ORG_KEY + request.organization = "123456789" + request.account_username = "foo_user" + request.server_hostname = SERVER_HOSTNAME_NOT_SATELLITE_PREFIX + "candlepin.foo.com" + request.rhsm_baseurl = "cdn.foo.com" + request.server_proxy_hostname = "proxy.foo.com" + request.server_proxy_port = 9001 + request.server_proxy_user = "foo_proxy_user" + request.account_password.set_secret("foo_password") + request.activation_keys.set_secret(["key1", "key2", "key3"]) + request.server_proxy_password.set_secret("foo_proxy_password") + # create a task + task = SetRHSMConfigurationTask(rhsm_config_proxy=mock_config_proxy, + rhsm_config_defaults=default_config, + subscription_request=request) + task.run() + # check that we tried to set the expected config keys via the RHSM config DBus API + expected_dict = {"server.hostname": get_variant(Str, "candlepin.foo.com"), + "server.proxy_hostname": get_variant(Str, "proxy.foo.com"), + "server.proxy_port": get_variant(Str, "9001"), + "server.proxy_user": get_variant(Str, "foo_proxy_user"), + "server.proxy_password": get_variant(Str, "foo_proxy_password"), + "rhsm.baseurl": get_variant(Str, "cdn.foo.com")} + + mock_config_proxy.SetAll.assert_called_once_with(expected_dict, "") + class RestoreRHSMDefaultsTaskTestCase(unittest.TestCase): """Test the RestoreRHSMDefaultsTask task.""" @@ -599,16 +648,19 @@ def test_username_password_success(self, private_bus, environ_get): # private register proxy get_proxy = private_bus.return_value.__enter__.return_value.get_proxy private_register_proxy = get_proxy.return_value + # make the Register() method return some JSON data + private_register_proxy.Register.return_value = '{"json":"stuff"}' # instantiate the task and run it task = RegisterWithUsernamePasswordTask(rhsm_register_server_proxy=register_server_proxy, username="foo_user", - password="bar_password") - task.run() + password="bar_password", + organization="foo_org") + assert task.run() == '{"json":"stuff"}' # check the private register proxy Register method was called correctly - private_register_proxy.Register.assert_called_once_with("", + private_register_proxy.Register.assert_called_once_with("foo_org", "foo_user", "bar_password", - {}, + {"enable_content": get_variant(Bool, True)}, {}, "en_US.UTF-8") @@ -627,17 +679,93 @@ def test_username_password_failure(self, private_bus, environ_get): # instantiate the task and run it task = RegisterWithUsernamePasswordTask(rhsm_register_server_proxy=register_server_proxy, username="foo_user", - password="bar_password") + password="bar_password", + organization="foo_org") with pytest.raises(RegistrationError): task.run() # check private register proxy Register method was called correctly - private_register_proxy.Register.assert_called_with("", + private_register_proxy.Register.assert_called_with("foo_org", "foo_user", "bar_password", - {}, + {"enable_content": get_variant(Bool, True)}, {}, "en_US.UTF-8") + @patch("pyanaconda.modules.subscription.runtime.RetrieveOrganizationsTask") + @patch("os.environ.get", return_value="en_US.UTF-8") + @patch("pyanaconda.modules.subscription.runtime.RHSMPrivateBus") + def test_username_password_org_single(self, private_bus, environ_get, retrieve_orgs_task): + """Test the RegisterWithUsernamePasswordTask - parsed single org.""" + # register server proxy + register_server_proxy = Mock() + # private register proxy + get_proxy = private_bus.return_value.__enter__.return_value.get_proxy + private_register_proxy = get_proxy.return_value + # make the Register() method return some JSON data + private_register_proxy.Register.return_value = '{"json":"stuff"}' + # mock the org data retrieval task to return single organization + org_data = [ + { + "key": "foo_org", + "displayName": "Foo Org", + } + ] + org_data_json = json.dumps(org_data) + org_data_list = RetrieveOrganizationsTask._parse_org_data_json(org_data_json) + retrieve_orgs_task.return_value.run.return_value = org_data_list + # prepare mock data callaback as well + # instantiate the task and run it - we set organization to "" to make the task + # fetch organization list + task = RegisterWithUsernamePasswordTask(rhsm_register_server_proxy=register_server_proxy, + username="foo_user", + password="bar_password", + organization="") + # if we get just a single organization, we don't actually have to feed + # it to the RHSM API, its only a problem if there are more than one + assert task.run() == '{"json":"stuff"}' + # check the private register proxy Register method was called correctly + private_register_proxy.Register.assert_called_once_with("", + "foo_user", + "bar_password", + {"enable_content": get_variant(Bool, True)}, + {}, + "en_US.UTF-8") + + @patch("pyanaconda.modules.subscription.runtime.RetrieveOrganizationsTask") + @patch("os.environ.get", return_value="en_US.UTF-8") + def test_username_password_org_multi(self, environ_get, retrieve_orgs_task): + """Test the RegisterWithUsernamePasswordTask - parsed multiple orgs.""" + # register server proxy + register_server_proxy = Mock() + # mock the org data retrieval task to return single organization + org_data = [ + { + "key": "foo_org", + "displayName": "Foo Org", + }, + { + "key": "bar_org", + "displayName": "Bar Org", + }, + { + "key": "baz_org", + "displayName": "Baz Org", + } + ] + org_data_json = json.dumps(org_data) + org_data_list = RetrieveOrganizationsTask._parse_org_data_json(org_data_json) + retrieve_orgs_task.return_value.run.return_value = org_data_list + # instantiate the task and run it - we set organization to "" to make the task + # fetch organization list + task = RegisterWithUsernamePasswordTask(rhsm_register_server_proxy=register_server_proxy, + username="foo_user", + password="bar_password", + organization="") + # if we get more than one organization, we can's automatically decide which one to + # use so we throw an exception to notify the user to pick one and try again + with pytest.raises(MultipleOrganizationsError): + task.run() + @patch("os.environ.get", return_value="en_US.UTF-8") @patch("pyanaconda.modules.subscription.runtime.RHSMPrivateBus") def test_org_key_success(self, private_bus, environ_get): @@ -648,11 +776,13 @@ def test_org_key_success(self, private_bus, environ_get): get_proxy = private_bus.return_value.__enter__.return_value.get_proxy private_register_proxy = get_proxy.return_value private_register_proxy.Register.return_value = True, "" + # make the Register() method return some JSON data + private_register_proxy.RegisterWithActivationKeys.return_value = '{"json":"stuff"}' # instantiate the task and run it task = RegisterWithOrganizationKeyTask(rhsm_register_server_proxy=register_server_proxy, organization="123456789", activation_keys=["foo", "bar", "baz"]) - task.run() + assert task.run() == '{"json":"stuff"}' # check private register proxy RegisterWithActivationKeys method was called correctly private_register_proxy.RegisterWithActivationKeys.assert_called_with( "123456789", @@ -693,157 +823,105 @@ def test_org_key_failure(self, private_bus, environ_get): class UnregisterTaskTestCase(unittest.TestCase): """Test the unregister task.""" + @patch("pyanaconda.modules.subscription.runtime.RollBackSatelliteProvisioningTask") @patch("os.environ.get", return_value="en_US.UTF-8") - def test_unregister_success(self, environ_get): + def test_unregister_success(self, environ_get, roll_back_task): """Test the UnregisterTask - success.""" - # register server proxy - rhsm_unregister_proxy = Mock() + rhsm_observer = Mock() # instantiate the task and run it - task = UnregisterTask(rhsm_unregister_proxy=rhsm_unregister_proxy) + task = UnregisterTask( + rhsm_observer=rhsm_observer, + registered_to_satellite=False, + rhsm_configuration={} + ) task.run() # check the unregister proxy Unregister method was called correctly - rhsm_unregister_proxy.Unregister.assert_called_once_with({}, "en_US.UTF-8") + rhsm_observer.get_proxy.assert_called_once_with(RHSM_UNREGISTER) + # registered_to_satellite is False, so roll back task should not run + roll_back_task.assert_not_called() + roll_back_task.return_value.run.assert_not_called() + @patch("pyanaconda.modules.subscription.runtime.RollBackSatelliteProvisioningTask") @patch("os.environ.get", return_value="en_US.UTF-8") - def test_unregister_failure(self, environ_get): + def test_unregister_failure(self, environ_get, roll_back_task): """Test the UnregisterTask - failure.""" - # register server proxy - rhsm_unregister_proxy = Mock() + rhsm_observer = Mock() + rhsm_unregister_proxy = rhsm_observer.get_proxy.return_value # raise DBusError with error message in JSON json_error = '{"message": "Unregistration failed."}' rhsm_unregister_proxy.Unregister.side_effect = DBusError(json_error) # instantiate the task and run it - task = UnregisterTask(rhsm_unregister_proxy=rhsm_unregister_proxy) + task = UnregisterTask( + rhsm_observer=rhsm_observer, + registered_to_satellite=False, + rhsm_configuration={} + ) with pytest.raises(DBusError): task.run() + # check the RHSM observer was used correctly + rhsm_observer.get_proxy.assert_called_once_with(RHSM_UNREGISTER) # check the unregister proxy Unregister method was called correctly rhsm_unregister_proxy.Unregister.assert_called_once_with({}, "en_US.UTF-8") + # registered_to_satellite is False, so roll back task should not run + roll_back_task.assert_not_called() + roll_back_task.return_value.run.assert_not_called() - -class AttachSubscriptionTaskTestCase(unittest.TestCase): - """Test the subscription task.""" - - @patch("os.environ.get", return_value="en_US.UTF-8") - def test_attach_subscription_task_success(self, environ_get): - """Test the AttachSubscriptionTask - success.""" - rhsm_attach_proxy = Mock() - task = AttachSubscriptionTask(rhsm_attach_proxy=rhsm_attach_proxy, - sla="foo_sla") - task.run() - rhsm_attach_proxy.AutoAttach.assert_called_once_with("foo_sla", - {}, - "en_US.UTF-8") - + @patch("pyanaconda.modules.subscription.runtime.RollBackSatelliteProvisioningTask") @patch("os.environ.get", return_value="en_US.UTF-8") - def test_attach_subscription_task_failure(self, environ_get): - """Test the AttachSubscriptionTask - failure.""" - rhsm_attach_proxy = Mock() + def test_unregister_failure_satellite(self, environ_get, roll_back_task): + """Test the UnregisterTask - unregister failure on Satellite.""" + rhsm_observer = Mock() + rhsm_unregister_proxy = rhsm_observer.get_proxy.return_value # raise DBusError with error message in JSON - json_error = '{"message": "Failed to attach subscription."}' - rhsm_attach_proxy.AutoAttach.side_effect = DBusError(json_error) - task = AttachSubscriptionTask(rhsm_attach_proxy=rhsm_attach_proxy, - sla="foo_sla") - with pytest.raises(SubscriptionError): + json_error = '{"message": "Unregistration failed."}' + rhsm_unregister_proxy.Unregister.side_effect = DBusError(json_error) + # instantiate the task and run it + task = UnregisterTask( + rhsm_observer=rhsm_observer, + registered_to_satellite=True, + rhsm_configuration={} + ) + with pytest.raises(DBusError): task.run() - rhsm_attach_proxy.AutoAttach.assert_called_once_with("foo_sla", - {}, - "en_US.UTF-8") - + # check the RHSM observer was used correctly + rhsm_observer.get_proxy.assert_called_once_with(RHSM_UNREGISTER) + # check the unregister proxy Unregister method was called correctly + rhsm_unregister_proxy.Unregister.assert_called_once_with({}, "en_US.UTF-8") + # registered_to_satellite is True, but unregistration failed before roll back + # could happen + roll_back_task.assert_not_called() + roll_back_task.return_value.run.assert_not_called() -class ParseAttachedSubscriptionsTaskTestCase(unittest.TestCase): - """Test the attached subscription parsing task.""" + @patch("pyanaconda.modules.subscription.runtime.RollBackSatelliteProvisioningTask") + @patch("os.environ.get", return_value="en_US.UTF-8") + def test_unregister_satellite_success(self, environ_get, roll_back_task): + """Test the UnregisterTask - Satellite rollback success.""" + rhsm_observer = Mock() + unregister_proxy = Mock() + config_proxy = Mock() + rhsm_observer.get_proxy.side_effect = [unregister_proxy, config_proxy] + # instantiate the task and run it + mock_rhsm_configuration = {"foo": "bar"} + task = UnregisterTask( + rhsm_observer=rhsm_observer, + registered_to_satellite=True, + rhsm_configuration=mock_rhsm_configuration + ) + task.run() + # check the unregister proxy Unregister method was called correctly + rhsm_observer.get_proxy.assert_has_calls([]) + # registered_to_satellite is False, so roll back task should not run + roll_back_task.assert_called_once_with(rhsm_config_proxy=config_proxy, + rhsm_configuration=mock_rhsm_configuration) + roll_back_task.return_value.run.assert_called_once() - def test_pretty_date(self): - """Test the pretty date method of ParseAttachedSubscriptionsTask.""" - pretty_date_method = ParseAttachedSubscriptionsTask._pretty_date - # try to parse ISO 8601 first - assert pretty_date_method("2015-12-22") == "Dec 22, 2015" - # the method expects short mm/dd/yy dates - assert pretty_date_method("12/22/15") == "Dec 22, 2015" - # returns the input if parsing fails - ambiguous_date = "noon of the twenty first century" - assert pretty_date_method(ambiguous_date) == ambiguous_date - - def test_subscription_json_parsing(self): - """Test the subscription JSON parsing method of ParseAttachedSubscriptionsTask.""" - parse_method = ParseAttachedSubscriptionsTask._parse_subscription_json - # the method should be able to survive the RHSM DBus API returning an empty string, - # as empty list of subscriptions is a lesser issue than crashed installation - assert parse_method("") == [] - # try parsing a json file containing two subscriptions - # - to make this look sane, we write it as a dict that we then convert to JSON - subscription_dict = { - "consumed": [ - { - "subscription_name": "Foo Bar Beta", - "service_level": "very good", - "sku": "ABC1234", - "contract": "12345678", - "starts": "05/12/20", - "ends": "05/12/21", - "quantity_used": "1" - }, - { - "subscription_name": "Foo Bar Beta NG", - "service_level": "even better", - "sku": "ABC4321", - "contract": "87654321", - "starts": "now", - "ends": "never", - "quantity_used": "1000" - }, - { - "subscription_name": "Foo Bar Beta NG", - "service_level": "much wow", - "sku": "ABC5678", - "contract": "12344321", - "starts": "2020-05-12", - "ends": "never", - "quantity_used": "1000" - } - ] - } - subscription_json = json.dumps(subscription_dict) - expected_structs = [ - { - "name": "Foo Bar Beta", - "service-level": "very good", - "sku": "ABC1234", - "contract": "12345678", - "start-date": "May 12, 2020", - "end-date": "May 12, 2021", - "consumed-entitlement-count": 1 - }, - { - "name": "Foo Bar Beta NG", - "service-level": "even better", - "sku": "ABC4321", - "contract": "87654321", - "start-date": "now", - "end-date": "never", - "consumed-entitlement-count": 1000 - }, - { - "name": "Foo Bar Beta NG", - "service-level": "much wow", - "sku": "ABC5678", - "contract": "12344321", - "start-date": "May 12, 2020", - "end-date": "never", - "consumed-entitlement-count": 1000 - } - ] - structs = get_native( - AttachedSubscription.to_structure_list(parse_method(subscription_json)) - ) - # check the content of the AttachedSubscription corresponds to the input JSON, - # including date formatting - assert structs == expected_structs +class ParseSubscriptionDataTaskTestCase(unittest.TestCase): + """Test the attached subscription parsing task.""" def test_system_purpose_json_parsing(self): - """Test the system purpose JSON parsing method of ParseAttachedSubscriptionsTask.""" - parse_method = ParseAttachedSubscriptionsTask._parse_system_purpose_json + """Test the system purpose JSON parsing method of ParseSubscriptionDataTask.""" + parse_method = ParseSubscriptionDataTask._parse_system_purpose_json # the parsing method should be able to survive also getting an empty string expected_struct = { "role": "", @@ -892,33 +970,985 @@ def test_system_purpose_json_parsing(self): @patch("os.environ.get", return_value="en_US.UTF-8") def test_attach_subscription_task_success(self, environ_get): - """Test the ParseAttachedSubscriptionsTask.""" + """Test the ParseSubscriptionDataTask.""" # prepare mock proxies the task is expected to interact with - rhsm_entitlement_proxy = Mock() - rhsm_entitlement_proxy.GetPools.return_value = "foo" rhsm_syspurpose_proxy = Mock() rhsm_syspurpose_proxy.GetSyspurpose.return_value = "bar" - task = ParseAttachedSubscriptionsTask(rhsm_entitlement_proxy=rhsm_entitlement_proxy, - rhsm_syspurpose_proxy=rhsm_syspurpose_proxy) + task = ParseSubscriptionDataTask(rhsm_syspurpose_proxy=rhsm_syspurpose_proxy) # mock the parsing methods - subscription1 = AttachedSubscription() - subscription2 = AttachedSubscription() - task._parse_subscription_json = Mock() - task._parse_subscription_json.return_value = [subscription1, subscription2] system_purpose_data = SystemPurposeData() task._parse_system_purpose_json = Mock() task._parse_system_purpose_json.return_value = system_purpose_data # run the task result = task.run() # check DBus proxies were called as expected - rhsm_entitlement_proxy.GetPools.assert_called_once_with({'pool_subsets': - get_variant(Str, "consumed")}, - {}, - "en_US.UTF-8") rhsm_syspurpose_proxy.GetSyspurpose.assert_called_once_with("en_US.UTF-8") # check the parsing methods were called - task._parse_subscription_json.assert_called_once_with("foo") task._parse_system_purpose_json.assert_called_once_with("bar") # check the result that has been returned is as expected - assert result.attached_subscriptions == [subscription1, subscription2] assert result.system_purpose_data == system_purpose_data + + +class SatelliteTasksTestCase(unittest.TestCase): + """Test the Satellite support tasks.""" + + @patch("pyanaconda.modules.subscription.satellite.download_satellite_provisioning_script") + def test_satellite_provisioning_script_download(self, download_function): + """Test the DownloadSatelliteProvisioningScriptTask.""" + # make the download function return a dummy script text + download_function.return_value = "foo bar" + # create the task and run it + task = DownloadSatelliteProvisioningScriptTask( + satellite_url="satellite.example.com", + proxy_url="proxy.example.com", + ) + assert task.run() == "foo bar" + # check the wrapped download function was called correctly + download_function.assert_called_with( + satellite_url="satellite.example.com", + proxy_url="proxy.example.com", + ) + + @patch("pyanaconda.modules.subscription.satellite.run_satellite_provisioning_script") + def test_satellite_provisioning_run_script(self, run_script_function): + """Test the RunSatelliteProvisioningScriptTask - success.""" + # create the task and run it + task = RunSatelliteProvisioningScriptTask( + provisioning_script="foo bar" + ) + task.run() + # check the wrapped run function was called correctly + run_script_function.assert_called_with( + provisioning_script="foo bar", + run_on_target_system=False + ) + + @patch("pyanaconda.modules.subscription.satellite.run_satellite_provisioning_script") + def test_satellite_provisioning_run_script_failure(self, run_script_function): + """Test the RunSatelliteProvisioningScriptTask - failure.""" + # make sure the run-script function raises the correct error + run_script_function.side_effect = SatelliteProvisioningError() + # create the task and run it + task = RunSatelliteProvisioningScriptTask( + provisioning_script="foo bar" + ) + with pytest.raises(SatelliteProvisioningError): + task.run() + # check the wrapped run function was called correctly + run_script_function.assert_called_with( + provisioning_script="foo bar", + run_on_target_system=False + ) + + def test_rhsm_config_backup(self): + """Test the BackupRHSMConfBeforeSatelliteProvisioningTask.""" + # create mock RHSM config proxy + config_proxy = Mock() + # make it return a DBus struct + config_proxy.GetAll.return_value = {"foo": get_variant(Str, "bar")} + # create the task and run it + task = BackupRHSMConfBeforeSatelliteProvisioningTask( + rhsm_config_proxy=config_proxy + ) + conf_backup = task.run() + # check the RHSM config proxy was called correctly + config_proxy.GetAll.assert_called_once_with("") + # check the DBus struct is correctly converted to a Python dict + assert conf_backup == {"foo": "bar"} + + def test_rhsm_roll_back(self): + """Test the RollBackSatelliteProvisioningTask.""" + # create mock RHSM config proxy + config_proxy = Mock() + # and mock RHSM configuration + rhsm_config = {"foo": "bar"} + # create the task and run it + task = RollBackSatelliteProvisioningTask( + rhsm_config_proxy=config_proxy, + rhsm_configuration=rhsm_config + ) + task.run() + # check the RHSM config proxy was called correctly + config_proxy.SetAll.assert_called_once_with({"foo": get_variant(Str, "bar")}, "") + + @patch("pyanaconda.modules.subscription.satellite.run_satellite_provisioning_script") + def test_provision_target_no_op(self, run_script_function): + """Test the ProvisionTargetSystemForSatelliteTask - no op.""" + # create the task and run it + task = ProvisionTargetSystemForSatelliteTask(provisioning_script=None) + task.run() + # make sure we did not try to provision the system with + # registered_to_satellite == False + run_script_function.assert_not_called() + + @patch("pyanaconda.modules.subscription.satellite.run_satellite_provisioning_script") + def test_provision_target_success(self, run_script_function): + """Test the ProvisionTargetSystemForSatelliteTask - success.""" + # make the run script function return True, indicating success + run_script_function.return_value = True + # create the task and run it + task = ProvisionTargetSystemForSatelliteTask(provisioning_script="foo") + task.run() + # make sure we did try to provision the system with + run_script_function.assert_called_once_with( + provisioning_script="foo", + run_on_target_system=True + ) + + @patch("pyanaconda.modules.subscription.satellite.run_satellite_provisioning_script") + def test_provision_target_failure(self, run_script_function): + """Test the ProvisionTargetSystemForSatelliteTask - failure.""" + # make the run script function return False, indicating failure + run_script_function.return_value = False + # create the task and run it + task = ProvisionTargetSystemForSatelliteTask(provisioning_script="foo") + # check if the correct exception for a failure is raised + with pytest.raises(SatelliteProvisioningError): + task.run() + # make sure we did try to provision the system with + run_script_function.assert_called_once_with( + provisioning_script="foo", + run_on_target_system=True + ) + + +class RegisterandSubscribeTestCase(unittest.TestCase): + """Test the RegisterAndSubscribeTask orchestration task. + + This task does orchestration of many individual tasks, + so it makes sense to have a separate test case for it. + """ + + def test_get_proxy_url(self): + """Test proxy URL generation in RegisterAndSubscribeTask.""" + # no proxy data provided + empty_request = SubscriptionRequest() + assert RegisterAndSubscribeTask._get_proxy_url(empty_request) is None + # proxy data provided in subscription request + request_with_proxy_data = SubscriptionRequest() + request_with_proxy_data.server_proxy_hostname = "proxy.example.com" + request_with_proxy_data.server_proxy_user = "foo_user" + request_with_proxy_data.server_proxy_password.set_secret("foo_password") + request_with_proxy_data.server_proxy_port = 1234 + assert RegisterAndSubscribeTask._get_proxy_url(request_with_proxy_data) == \ + "http://foo_user:foo_password@proxy.example.com:1234" + # one more time without valid port set + request_with_proxy_data = SubscriptionRequest() + request_with_proxy_data.server_proxy_hostname = "proxy.example.com" + request_with_proxy_data.server_proxy_user = "foo_user" + request_with_proxy_data.server_proxy_password.set_secret("foo_password") + request_with_proxy_data.server_proxy_port = -1 + # this should result in the default proxy port 3128 being used + assert RegisterAndSubscribeTask._get_proxy_url(request_with_proxy_data) == \ + "http://foo_user:foo_password@proxy.example.com:3128" + + def test_registration_data_json_parsing(self): + """Test the registration data JSON parsing method of RegisterAndSubscribeTask.""" + parse_method = RegisterAndSubscribeTask._detect_sca_from_registration_data + # the parsing method should be able to survive also getting an empty string + # or even None, returning False + assert not parse_method("") + assert not parse_method(None) + + # registration data without owner key + no_owner_data = { + "foo": "123", + "bar": "456", + "baz": "789" + } + assert not parse_method(json.dumps(no_owner_data)) + + # registration data with owner key but without the necessary + # contentAccessMode key + no_access_mode_data = { + "foo": "123", + "owner": { + "id": "abc", + "key": "admin", + "displayName": "Admin Owner" + }, + "bar": "456", + "baz": "789" + } + assert not parse_method(json.dumps(no_access_mode_data)) + + # registration data with owner key but without the necessary + # contentAccessMode key + no_access_mode_data = { + "foo": "123", + "owner": { + "id": "abc", + "key": "admin", + "displayName": "Admin Owner" + }, + "bar": "456", + "baz": "789" + } + assert not parse_method(json.dumps(no_access_mode_data)) + + # registration data for SCA mode + sca_mode_data = { + "foo": "123", + "owner": { + "id": "abc", + "key": "admin", + "displayName": "Admin Owner", + "contentAccessMode": "org_environment" + }, + "bar": "456", + "baz": "789" + } + assert parse_method(json.dumps(sca_mode_data)) + + # registration data for entitlement mode + entitlement_mode_data = { + "foo": "123", + "owner": { + "id": "abc", + "key": "admin", + "displayName": "Admin Owner", + "contentAccessMode": "entitlement" + }, + "bar": "456", + "baz": "789" + } + assert not parse_method(json.dumps(entitlement_mode_data)) + + # registration data for unknown mode + unknown_mode_data = { + "foo": "123", + "owner": { + "id": "abc", + "key": "admin", + "displayName": "Admin Owner", + "contentAccessMode": "something_else" + }, + "bar": "456", + "baz": "789" + } + assert not parse_method(json.dumps(unknown_mode_data)) + + @patch("pyanaconda.modules.subscription.runtime.DownloadSatelliteProvisioningScriptTask") + def test_provision_system_for_satellite_skip(self, download_task): + """Test Satellite provisioning in RegisterAndSubscribeTask - skip.""" + # create the task and related bits + subscription_request = SubscriptionRequest() + subscription_request.server_hostname = \ + SERVER_HOSTNAME_NOT_SATELLITE_PREFIX + "something.else.example.com" + task = RegisterAndSubscribeTask( + rhsm_observer=Mock(), + subscription_request=subscription_request, + system_purpose_data=Mock(), + registered_callback=Mock(), + registered_to_satellite_callback=Mock(), + simple_content_access_callback=Mock(), + subscription_attached_callback=Mock(), + subscription_data_callback=Mock(), + satellite_script_callback=Mock(), + config_backup_callback=Mock() + ) + # run the provisioning method + task._provision_system_for_satellite() + # detect if provisioning is skipped by checking if the + # DownloadSatelliteProvisioningScriptTask has been instantiated + download_task.assert_not_called() + + @patch("pyanaconda.modules.subscription.runtime.DownloadSatelliteProvisioningScriptTask") + def test_provision_system_for_satellite_download_error(self, download_task): + """Test Satellite provisioning in RegisterAndSubscribeTask - script download error.""" + # create the task and related bits + subscription_request = SubscriptionRequest() + subscription_request.server_hostname = "satellite.example.com" + satellite_script_callback = Mock() + task = RegisterAndSubscribeTask( + rhsm_observer=Mock(), + subscription_request=subscription_request, + system_purpose_data=Mock(), + registered_callback=Mock(), + registered_to_satellite_callback=Mock(), + simple_content_access_callback=Mock(), + subscription_attached_callback=Mock(), + subscription_data_callback=Mock(), + satellite_script_callback=satellite_script_callback, + config_backup_callback=Mock() + ) + # make the mock download task fail + download_task.side_effect = SatelliteProvisioningError() + # run the provisioning method, check correct exception is raised + with pytest.raises(SatelliteProvisioningError): + task._provision_system_for_satellite() + # download task should have been instantiated + download_task.assert_called_once_with( + satellite_url='satellite.example.com', + proxy_url=None) + # but the callback should not have been called due to the failure + satellite_script_callback.assert_not_called() + + @patch("pyanaconda.core.util.restart_service") + @patch("pyanaconda.modules.subscription.runtime.RunSatelliteProvisioningScriptTask") + @patch("pyanaconda.modules.subscription.runtime.DownloadSatelliteProvisioningScriptTask") + @patch("pyanaconda.modules.subscription.runtime.BackupRHSMConfBeforeSatelliteProvisioningTask") + def test_provision_satellite_run_error(self, backup_task, download_task, run_script_task, + restart_service): + """Test Satellite provisioning in RegisterAndSubscribeTask - script run failed.""" + # create the task and related bits + subscription_request = SubscriptionRequest() + subscription_request.server_hostname = "satellite.example.com" + satellite_script_callback = Mock() + task = RegisterAndSubscribeTask( + rhsm_observer=Mock(), + subscription_request=subscription_request, + system_purpose_data=Mock(), + registered_callback=Mock(), + registered_to_satellite_callback=Mock(), + simple_content_access_callback=Mock(), + subscription_attached_callback=Mock(), + subscription_data_callback=Mock(), + satellite_script_callback=satellite_script_callback, + config_backup_callback=Mock() + ) + # make the mock download task return the script from its run() method + download_task.return_value.run.return_value = "foo bar script" + # make the mock run task fail + run_script_task.side_effect = SatelliteProvisioningError() + # make the mock backup task return mock RHSM config dict + backup_task.return_value.run.return_value = {"foo": {"bar": "baz"}} + # run the provisioning method, check correct exception is raised + with pytest.raises(SatelliteProvisioningError): + task._provision_system_for_satellite() + # download task should have been instantiated + download_task.assert_called_once_with( + satellite_url='satellite.example.com', + proxy_url=None) + # download callback should have been called + satellite_script_callback.assert_called_once() + # then the run script task should have been instantiated + run_script_task.assert_called_once_with(provisioning_script="foo bar script") + # but the next call to restart_service should not happen + # due to the exception being raised + restart_service.assert_not_called() + + @patch("pyanaconda.core.util.restart_service") + @patch("pyanaconda.modules.subscription.runtime.RunSatelliteProvisioningScriptTask") + @patch("pyanaconda.modules.subscription.runtime.BackupRHSMConfBeforeSatelliteProvisioningTask") + @patch("pyanaconda.modules.subscription.runtime.DownloadSatelliteProvisioningScriptTask") + def test_provision_success(self, download_task, backup_task, run_script_task, restart_service): + """Test Satellite provisioning in RegisterAndSubscribeTask - success.""" + # this tests a simulated successful end-to-end provisioning run, which contains + # some more bits that have been skipped in the previous tests for complexity: + # - check proxy URL propagates correctly + # - check the backup task (run between download and run tasks) is run correctly + + # create the task and related bits + rhsm_observer = Mock() + subscription_request = SubscriptionRequest() + subscription_request.server_hostname = "satellite.example.com" + subscription_request.server_proxy_hostname = "proxy.example.com" + subscription_request.server_proxy_user = "foo_user" + subscription_request.server_proxy_password.set_secret("foo_password") + subscription_request.server_proxy_port = 1234 + config_backup_callback = Mock() + satellite_script_callback = Mock() + task = RegisterAndSubscribeTask( + rhsm_observer=rhsm_observer, + subscription_request=subscription_request, + system_purpose_data=Mock(), + registered_callback=Mock(), + registered_to_satellite_callback=Mock(), + simple_content_access_callback=Mock(), + subscription_attached_callback=Mock(), + subscription_data_callback=Mock(), + satellite_script_callback=satellite_script_callback, + config_backup_callback=config_backup_callback + ) + # mock the roll back method + task._roll_back_satellite_provisioning = Mock() + # make the mock download task return the script from its run() method + download_task.return_value.run.return_value = "foo bar script" + # make the mock backup task return mock RHSM config dict + backup_task.return_value.run.return_value = {"foo": {"bar": "baz"}} + # run the provisioning method + task._provision_system_for_satellite() + # download task should have been instantiated + download_task.assert_called_once_with( + satellite_url='satellite.example.com', + proxy_url='http://foo_user:foo_password@proxy.example.com:1234') + # download callback should have been called + satellite_script_callback.assert_called_once() + # next we should attempt to backup RHSM configuration, so that + # unregistration can correctly cleanup after a Satellite + # registration attempt + rhsm_observer.get_proxy.assert_called_once_with(RHSM_CONFIG) + backup_task.assert_called_once_with(rhsm_config_proxy=rhsm_observer.get_proxy.return_value) + config_backup_callback.assert_called_once_with({"foo.bar": "baz"}) + # then the run script task should have been instantiated + run_script_task.assert_called_once_with(provisioning_script="foo bar script") + # then the RHSM service restart should happen + restart_service.assert_called_once_with(RHSM_SERVICE_NAME) + # make sure the rollback method was not called + task._roll_back_satellite_provisioning.assert_not_called() + + @patch("pyanaconda.modules.subscription.runtime.RegisterWithUsernamePasswordTask") + def test_registration_error_username_password(self, register_username_task): + """Test RegisterAndSubscribeTask - username + password registration error.""" + # create the task and related bits + rhsm_observer = Mock() + subscription_request = SubscriptionRequest() + subscription_request.type = SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD + subscription_request.account_username = "foo_user" + subscription_request.account_password.set_secret("foo_password") + task = RegisterAndSubscribeTask( + rhsm_observer=rhsm_observer, + subscription_request=subscription_request, + system_purpose_data=Mock(), + registered_callback=Mock(), + registered_to_satellite_callback=Mock(), + simple_content_access_callback=Mock(), + subscription_attached_callback=Mock(), + subscription_data_callback=Mock(), + satellite_script_callback=Mock(), + config_backup_callback=Mock() + ) + # make the register task throw an exception + register_username_task.return_value.run_with_signals.side_effect = RegistrationError() + # check the exception is raised as expected + with pytest.raises(RegistrationError): + task.run() + # check the register task was properly instantiated + register_username_task.assert_called_once_with( + rhsm_register_server_proxy=rhsm_observer.get_proxy.return_value, + username='foo_user', + password='foo_password', + organization='' + ) + # check the register task has been run + register_username_task.return_value.run_with_signals.assert_called_once() + + @patch("pyanaconda.modules.subscription.runtime.RegisterWithOrganizationKeyTask") + def test_registration_error_org_key(self, register_org_task): + """Test RegisterAndSubscribeTask - org + key registration error.""" + # create the task and related bits + rhsm_observer = Mock() + subscription_request = SubscriptionRequest() + subscription_request.type = SUBSCRIPTION_REQUEST_TYPE_ORG_KEY + subscription_request.organization = "foo_org" + subscription_request.activation_keys.set_secret(["key1", "key2", "key3"]) + task = RegisterAndSubscribeTask( + rhsm_observer=rhsm_observer, + subscription_request=subscription_request, + system_purpose_data=Mock(), + registered_callback=Mock(), + registered_to_satellite_callback=Mock(), + simple_content_access_callback=Mock(), + subscription_attached_callback=Mock(), + subscription_data_callback=Mock(), + satellite_script_callback=Mock(), + config_backup_callback=Mock() + ) + # make the register task throw an exception + register_org_task.return_value.run_with_signals.side_effect = RegistrationError() + # check the exception is raised as expected + with pytest.raises(RegistrationError): + task.run() + # check the register task was properly instantiated + register_org_task.assert_called_once_with( + rhsm_register_server_proxy=rhsm_observer.get_proxy.return_value, + organization='foo_org', + activation_keys=['key1', 'key2', 'key3'] + ) + # check the register task has been run + register_org_task.return_value.run_with_signals.assert_called_once() + + @patch("pyanaconda.modules.subscription.runtime.ParseAttachedSubscriptionsTask") + @patch("pyanaconda.modules.subscription.runtime.RegisterWithOrganizationKeyTask") + def test_registration_and_subscribe(self, register_task, parse_task): + """Test RegisterAndSubscribeTask - success.""" + # create the task and related bits + rhsm_observer = Mock() + rhsm_register_server = Mock() + rhsm_entitlement = Mock() + rhsm_syspurpose = Mock() + rhsm_observer.get_proxy.side_effect = [ + rhsm_register_server, rhsm_entitlement, rhsm_syspurpose + ] + subscription_request = SubscriptionRequest() + subscription_request.type = SUBSCRIPTION_REQUEST_TYPE_ORG_KEY + subscription_request.organization = "foo_org" + subscription_request.activation_keys.set_secret(["key1", "key2", "key3"]) + system_purpose_data = SystemPurposeData() + system_purpose_data.sla = "foo_sla" + subscription_attached_callback = Mock() + task = RegisterAndSubscribeTask( + rhsm_observer=rhsm_observer, + subscription_request=subscription_request, + system_purpose_data=system_purpose_data, + registered_callback=Mock(), + registered_to_satellite_callback=Mock(), + simple_content_access_callback=Mock(), + subscription_attached_callback=subscription_attached_callback, + subscription_data_callback=Mock(), + satellite_script_callback=Mock(), + config_backup_callback=Mock() + ) + # mock the Satellite provisioning method + task._provision_system_for_satellite = Mock() + # run the main task + task.run() + # check satellite provisioning was not attempted + task._provision_system_for_satellite.assert_not_called() + # check the register task was properly instantiated and run + register_task.assert_called_once_with( + rhsm_register_server_proxy=rhsm_register_server, + organization='foo_org', + activation_keys=['key1', 'key2', 'key3'] + ) + register_task.return_value.run_with_signals.assert_called_once() + # also check the callback was called correctly + subscription_attached_callback.assert_called_once_with(True) + # check the subscription parsing task has been properly instantiated and run + parse_task.assert_called_once_with( + rhsm_entitlement_proxy=rhsm_entitlement, + rhsm_syspurpose_proxy=rhsm_syspurpose + ) + parse_task.return_value.run_with_signals.assert_called_once() + + @patch("pyanaconda.modules.subscription.runtime.ParseAttachedSubscriptionsTask") + @patch("pyanaconda.modules.subscription.runtime.RegisterWithOrganizationKeyTask") + def test_registration_and_subscribe_satellite(self, register_task, parse_task): + """Test RegisterAndSubscribeTask - success with satellite provisioning.""" + # create the task and related bits + rhsm_observer = Mock() + rhsm_register_server = Mock() + rhsm_entitlement = Mock() + rhsm_syspurpose = Mock() + rhsm_observer.get_proxy.side_effect = [ + rhsm_register_server, rhsm_entitlement, rhsm_syspurpose + ] + subscription_request = SubscriptionRequest() + subscription_request.type = SUBSCRIPTION_REQUEST_TYPE_ORG_KEY + subscription_request.organization = "foo_org" + subscription_request.activation_keys.set_secret(["key1", "key2", "key3"]) + subscription_request.server_hostname = "satellite.example.com" + system_purpose_data = SystemPurposeData() + system_purpose_data.sla = "foo_sla" + subscription_attached_callback = Mock() + task = RegisterAndSubscribeTask( + rhsm_observer=rhsm_observer, + subscription_request=subscription_request, + system_purpose_data=system_purpose_data, + registered_callback=Mock(), + registered_to_satellite_callback=Mock(), + simple_content_access_callback=Mock(), + subscription_attached_callback=subscription_attached_callback, + subscription_data_callback=Mock(), + satellite_script_callback=Mock(), + config_backup_callback=Mock() + ) + # mock the Satellite provisioning method + task._provision_system_for_satellite = Mock() + # run the main task + task.run() + # check satellite provisioning was attempted + task._provision_system_for_satellite.assert_called_once_with() + # check the register task was properly instantiated and run + register_task.assert_called_once_with( + rhsm_register_server_proxy=rhsm_register_server, + organization='foo_org', + activation_keys=['key1', 'key2', 'key3'] + ) + register_task.return_value.run_with_signals.assert_called_once() + # also check the callback was called correctly + subscription_attached_callback.assert_called_once_with(True) + # check the subscription parsing task has been properly instantiated and run + parse_task.assert_called_once_with( + rhsm_entitlement_proxy=rhsm_entitlement, + rhsm_syspurpose_proxy=rhsm_syspurpose + ) + parse_task.return_value.run_with_signals.assert_called_once() + + @patch("pyanaconda.modules.subscription.runtime.ParseAttachedSubscriptionsTask") + @patch("pyanaconda.modules.subscription.runtime.RegisterWithOrganizationKeyTask") + def test_registration_failure_satellite(self, register_task, parse_task): + """Test RegisterAndSubscribeTask - registration failure with satellite provisioning.""" + # create the task and related bits + rhsm_observer = Mock() + rhsm_register_server = Mock() + rhsm_entitlement = Mock() + rhsm_syspurpose = Mock() + rhsm_observer.get_proxy.side_effect = [ + rhsm_register_server, rhsm_entitlement, rhsm_syspurpose + ] + subscription_request = SubscriptionRequest() + subscription_request.type = SUBSCRIPTION_REQUEST_TYPE_ORG_KEY + subscription_request.organization = "foo_org" + subscription_request.activation_keys.set_secret(["key1", "key2", "key3"]) + subscription_request.server_hostname = "satellite.example.com" + system_purpose_data = SystemPurposeData() + system_purpose_data.sla = "foo_sla" + subscription_attached_callback = Mock() + task = RegisterAndSubscribeTask( + rhsm_observer=rhsm_observer, + subscription_request=subscription_request, + system_purpose_data=system_purpose_data, + registered_callback=Mock(), + registered_to_satellite_callback=Mock(), + simple_content_access_callback=Mock(), + subscription_attached_callback=subscription_attached_callback, + subscription_data_callback=Mock(), + satellite_script_callback=Mock(), + config_backup_callback=Mock() + ) + # mock the Satellite provisioning method + task._provision_system_for_satellite = Mock() + # mock the Satellite rollback method + task._roll_back_satellite_provisioning = Mock() + # make the register task throw an exception + register_task.return_value.run_with_signals.side_effect = RegistrationError() + # run the main task, epxect registration error + with pytest.raises(RegistrationError): + task.run() + # check satellite provisioning was attempted + task._provision_system_for_satellite.assert_called_once_with() + # check the register task was properly instantiated and run + register_task.assert_called_once_with( + rhsm_register_server_proxy=rhsm_register_server, + organization='foo_org', + activation_keys=['key1', 'key2', 'key3'] + ) + register_task.return_value.run_with_signals.assert_called_once() + # also check the callback was not called + subscription_attached_callback.assert_not_called() + # check the subscription parsing task has not been instantiated and run + parse_task.assert_not_called() + parse_task.return_value.run_with_signals.assert_not_called() + # the Satellite provisioning rollback should have been called due to the failure + task._roll_back_satellite_provisioning.assert_called_once() + + +class RetrieveOrganizationsTaskTestCase(unittest.TestCase): + """Test the organization data parsing task.""" + + def test_org_data_json_parsing(self): + """Test the organization data JSON parsing method of RetrieveOrganizationsTask.""" + parse_method = RetrieveOrganizationsTask._parse_org_data_json + # the parsing method should be able to survive also getting an empty string, + # resulting in an empty list being returned + struct = get_native( + OrganizationData.to_structure_list(parse_method("")) + ) + assert struct == [] + + # try data with single organization + single_org_data = [ + { + "key": "123abc", + "displayName": "Foo Org", + "contentAccessMode": "entitlement" + } + ] + single_org_data_json = json.dumps(single_org_data) + expected_struct_list = [ + { + "id": "123abc", + "name": "Foo Org", + } + ] + + struct = get_native( + OrganizationData.to_structure_list(parse_method(single_org_data_json)) + ) + assert struct == expected_struct_list + + # try multiple organizations: + # - one in entitlement (classic) mode + # - one in Simple Content Access mode + # - one in unknown unexpected mode (should fall back to entitlement/classic mode) + multiple_org_data = [ + { + "key": "123a", + "displayName": "Foo Org", + "contentAccessMode": "entitlement" + }, + { + "key": "123b", + "displayName": "Bar Org", + "contentAccessMode": "org_environment" + }, + { + "key": "123c", + "displayName": "Baz Org", + "contentAccessMode": "something_else" + } + ] + multiple_org_data_json = json.dumps(multiple_org_data) + expected_struct_list = [ + { + "id": "123a", + "name": "Foo Org", + }, + { + "id": "123b", + "name": "Bar Org", + }, + { + "id": "123c", + "name": "Baz Org", + } + ] + structs = get_native( + OrganizationData.to_structure_list(parse_method(multiple_org_data_json)) + ) + assert structs == expected_struct_list + + @patch("os.environ.get", return_value="en_US.UTF-8") + @patch("pyanaconda.modules.subscription.runtime.RHSMPrivateBus") + def test_get_org_data(self, private_bus, environ_get): + """Test the RetrieveOrganizationsTask.""" + # register server proxy + register_server_proxy = Mock() + # private register proxy + get_proxy = private_bus.return_value.__enter__.return_value.get_proxy + private_register_proxy = get_proxy.return_value + # mock the GetOrgs JSON output + multiple_org_data = [ + { + "key": "123a", + "displayName": "Foo Org", + "contentAccessMode": "entitlement" + }, + { + "key": "123b", + "displayName": "Bar Org", + "contentAccessMode": "org_environment" + }, + { + "key": "123c", + "displayName": "Baz Org", + "contentAccessMode": "something_else" + } + ] + multiple_org_data_json = json.dumps(multiple_org_data) + private_register_proxy.GetOrgs.return_value = multiple_org_data_json + + # instantiate the task and run it + task = RetrieveOrganizationsTask(rhsm_register_server_proxy=register_server_proxy, + username="foo_user", + password="bar_password") + org_data_structs = task.run() + # check the structs based on the JSON data look as expected + expected_struct_list = [ + { + "id": "123a", + "name": "Foo Org", + }, + { + "id": "123b", + "name": "Bar Org", + }, + { + "id": "123c", + "name": "Baz Org", + } + ] + structs = get_native( + OrganizationData.to_structure_list(org_data_structs) + ) + assert structs == expected_struct_list + + # check the private register proxy Register method was called correctly + private_register_proxy.GetOrgs.assert_called_once_with("foo_user", + "bar_password", + {}, + "en_US.UTF-8") + + @patch("os.environ.get", return_value="en_US.UTF-8") + @patch("pyanaconda.modules.subscription.runtime.RHSMPrivateBus") + def test_get_org_data_cached(self, private_bus, environ_get): + """Test the RetrieveOrganizationsTask - return cached data on error.""" + # register server proxy + register_server_proxy = Mock() + # private register proxy + get_proxy = private_bus.return_value.__enter__.return_value.get_proxy + private_register_proxy = get_proxy.return_value + # simulate GetOrgs call failure + private_register_proxy.GetOrgs.side_effect = DBusError("org listing failed") + # create some dummy cached data + cached_structs_list = [ + { + "id": get_variant(Str, "123a cached"), + "name": get_variant(Str, "Foo Org cached"), + }, + { + "id": get_variant(Str, "123b cached"), + "name": get_variant(Str, "Bar Org cached"), + }, + { + "id": get_variant(Str, "123c cached"), + "name": get_variant(Str, "Baz Org cached"), + } + ] + cached_structs = OrganizationData.from_structure_list(cached_structs_list) + RetrieveOrganizationsTask._org_data_list_cache = cached_structs + + # instantiate the task and run it with cached data + task = RetrieveOrganizationsTask(rhsm_register_server_proxy=register_server_proxy, + username="foo_user", + password="bar_password") + org_data_structs = task.run() + # check the returned structs are based on the cache data, not the + # JSON data the mock-API would return + expected_struct_list = [ + { + "id": "123a cached", + "name": "Foo Org cached", + }, + { + "id": "123b cached", + "name": "Bar Org cached", + }, + { + "id": "123c cached", + "name": "Baz Org cached", + } + ] + structs = get_native( + OrganizationData.to_structure_list(org_data_structs) + ) + assert structs == expected_struct_list + + # check the private register proxy Register method was *not* called + # as all data should come from the cache, if provided, with *no* + # DBus API access + private_register_proxy.GetOrgs.assert_called_once_with( + 'foo_user', 'bar_password', {}, 'en_US.UTF-8' + ) + + @patch("os.environ.get", return_value="en_US.UTF-8") + @patch("pyanaconda.modules.subscription.runtime.RHSMPrivateBus") + def test_get_org_data_ignore_cache(self, private_bus, environ_get): + """Test the RetrieveOrganizationsTask - do not use cache on success.""" + # register server proxy + register_server_proxy = Mock() + # private register proxy + get_proxy = private_bus.return_value.__enter__.return_value.get_proxy + private_register_proxy = get_proxy.return_value + # mock the GetOrgs JSON output + multiple_org_data = [ + { + "key": "123a", + "displayName": "Foo Org", + "contentAccessMode": "entitlement" + }, + { + "key": "123b", + "displayName": "Bar Org", + "contentAccessMode": "org_environment" + }, + { + "key": "123c", + "displayName": "Baz Org", + "contentAccessMode": "something_else" + } + ] + multiple_org_data_json = json.dumps(multiple_org_data) + private_register_proxy.GetOrgs.return_value = multiple_org_data_json + # create some dummy cached data + cached_structs_list = [ + { + "id": get_variant(Str, "123a cached"), + "name": get_variant(Str, "Foo Org cached"), + }, + { + "id": get_variant(Str, "123b cached"), + "name": get_variant(Str, "Bar Org cached"), + }, + { + "id": get_variant(Str, "123c cached"), + "name": get_variant(Str, "Baz Org cached"), + } + ] + cached_structs = OrganizationData.from_structure_list(cached_structs_list) + RetrieveOrganizationsTask._org_data_list_cache = cached_structs + + # instantiate the task and run it with cached data + task = RetrieveOrganizationsTask(rhsm_register_server_proxy=register_server_proxy, + username="foo_user", + password="bar_password") + org_data_structs = task.run() + + # check the structs based on the GetOrgs returned JSON data look as expected + expected_struct_list = [ + { + "id": "123a", + "name": "Foo Org", + }, + { + "id": "123b", + "name": "Bar Org", + }, + { + "id": "123c", + "name": "Baz Org", + } + ] + structs = get_native( + OrganizationData.to_structure_list(org_data_structs) + ) + assert structs == expected_struct_list + + # check the private register proxy Register method was *not* called + # as all data should come from the cache, if provided, with *no* + # DBus API access + private_register_proxy.GetOrgs.assert_called_once_with( + 'foo_user', 'bar_password', {}, 'en_US.UTF-8' + ) + + @patch("os.environ.get", return_value="en_US.UTF-8") + @patch("pyanaconda.modules.subscription.runtime.RHSMPrivateBus") + def test_get_org_data_cache_reset(self, private_bus, environ_get): + """Test the RetrieveOrganizationsTask - test cache reset.""" + # register server proxy + register_server_proxy = Mock() + # private register proxy + get_proxy = private_bus.return_value.__enter__.return_value.get_proxy + private_register_proxy = get_proxy.return_value + # simulate GetOrgs call failure + private_register_proxy.GetOrgs.side_effect = DBusError("org listing failed") + # create some dummy cached data + cached_structs_list = [ + { + "id": get_variant(Str, "123a cached"), + "name": get_variant(Str, "Foo Org cached"), + }, + { + "id": get_variant(Str, "123b cached"), + "name": get_variant(Str, "Bar Org cached"), + }, + { + "id": get_variant(Str, "123c cached"), + "name": get_variant(Str, "Baz Org cached"), + } + ] + cached_structs = OrganizationData.from_structure_list(cached_structs_list) + RetrieveOrganizationsTask._org_data_list_cache = cached_structs + + # instantiate the task and run it with cached data + task = RetrieveOrganizationsTask(rhsm_register_server_proxy=register_server_proxy, + username="foo_user", + password="bar_password", + reset_cache=True) + org_data_structs = task.run() + # we dropped the cache and the GetOrgs() call failed, so we return the + # contents of the empty cache + expected_struct_list = [] + structs = get_native( + OrganizationData.to_structure_list(org_data_structs) + ) + assert structs == expected_struct_list + + # check the private register proxy Register method was *not* called + # as all data should come from the cache, if provided, with *no* + # DBus API access + private_register_proxy.GetOrgs.assert_called_once_with( + 'foo_user', 'bar_password', {}, 'en_US.UTF-8' + ) diff --git a/tests/unit_tests/pyanaconda_tests/modules/subscription/utils_test.py b/tests/unit_tests/pyanaconda_tests/modules/subscription/utils_test.py new file mode 100644 index 00000000000..2165a78f90e --- /dev/null +++ b/tests/unit_tests/pyanaconda_tests/modules/subscription/utils_test.py @@ -0,0 +1,35 @@ +# +# Copyright (C) 2021 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. +# +# Red Hat Author(s): Martin Kolman +# + +import unittest + +from pyanaconda.modules.subscription.utils import flatten_rhsm_nested_dict + + +class FlattenRHSMNestedDictTestCase(unittest.TestCase): + """Test the RHSM nested dict flattening function.""" + + def test_empty_dict(self): + """Test the flattening function can handle an empty dict being passed.""" + assert flatten_rhsm_nested_dict({}) == {} + + def test_nested_dict(self): + """Test the flattening function can handle a nested dict being passed.""" + assert flatten_rhsm_nested_dict({"foo": {"bar": "baz"}}) == {"foo.bar": "baz"} diff --git a/tests/unit_tests/pyanaconda_tests/test_subscription_helpers.py b/tests/unit_tests/pyanaconda_tests/test_subscription_helpers.py index 606b780ba2e..5ef7e303af7 100644 --- a/tests/unit_tests/pyanaconda_tests/test_subscription_helpers.py +++ b/tests/unit_tests/pyanaconda_tests/test_subscription_helpers.py @@ -21,7 +21,7 @@ import tempfile import unittest -from unittest.mock import patch, Mock, call +from unittest.mock import patch, Mock, MagicMock, call from dasbus.typing import * # pylint: disable=wildcard-import @@ -33,14 +33,16 @@ SUBSCRIPTION_REQUEST_TYPE_ORG_KEY, SOURCE_TYPE_CLOSEST_MIRROR, \ SOURCE_TYPE_CDN, SOURCE_TYPE_CDROM, PAYLOAD_TYPE_DNF, PAYLOAD_TYPE_RPM_OSTREE, \ SOURCE_TYPE_URL -from pyanaconda.core.subscription import check_system_purpose_set -from pyanaconda.modules.common.constants.services import BOSS, SUBSCRIPTION + from pyanaconda.modules.common.errors.subscription import UnregistrationError, \ - RegistrationError, SubscriptionError + RegistrationError, SatelliteProvisioningError from pyanaconda.modules.common.structures.subscription import SubscriptionRequest + +from pyanaconda.core.subscription import check_system_purpose_set + from pyanaconda.ui.lib.subscription import SubscriptionPhase, \ register_and_subscribe, unregister, org_keys_sufficient, \ - username_password_sufficient, check_cdn_is_installation_source, is_cdn_registration_required + username_password_sufficient, check_cdn_is_installation_source class CheckSystemPurposeSetTestCase(unittest.TestCase): @@ -215,18 +217,14 @@ def test_username_password_sufficient_direct_request(self): @patch("pyanaconda.modules.common.task.sync_run_task") @patch("pyanaconda.core.threads.thread_manager.wait") @patch("pyanaconda.modules.common.constants.services.SUBSCRIPTION.get_proxy") - def test_register_org_key(self, get_proxy, thread_mgr_wait, run_task, switch_source): - """Test the register_and_subscribe() helper method - org & key.""" + def test_register_success(self, get_proxy, thread_mgr_wait, run_task, switch_source): + """Test the register_and_subscribe() helper method - success.""" payload = Mock() - source_proxy = payload.get_source_proxy.return_value - source_proxy.Type = SOURCE_TYPE_CLOSEST_MIRROR progress_callback = Mock() error_callback = Mock() subscription_proxy = get_proxy.return_value # simulate the system not being registered subscription_proxy.IsRegistered = False - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.KEY_REQUEST # run the function register_and_subscribe(payload=payload, progress_callback=progress_callback, @@ -236,16 +234,13 @@ def test_register_org_key(self, get_proxy, thread_mgr_wait, run_task, switch_sou # system was no registered, so no unregistration phase progress_callback.assert_has_calls( [call(SubscriptionPhase.REGISTER), - call(SubscriptionPhase.ATTACH_SUBSCRIPTION), call(SubscriptionPhase.DONE)] ) # we were successful, so no error callback calls error_callback.assert_not_called() # we should have requested the appropriate tasks subscription_proxy.SetRHSMConfigWithTask.assert_called_once() - subscription_proxy.RegisterOrganizationKeyWithTask.assert_called_once() - subscription_proxy.AttachSubscriptionWithTask.assert_called_once() - subscription_proxy.ParseAttachedSubscriptionsWithTask.assert_called_once() + subscription_proxy.RegisterAndSubscribeWithTask.assert_called_once() # not tried to set the CDN source switch_source.assert_not_called() # and tried to run them @@ -253,48 +248,6 @@ def test_register_org_key(self, get_proxy, thread_mgr_wait, run_task, switch_sou @patch("pyanaconda.ui.lib.subscription.switch_source") @patch("pyanaconda.modules.common.task.sync_run_task") - @patch("pyanaconda.core.threads.thread_manager.wait") - @patch("pyanaconda.modules.common.constants.services.SUBSCRIPTION.get_proxy") - def test_register_username_password(self, get_proxy, thread_mgr_wait, run_task, switch_source): - """Test the register_and_subscribe() helper method - username & password.""" - payload = Mock() - source_proxy = payload.get_source_proxy.return_value - source_proxy.Type = SOURCE_TYPE_CLOSEST_MIRROR - progress_callback = Mock() - error_callback = Mock() - subscription_proxy = get_proxy.return_value - # simulate the system not being registered - subscription_proxy.IsRegistered = False - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.PASSWORD_REQUEST - # run the function - register_and_subscribe(payload=payload, - progress_callback=progress_callback, - error_callback=error_callback) - # we should have waited on network - thread_mgr_wait.assert_called_once_with(THREAD_WAIT_FOR_CONNECTING_NM) - # system was no registered, so no unregistration phase - print(error_callback.mock_calls) - progress_callback.assert_has_calls( - [call(SubscriptionPhase.REGISTER), - call(SubscriptionPhase.ATTACH_SUBSCRIPTION), - call(SubscriptionPhase.DONE)] - ) - # we were successful, so no error callback calls - error_callback.assert_not_called() - # we should have requested the appropriate tasks - subscription_proxy.SetRHSMConfigWithTask.assert_called_once() - subscription_proxy.RegisterUsernamePasswordWithTask.assert_called_once() - subscription_proxy.AttachSubscriptionWithTask.assert_called_once() - subscription_proxy.ParseAttachedSubscriptionsWithTask.assert_called_once() - # not tried to set the CDN source - switch_source.assert_not_called() - # and tried to run them - run_task.assert_called() - - @patch("pyanaconda.ui.lib.subscription.switch_source") - @patch("pyanaconda.modules.common.task.sync_run_task") - @patch("pyanaconda.core.threads.thread_manager.wait") @patch("pyanaconda.modules.common.constants.services.SUBSCRIPTION.get_proxy") def test_unregister_register(self, get_proxy, thread_mgr_wait, run_task, switch_source): """Test the register_and_subscribe() helper method - registered system.""" @@ -307,8 +260,6 @@ def test_unregister_register(self, get_proxy, thread_mgr_wait, run_task, switch_ # simulate the system being registered, # - this should add additional unregister phase and task subscription_proxy.IsRegistered = True - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.KEY_REQUEST # run the function register_and_subscribe(payload=payload, progress_callback=progress_callback, @@ -319,7 +270,6 @@ def test_unregister_register(self, get_proxy, thread_mgr_wait, run_task, switch_ progress_callback.assert_has_calls( [call(SubscriptionPhase.UNREGISTER), call(SubscriptionPhase.REGISTER), - call(SubscriptionPhase.ATTACH_SUBSCRIPTION), call(SubscriptionPhase.DONE)] ) # we were successful, so no error callback calls @@ -327,9 +277,7 @@ def test_unregister_register(self, get_proxy, thread_mgr_wait, run_task, switch_ # we should have requested the appropriate tasks subscription_proxy.SetRHSMConfigWithTask.assert_called_once() subscription_proxy.UnregisterWithTask.assert_called_once() - subscription_proxy.RegisterOrganizationKeyWithTask.assert_called_once() - subscription_proxy.AttachSubscriptionWithTask.assert_called_once() - subscription_proxy.ParseAttachedSubscriptionsWithTask.assert_called_once() + subscription_proxy.SetRHSMConfigWithTask.assert_called_once() # not tried to set the CDN source switch_source.assert_not_called() # and tried to run them @@ -348,10 +296,9 @@ def test_unregister_task_failed(self, get_proxy, thread_mgr_wait, run_task, swit # simulate the system being registered, # - this should add additional unregister phase and task subscription_proxy.IsRegistered = True - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.KEY_REQUEST # make the first (unregistration) task fail - run_task.side_effect = [True, UnregistrationError("unregistration failed")] + unregistration_error = UnregistrationError("unregistration failed") + run_task.side_effect = [True, unregistration_error] # run the function register_and_subscribe(payload=payload, progress_callback=progress_callback, @@ -363,7 +310,7 @@ def test_unregister_task_failed(self, get_proxy, thread_mgr_wait, run_task, swit [call(SubscriptionPhase.UNREGISTER)] ) # and the error callback should have been triggered - error_callback.assert_called_once_with("unregistration failed") + error_callback.assert_called_once_with(unregistration_error) # we should have requested the appropriate tasks subscription_proxy.SetRHSMConfigWithTask.assert_called_once() subscription_proxy.UnregisterWithTask.assert_called_once() @@ -377,18 +324,17 @@ def test_unregister_task_failed(self, get_proxy, thread_mgr_wait, run_task, swit @patch("pyanaconda.modules.common.task.sync_run_task") @patch("pyanaconda.core.threads.thread_manager.wait") @patch("pyanaconda.modules.common.constants.services.SUBSCRIPTION.get_proxy") - def test_register_org_key_failed(self, get_proxy, thread_mgr_wait, run_task, switch_source): - """Test the register_and_subscribe() helper method - org & key failed.""" + def test_sat_provisioning_failed(self, get_proxy, thread_mgr_wait, run_task, switch_source): + """Test the register_and_subscribe() helper method - Satellite provisioning failed.""" payload = Mock() progress_callback = Mock() error_callback = Mock() subscription_proxy = get_proxy.return_value # simulate the system not being registered subscription_proxy.IsRegistered = False - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.KEY_REQUEST # make the first (registration) task fail - run_task.side_effect = [True, RegistrationError("registration failed")] + sat_error = SatelliteProvisioningError("Satellite provisioning failed") + run_task.side_effect = [True, sat_error] # run the function register_and_subscribe(payload=payload, progress_callback=progress_callback, @@ -400,10 +346,10 @@ def test_register_org_key_failed(self, get_proxy, thread_mgr_wait, run_task, swi [call(SubscriptionPhase.REGISTER)] ) # and the error callback should have been triggered - error_callback.assert_called_once_with("registration failed") + error_callback.assert_called_once_with(sat_error) # we should have requested the appropriate tasks subscription_proxy.SetRHSMConfigWithTask.assert_called_once() - subscription_proxy.RegisterOrganizationKeyWithTask.assert_called_once() + subscription_proxy.RegisterAndSubscribeWithTask.assert_called_once() # and tried to run them run_task.assert_called() # setting CDN as installation source does not make sense @@ -414,53 +360,17 @@ def test_register_org_key_failed(self, get_proxy, thread_mgr_wait, run_task, swi @patch("pyanaconda.modules.common.task.sync_run_task") @patch("pyanaconda.core.threads.thread_manager.wait") @patch("pyanaconda.modules.common.constants.services.SUBSCRIPTION.get_proxy") - def test_register_key_missing(self, get_proxy, thread_mgr_wait, run_task, switch_source): - """Test the register_and_subscribe() helper method - key missing.""" - payload = Mock() - progress_callback = Mock() - error_callback = Mock() - subscription_proxy = get_proxy.return_value - # simulate the system not being registered - subscription_proxy.IsRegistered = False - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.KEY_MISSING_REQUEST - # run the function - register_and_subscribe(payload=payload, - progress_callback=progress_callback, - error_callback=error_callback) - # we should have waited on network - thread_mgr_wait.assert_called_once_with(THREAD_WAIT_FOR_CONNECTING_NM) - # there should be only the registration phase - progress_callback.assert_has_calls( - [call(SubscriptionPhase.REGISTER)] - ) - # and the error callback should have been triggered - error_callback.assert_called_once() - # in this case we fail before requesting any other task than - # the config one - subscription_proxy.SetRHSMConfigWithTask.assert_called_once() - run_task.assert_called() - # setting CDN as installation source does not make sense - # when we were not able to attach a subscription - switch_source.assert_not_called() - - @patch("pyanaconda.ui.lib.subscription.switch_source") - @patch("pyanaconda.modules.common.task.sync_run_task") - @patch("pyanaconda.core.threads.thread_manager.wait") - @patch("pyanaconda.modules.common.constants.services.SUBSCRIPTION.get_proxy") - def test_register_username_password_task_failed(self, get_proxy, thread_mgr_wait, - run_task, switch_source): - """Test the register_and_subscribe() helper method - username & password failed.""" + def test_register_failed(self, get_proxy, thread_mgr_wait, run_task, switch_source): + """Test the register_and_subscribe() helper method - failed to register.""" payload = Mock() progress_callback = Mock() error_callback = Mock() subscription_proxy = get_proxy.return_value # simulate the system not being registered subscription_proxy.IsRegistered = False - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.PASSWORD_REQUEST # make the first (registration) task fail - run_task.side_effect = [True, RegistrationError("registration failed")] + registration_error = RegistrationError("registration failed") + run_task.side_effect = [True, registration_error] # run the function register_and_subscribe(payload=payload, progress_callback=progress_callback, @@ -472,10 +382,10 @@ def test_register_username_password_task_failed(self, get_proxy, thread_mgr_wait [call(SubscriptionPhase.REGISTER)] ) # and the error callback should have been triggered - error_callback.assert_called_once_with("registration failed") + error_callback.assert_called_once_with(registration_error) # we should have requested the appropriate tasks subscription_proxy.SetRHSMConfigWithTask.assert_called_once() - subscription_proxy.RegisterUsernamePasswordWithTask.assert_called_once() + subscription_proxy.RegisterAndSubscribeWithTask.assert_called_once() # and tried to run them run_task.assert_called() # setting CDN as installation source does not make sense @@ -484,48 +394,13 @@ def test_register_username_password_task_failed(self, get_proxy, thread_mgr_wait @patch("pyanaconda.ui.lib.subscription.switch_source") @patch("pyanaconda.modules.common.task.sync_run_task") - @patch("pyanaconda.core.threads.thread_manager.wait") - @patch("pyanaconda.modules.common.constants.services.SUBSCRIPTION.get_proxy") - def test_register_password_missing(self, get_proxy, thread_mgr_wait, run_task, switch_source): - """Test the register_and_subscribe() helper method - password missing.""" - payload = Mock() - progress_callback = Mock() - error_callback = Mock() - subscription_proxy = get_proxy.return_value - # simulate the system not being registered - subscription_proxy.IsRegistered = False - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.PASSWORD_MISSING_REQUEST - # run the function - register_and_subscribe(payload=payload, - progress_callback=progress_callback, - error_callback=error_callback) - # we should have waited on network - thread_mgr_wait.assert_called_once_with(THREAD_WAIT_FOR_CONNECTING_NM) - # there should be only the registration phase - progress_callback.assert_has_calls( - [call(SubscriptionPhase.REGISTER)] - ) - # and the error callback should have been triggered - error_callback.assert_called_once() - # in this case we fail before requesting any other task than - # the config one - subscription_proxy.SetRHSMConfigWithTask.assert_called_once() - run_task.assert_called() - # setting CDN as installation source does not make sense - # when we were not able to attach a subscription - switch_source.assert_not_called() - - @patch("pyanaconda.payload.manager.payloadMgr.start") - @patch("pyanaconda.ui.lib.subscription.switch_source") - @patch("pyanaconda.modules.common.task.sync_run_task") - @patch("pyanaconda.core.threads.thread_manager.wait") @patch("pyanaconda.modules.common.constants.services.SUBSCRIPTION.get_proxy") def test_register_override_cdrom(self, get_proxy, thread_mgr_wait, run_task, switch_source, restart_thread): """Test the register_and_subscribe() helper method - override CDROM source.""" payload = Mock() payload.type = PAYLOAD_TYPE_DNF + payload.data.repo.dataList = MagicMock(return_value=[]) source_proxy_1 = Mock() source_proxy_1.Type = SOURCE_TYPE_CDROM source_proxy_2 = Mock() @@ -539,8 +414,6 @@ def test_register_override_cdrom(self, get_proxy, thread_mgr_wait, run_task, swi subscription_proxy = get_proxy.return_value # simulate the system not being registered subscription_proxy.IsRegistered = False - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.KEY_REQUEST # run the function register_and_subscribe(payload=payload, progress_callback=progress_callback, @@ -551,18 +424,15 @@ def test_register_override_cdrom(self, get_proxy, thread_mgr_wait, run_task, swi # system was no registered, so no unregistration phase progress_callback.assert_has_calls( [call(SubscriptionPhase.REGISTER), - call(SubscriptionPhase.ATTACH_SUBSCRIPTION), call(SubscriptionPhase.DONE)] ) # we were successful, so no error callback calls error_callback.assert_not_called() # we should have requested the appropriate tasks subscription_proxy.SetRHSMConfigWithTask.assert_called_once() - subscription_proxy.RegisterOrganizationKeyWithTask.assert_called_once() - subscription_proxy.AttachSubscriptionWithTask.assert_called_once() - subscription_proxy.ParseAttachedSubscriptionsWithTask.assert_called_once() + subscription_proxy.RegisterAndSubscribeWithTask.assert_called_once() # and tried to override the CDROM source, as it is on a list of sources - # that are appropriate to be overriden by the CDN source + # that are appropriate to be overridden by the CDN source switch_source.assert_called_once_with(payload, SOURCE_TYPE_CDN) # and tried to run them run_task.assert_called() @@ -580,6 +450,7 @@ def test_register_override_cdrom_no_restart(self, get_proxy, thread_mgr_wait, ru """Test the register_and_subscribe() helper method - override CDROM source, no restart.""" payload = Mock() payload.type = PAYLOAD_TYPE_DNF + payload.data.repo.dataList = MagicMock(return_value=[]) source_proxy_1 = Mock() source_proxy_1.Type = SOURCE_TYPE_CDROM source_proxy_2 = Mock() @@ -593,8 +464,6 @@ def test_register_override_cdrom_no_restart(self, get_proxy, thread_mgr_wait, ru subscription_proxy = get_proxy.return_value # simulate the system not being registered subscription_proxy.IsRegistered = False - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.KEY_REQUEST # run the function & tell it not to restart payload register_and_subscribe(payload=payload, progress_callback=progress_callback, @@ -605,63 +474,21 @@ def test_register_override_cdrom_no_restart(self, get_proxy, thread_mgr_wait, ru # system was no registered, so no unregistration phase progress_callback.assert_has_calls( [call(SubscriptionPhase.REGISTER), - call(SubscriptionPhase.ATTACH_SUBSCRIPTION), call(SubscriptionPhase.DONE)] ) # we were successful, so no error callback calls error_callback.assert_not_called() # we should have requested the appropriate tasks subscription_proxy.SetRHSMConfigWithTask.assert_called_once() - subscription_proxy.RegisterOrganizationKeyWithTask.assert_called_once() - subscription_proxy.AttachSubscriptionWithTask.assert_called_once() - subscription_proxy.ParseAttachedSubscriptionsWithTask.assert_called_once() + subscription_proxy.RegisterAndSubscribeWithTask.assert_called_once() # and tried to override the CDROM source, as it is on a list of sources - # that are appropriate to be overriden by the CDN source + # that are appropriate to be overridden by the CDN source switch_source.assert_called_once_with(payload, SOURCE_TYPE_CDN) # and tried to run them run_task.assert_called() # we told the payload not to restart restart_thread.assert_not_called() - @patch("pyanaconda.ui.lib.subscription.switch_source") - @patch("pyanaconda.modules.common.task.sync_run_task") - @patch("pyanaconda.core.threads.thread_manager.wait") - @patch("pyanaconda.modules.common.constants.services.SUBSCRIPTION.get_proxy") - def test_subscription_task_failed(self, get_proxy, thread_mgr_wait, run_task, switch_source): - """Test the register_and_subscribe() helper method - failed to attach subscription.""" - payload = Mock() - progress_callback = Mock() - error_callback = Mock() - subscription_proxy = get_proxy.return_value - # simulate the system not being registered - subscription_proxy.IsRegistered = False - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.PASSWORD_REQUEST - # make the second (subscription) task fail - run_task.side_effect = [True, True, SubscriptionError("failed to attach subscription")] - # run the function - register_and_subscribe(payload=payload, - progress_callback=progress_callback, - error_callback=error_callback) - # we should have waited on network - thread_mgr_wait.assert_called_once_with(THREAD_WAIT_FOR_CONNECTING_NM) - # there should be only the registration & subscription phase - progress_callback.assert_has_calls( - [call(SubscriptionPhase.REGISTER), - call(SubscriptionPhase.ATTACH_SUBSCRIPTION)] - ) - # and the error callback should have been triggered - error_callback.assert_called_once_with("failed to attach subscription") - # we should have requested the appropriate tasks - subscription_proxy.SetRHSMConfigWithTask.assert_called_once() - subscription_proxy.RegisterUsernamePasswordWithTask.assert_called_once() - subscription_proxy.AttachSubscriptionWithTask.assert_called_once() - # and tried to run them - run_task.assert_called() - # setting CDN as installation source does not make sense - # when we were not able to attach a subscription - switch_source.assert_not_called() - @patch("pyanaconda.modules.common.task.sync_run_task") @patch("pyanaconda.modules.common.constants.services.SUBSCRIPTION.get_proxy") def test_unregister(self, get_proxy, run_task): @@ -726,7 +553,8 @@ def test_unregister_failed(self, get_proxy, run_task): # simulate the system being registered, subscription_proxy.IsRegistered = True # make the unregistration task fail - run_task.side_effect = [True, UnregistrationError("unregistration failed")] + unregistration_error = UnregistrationError("unregistration failed") + run_task.side_effect = [True, unregistration_error] # run the function unregister(payload=payload, overridden_source_type=None, @@ -737,7 +565,7 @@ def test_unregister_failed(self, get_proxy, run_task): [call(SubscriptionPhase.UNREGISTER)] ) # and the error callback should have been triggered - error_callback.assert_called_once_with("unregistration failed") + error_callback.assert_called_once_with(unregistration_error) # we should have requested the appropriate tasks subscription_proxy.SetRHSMConfigWithTask.assert_called_once() subscription_proxy.UnregisterWithTask.assert_called_once() @@ -803,32 +631,6 @@ def test_check_cdn_is_installation_source(self): ostree_payload.type = PAYLOAD_TYPE_RPM_OSTREE assert not check_cdn_is_installation_source(ostree_payload) - @patch_dbus_get_proxy_with_cache - def test_is_cdn_registration_required(self, proxy_getter): - """Test the is_cdn_registration_required function.""" - dnf_payload = Mock() - dnf_payload.type = PAYLOAD_TYPE_DNF - - source_proxy = dnf_payload.get_source_proxy.return_value - source_proxy.Type = SOURCE_TYPE_CDN - - boss_proxy = BOSS.get_proxy() - boss_proxy.GetModules.return_value = [SUBSCRIPTION.service_name] - - subscription_proxy = SUBSCRIPTION.get_proxy() - subscription_proxy.IsSubscriptionAttached = False - - assert is_cdn_registration_required(dnf_payload) is True - - subscription_proxy.IsSubscriptionAttached = True - assert is_cdn_registration_required(dnf_payload) is False - - boss_proxy.GetModules.return_value = [] - assert is_cdn_registration_required(dnf_payload) is False - - source_proxy.Type = SOURCE_TYPE_CDROM - assert is_cdn_registration_required(dnf_payload) is False - @patch("pyanaconda.ui.lib.subscription.switch_source") @patch("pyanaconda.modules.common.task.sync_run_task") @patch("pyanaconda.core.threads.thread_manager.wait") @@ -842,8 +644,6 @@ def test_unsupported_payload_reg(self, get_proxy, thread_mgr_wait, run_task, swi subscription_proxy = get_proxy.return_value # simulate the system not being registered subscription_proxy.IsRegistered = False - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.PASSWORD_REQUEST # run the function register_and_subscribe(payload=payload, progress_callback=progress_callback, @@ -854,16 +654,13 @@ def test_unsupported_payload_reg(self, get_proxy, thread_mgr_wait, run_task, swi print(error_callback.mock_calls) progress_callback.assert_has_calls( [call(SubscriptionPhase.REGISTER), - call(SubscriptionPhase.ATTACH_SUBSCRIPTION), call(SubscriptionPhase.DONE)] ) # we were successful, so no error callback calls error_callback.assert_not_called() # we should have requested the appropriate tasks subscription_proxy.SetRHSMConfigWithTask.assert_called_once() - subscription_proxy.RegisterUsernamePasswordWithTask.assert_called_once() - subscription_proxy.AttachSubscriptionWithTask.assert_called_once() - subscription_proxy.ParseAttachedSubscriptionsWithTask.assert_called_once() + subscription_proxy.RegisterAndSubscribeWithTask.assert_called_once() # not tried to set the CDN source switch_source.assert_not_called() # and tried to run them @@ -919,8 +716,6 @@ def test_register_payload_restart(self, get_proxy, thread_mgr_wait, run_task, sw subscription_proxy = get_proxy.return_value # simulate the system not being registered subscription_proxy.IsRegistered = False - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.PASSWORD_REQUEST # run the function register_and_subscribe(payload=payload, progress_callback=progress_callback, @@ -932,16 +727,13 @@ def test_register_payload_restart(self, get_proxy, thread_mgr_wait, run_task, sw print(error_callback.mock_calls) progress_callback.assert_has_calls( [call(SubscriptionPhase.REGISTER), - call(SubscriptionPhase.ATTACH_SUBSCRIPTION), call(SubscriptionPhase.DONE)] ) # we were successful, so no error callback calls error_callback.assert_not_called() # we should have requested the appropriate tasks subscription_proxy.SetRHSMConfigWithTask.assert_called_once() - subscription_proxy.RegisterUsernamePasswordWithTask.assert_called_once() - subscription_proxy.AttachSubscriptionWithTask.assert_called_once() - subscription_proxy.ParseAttachedSubscriptionsWithTask.assert_called_once() + subscription_proxy.RegisterAndSubscribeWithTask.assert_called_once() # and tried to run them run_task.assert_called() # tried to restart the payload as CDN is set and we need to restart @@ -965,8 +757,6 @@ def test_register_payload_no_restart(self, get_proxy, thread_mgr_wait, run_task, subscription_proxy = get_proxy.return_value # simulate the system not being registered subscription_proxy.IsRegistered = False - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.PASSWORD_REQUEST # run the function register_and_subscribe(payload=payload, progress_callback=progress_callback, @@ -978,16 +768,13 @@ def test_register_payload_no_restart(self, get_proxy, thread_mgr_wait, run_task, print(error_callback.mock_calls) progress_callback.assert_has_calls( [call(SubscriptionPhase.REGISTER), - call(SubscriptionPhase.ATTACH_SUBSCRIPTION), call(SubscriptionPhase.DONE)] ) # we were successful, so no error callback calls error_callback.assert_not_called() # we should have requested the appropriate tasks subscription_proxy.SetRHSMConfigWithTask.assert_called_once() - subscription_proxy.RegisterUsernamePasswordWithTask.assert_called_once() - subscription_proxy.AttachSubscriptionWithTask.assert_called_once() - subscription_proxy.ParseAttachedSubscriptionsWithTask.assert_called_once() + subscription_proxy.RegisterAndSubscribeWithTask.assert_called_once() # and tried to run them run_task.assert_called() # told the helper method not to restart @@ -1010,8 +797,6 @@ def test_register_no_payload_restart(self, get_proxy, thread_mgr_wait, run_task, subscription_proxy = get_proxy.return_value # simulate the system not being registered subscription_proxy.IsRegistered = False - # simulate subscription request - subscription_proxy.SubscriptionRequest = self.PASSWORD_REQUEST # run the function register_and_subscribe(payload=payload, progress_callback=progress_callback, @@ -1023,16 +808,13 @@ def test_register_no_payload_restart(self, get_proxy, thread_mgr_wait, run_task, print(error_callback.mock_calls) progress_callback.assert_has_calls( [call(SubscriptionPhase.REGISTER), - call(SubscriptionPhase.ATTACH_SUBSCRIPTION), call(SubscriptionPhase.DONE)] ) # we were successful, so no error callback calls error_callback.assert_not_called() # we should have requested the appropriate tasks subscription_proxy.SetRHSMConfigWithTask.assert_called_once() - subscription_proxy.RegisterUsernamePasswordWithTask.assert_called_once() - subscription_proxy.AttachSubscriptionWithTask.assert_called_once() - subscription_proxy.ParseAttachedSubscriptionsWithTask.assert_called_once() + subscription_proxy.RegisterAndSubscribeWithTask.assert_called_once() # not tried to set the CDN source switch_source.assert_not_called() # and tried to run them