def write_esm_announcement_message(cfg: config.UAConfig, series: str) -> None: """Write human-readable messages if ESM is offered on this LTS release. Do not write ESM announcements if esm-apps is enabled or beta. :param cfg: UAConfig instance for this environment. :param series: string of Ubuntu release series: 'xenial'. """ apps_cls = entitlements.entitlement_factory(cfg=cfg, name="esm-apps") apps_inst = apps_cls(cfg) enabled_status = ApplicationStatus.ENABLED apps_not_enabled = apps_inst.application_status()[0] != enabled_status config_allow_beta = util.is_config_value_true( config=cfg.cfg, path_to_value="features.allow_beta") apps_not_beta = bool(config_allow_beta or not apps_cls.is_beta) msg_dir = os.path.join(cfg.data_dir, "messages") esm_news_file = os.path.join(msg_dir, ExternalMessage.ESM_ANNOUNCE.value) platform_info = util.get_platform_info() is_active_esm = util.is_active_esm(platform_info["series"]) if is_active_esm: ua_esm_url = defaults.EOL_UA_URL_TMPL.format( hyphenatedrelease=platform_info["release"].replace(".", "-")) else: ua_esm_url = defaults.BASE_ESM_URL if apps_not_beta and apps_not_enabled: util.write_file(esm_news_file, "\n" + ANNOUNCE_ESM_TMPL.format(url=ua_esm_url)) else: util.remove_file(esm_news_file)
def _handle_beta_resources(cfg, show_beta, response) -> Dict[str, Any]: """Remove beta services from response dict if needed""" config_allow_beta = util.is_config_value_true( config=cfg.cfg, path_to_value="features.allow_beta" ) show_beta |= config_allow_beta if show_beta: return response new_response = copy.deepcopy(response) released_resources = [] for resource in new_response.get("services", {}): resource_name = resource["name"] try: ent_cls = entitlement_factory(cfg=cfg, name=resource_name) except exceptions.EntitlementNotFoundError: """ Here we cannot know the status of a service, since it is not listed as a valid entitlement. Therefore, we keep this service in the list, since we cannot validate if it is a beta service or not. """ released_resources.append(resource) continue enabled_status = UserFacingStatus.ACTIVE.value if not ent_cls.is_beta or resource.get("status", "") == enabled_status: released_resources.append(resource) if released_resources: new_response["services"] = released_resources return new_response
def _unattached_status(cfg: UAConfig) -> Dict[str, Any]: """Return unattached status as a dict.""" response = copy.deepcopy(DEFAULT_STATUS) response["version"] = version.get_version(features=cfg.features) resources = get_available_resources(cfg) for resource in resources: if resource.get("available"): available = UserFacingAvailability.AVAILABLE.value else: available = UserFacingAvailability.UNAVAILABLE.value try: ent_cls = entitlement_factory( cfg=cfg, name=resource.get("name", "") ) except exceptions.EntitlementNotFoundError: LOG.debug( messages.AVAILABILITY_FROM_UNKNOWN_SERVICE.format( service=resource.get("name", "without a 'name' key") ) ) continue response["services"].append( { "name": resource.get("presentedAs", resource["name"]), "description": ent_cls.description, "available": available, } ) response["services"].sort(key=lambda x: x.get("name", "")) return response
def _enable_required_services( self, ) -> Tuple[bool, Optional[messages.NamedMessage]]: """ Prompt user when required services are found during enable. When enabling a service, we may find that there are required services that must be enabled first. In that situation, we can ask the user if the required service should be enabled before proceeding. """ from uaclient.entitlements import entitlement_factory for required_service in self.required_services: try: ent_cls = entitlement_factory( cfg=self.cfg, name=required_service ) except exceptions.EntitlementNotFoundError: msg = messages.REQUIRED_SERVICE_NOT_FOUND.format( service=required_service ) return False, msg ent = ent_cls(self.cfg, allow_beta=True) is_service_disabled = ( ent.application_status()[0] == ApplicationStatus.DISABLED ) if is_service_disabled: user_msg = messages.REQUIRED_SERVICE.format( service_being_enabled=self.title, required_service=ent.title, ) e_msg = messages.REQUIRED_SERVICE_STOPS_ENABLE.format( service_being_enabled=self.title, required_service=ent.title, ) if not util.prompt_for_confirmation( msg=user_msg, assume_yes=self.assume_yes ): return False, e_msg event.info("Enabling required service: {}".format(ent.title)) ret, fail = ent.enable(silent=True) if not ret: error_msg = "" if fail.message and fail.message.msg: error_msg = "\n" + fail.message.msg msg = messages.ERROR_ENABLING_REQUIRED_SERVICE.format( error=error_msg, service=ent.title ) return ret, msg return True, None
def _get_service_for_pocket(pocket: str, cfg: UAConfig): service_to_check = "no-service-needed" if pocket == UA_INFRA_POCKET: service_to_check = "esm-infra" elif pocket == UA_APPS_POCKET: service_to_check = "esm-apps" ent_cls = entitlement_factory(cfg=cfg, name=service_to_check) return ent_cls(cfg) if ent_cls else None
def test_entitlement_factory(self, FakeConfig): m_cls_1 = mock.MagicMock() m_cls_1.return_value.valid_names = ["ent1", "othername"] m_cls_2 = mock.MagicMock() m_cls_2.return_value.valid_names = ["ent2"] ents = {m_cls_1, m_cls_2} cfg = FakeConfig() with mock.patch.object(entitlements, "ENTITLEMENT_CLASSES", ents): assert m_cls_1 == entitlements.entitlement_factory( cfg=cfg, name="othername" ) assert m_cls_2 == entitlements.entitlement_factory( cfg=cfg, name="ent2" ) with pytest.raises(exceptions.EntitlementNotFoundError): entitlements.entitlement_factory(cfg=cfg, name="nonexistent")
def help(cfg, name): """Return help information from an uaclient service as a dict :param name: Name of the service for which to return help data. :raises: UserFacingError when no help is available. """ resources = get_available_resources(cfg) help_resource = None # We are using an OrderedDict here to guarantee # that if we need to print the result of this # dict, the order of insertion will always be respected response_dict = OrderedDict() response_dict["name"] = name for resource in resources: if resource["name"] == name or resource.get("presentedAs") == name: try: help_ent_cls = entitlement_factory( cfg=cfg, name=resource["name"] ) except exceptions.EntitlementNotFoundError: continue help_resource = resource help_ent = help_ent_cls(cfg) break if help_resource is None: raise exceptions.UserFacingError( "No help available for '{}'".format(name) ) if cfg.is_attached: service_status = _attached_service_status(help_ent, {}) status_msg = service_status["status"] response_dict["entitled"] = service_status["entitled"] response_dict["status"] = status_msg if status_msg == "enabled" and help_ent_cls.is_beta: response_dict["beta"] = True else: if help_resource["available"]: available = UserFacingAvailability.AVAILABLE.value else: available = UserFacingAvailability.UNAVAILABLE.value response_dict["available"] = available response_dict["help"] = help_ent.help_info return response_dict
def _check_any_service_is_active(self, services: Tuple[str, ...]) -> bool: from uaclient.entitlements import ( EntitlementNotFoundError, entitlement_factory, ) for service in services: try: ent_cls = entitlement_factory(cfg=self.cfg, name=service) except EntitlementNotFoundError: continue ent_status, _ = ent_cls(self.cfg).application_status() if ent_status == ApplicationStatus.ENABLED: return True return False
def enable_entitlement_by_name( cfg: config.UAConfig, name: str, *, assume_yes: bool = False, allow_beta: bool = False ): """ Constructs an entitlement based on the name provided. Passes kwargs onto the entitlement constructor. :raise EntitlementNotFoundError: If no entitlement with the given name is found, then raises this error. """ ent_cls = entitlements.entitlement_factory(cfg=cfg, name=name) entitlement = ent_cls( cfg, assume_yes=assume_yes, allow_beta=allow_beta, called_name=name ) return entitlement.enable()
def check_required_services_active(self): """ Check if all required services are active :return: True if all required services are active False is at least one of the required services is disabled """ from uaclient.entitlements import entitlement_factory for required_service in self.required_services: try: ent_cls = entitlement_factory( cfg=self.cfg, name=required_service ) ent_status, _ = ent_cls(self.cfg).application_status() if ent_status != ApplicationStatus.ENABLED: return False except exceptions.EntitlementNotFoundError: pass return True
def _disable_dependent_services( self, silent: bool ) -> Tuple[bool, Optional[messages.NamedMessage]]: """ Disable dependent services When performing a disable operation, we might have other services that depend on the original services. If that is true, we will alert the user about this and prompt for confirmation to disable these services as well. @param silent: Boolean set True to silence print/log of messages """ from uaclient.entitlements import entitlement_factory for dependent_service in self.dependent_services: try: ent_cls = entitlement_factory( cfg=self.cfg, name=dependent_service ) except exceptions.EntitlementNotFoundError: msg = messages.DEPENDENT_SERVICE_NOT_FOUND.format( service=dependent_service ) event.info(info_msg=msg.msg, file_type=sys.stderr) return False, msg ent = ent_cls(cfg=self.cfg, assume_yes=True) is_service_enabled = ( ent.application_status()[0] == ApplicationStatus.ENABLED ) if is_service_enabled: user_msg = messages.DEPENDENT_SERVICE.format( dependent_service=ent.title, service_being_disabled=self.title, ) e_msg = messages.DEPENDENT_SERVICE_STOPS_DISABLE.format( service_being_disabled=self.title, dependent_service=ent.title, ) if not util.prompt_for_confirmation( msg=user_msg, assume_yes=self.assume_yes ): return False, e_msg if not silent: event.info( messages.DISABLING_DEPENDENT_SERVICE.format( required_service=ent.title ) ) ret, fail = ent.disable(silent=True) if not ret: error_msg = "" if fail.message and fail.message.msg: error_msg = "\n" + fail.message.msg msg = messages.FAILED_DISABLING_DEPENDENT_SERVICE.format( error=error_msg, required_service=ent.title ) return False, msg return True, None
def write_apt_and_motd_templates(cfg: config.UAConfig, series: str) -> None: """Write messaging templates about available esm packages. :param cfg: UAConfig instance for this environment. :param series: string of Ubuntu release series: 'xenial'. """ apps_no_pkg_file = ExternalMessage.APT_PRE_INVOKE_APPS_NO_PKGS.value apps_pkg_file = ExternalMessage.APT_PRE_INVOKE_APPS_PKGS.value infra_no_pkg_file = ExternalMessage.APT_PRE_INVOKE_INFRA_NO_PKGS.value infra_pkg_file = ExternalMessage.APT_PRE_INVOKE_INFRA_PKGS.value motd_apps_no_pkg_file = ExternalMessage.MOTD_APPS_NO_PKGS.value motd_apps_pkg_file = ExternalMessage.MOTD_APPS_PKGS.value motd_infra_no_pkg_file = ExternalMessage.MOTD_INFRA_NO_PKGS.value motd_infra_pkg_file = ExternalMessage.MOTD_INFRA_PKGS.value no_warranty_file = ExternalMessage.UBUNTU_NO_WARRANTY.value msg_dir = os.path.join(cfg.data_dir, "messages") apps_cls = entitlements.entitlement_factory(cfg=cfg, name="esm-apps") apps_inst = apps_cls(cfg) config_allow_beta = util.is_config_value_true( config=cfg.cfg, path_to_value="features.allow_beta") apps_valid = bool(config_allow_beta or not apps_cls.is_beta) infra_cls = entitlements.entitlement_factory(cfg=cfg, name="esm-infra") infra_inst = infra_cls(cfg) expiry_status, remaining_days = get_contract_expiry_status(cfg) enabled_status = ApplicationStatus.ENABLED msg_esm_apps = False msg_esm_infra = False if util.is_active_esm(series): no_warranty_msg = "" if expiry_status in ( ContractExpiryStatus.EXPIRED, ContractExpiryStatus.NONE, ): no_warranty_msg = UBUNTU_NO_WARRANTY if infra_inst.application_status()[0] != enabled_status: msg_esm_infra = True no_warranty_msg = UBUNTU_NO_WARRANTY elif remaining_days <= defaults.CONTRACT_EXPIRY_PENDING_DAYS: msg_esm_infra = True _write_template_or_remove(no_warranty_msg, os.path.join(msg_dir, no_warranty_file)) if not msg_esm_infra: # write_apt_and_motd_templates is only called if util.is_lts(series) msg_esm_apps = apps_valid if msg_esm_infra: _write_esm_service_msg_templates( cfg, infra_inst, expiry_status, remaining_days, infra_pkg_file, infra_no_pkg_file, motd_infra_pkg_file, motd_infra_no_pkg_file, ) else: _remove_msg_templates( msg_dir=os.path.join(cfg.data_dir, "messages"), msg_template_names=[ infra_pkg_file, infra_no_pkg_file, motd_infra_pkg_file, motd_infra_no_pkg_file, ], ) if msg_esm_apps: _write_esm_service_msg_templates( cfg, apps_inst, expiry_status, remaining_days, apps_pkg_file, apps_no_pkg_file, motd_apps_pkg_file, motd_apps_no_pkg_file, ) else: _remove_msg_templates( msg_dir=os.path.join(cfg.data_dir, "messages"), msg_template_names=[ apps_pkg_file, apps_no_pkg_file, motd_apps_pkg_file, motd_apps_no_pkg_file, ], )
class TestStatus: esm_desc = entitlement_factory(cfg=UAConfig(), name="esm-infra").description ros_desc = entitlement_factory(cfg=UAConfig(), name="ros").description def check_beta(self, cls, show_beta, uacfg=None, status=""): if not show_beta: if status == "enabled": return False if uacfg: allow_beta = uacfg.cfg.get("features", {}).get("allow_beta", False) if allow_beta: return False return cls.is_beta return False @pytest.mark.parametrize( "show_beta,expected_services", ( ( True, [ { "available": "yes", "name": "esm-infra", "description": esm_desc, }, { "available": "no", "name": "ros", "description": ros_desc, }, ], ), ( False, [{ "available": "yes", "name": "esm-infra", "description": esm_desc, }], ), ), ) @mock.patch("uaclient.status.get_available_resources") @mock.patch("uaclient.status.os.getuid", return_value=0) def test_root_unattached( self, _m_getuid, m_get_available_resources, _m_should_reboot, m_remove_notice, show_beta, expected_services, FakeConfig, ): """Test we get the correct status dict when unattached""" cfg = FakeConfig() m_get_available_resources.return_value = [ { "name": "esm-infra", "available": True }, { "name": "ros", "available": False }, ] expected = copy.deepcopy(DEFAULT_STATUS) expected["version"] = mock.ANY expected["services"] = expected_services with mock.patch( "uaclient.status._get_config_status") as m_get_cfg_status: m_get_cfg_status.return_value = DEFAULT_CFG_STATUS assert expected == status.status(cfg=cfg, show_beta=show_beta) expected_calls = [ mock.call( "", messages.ENABLE_REBOOT_REQUIRED_TMPL.format( operation="fix operation"), ) ] assert expected_calls == m_remove_notice.call_args_list @pytest.mark.parametrize("show_beta", (True, False)) @pytest.mark.parametrize("features_override", ((None), ({ "allow_beta": True }))) @pytest.mark.parametrize( "avail_res,entitled_res,uf_entitled,uf_status", ( ( # Empty lists means UNENTITLED and UNAVAILABLE [], [], ContractStatus.UNENTITLED.value, UserFacingStatus.UNAVAILABLE.value, ), ( # available == False means UNAVAILABLE [{ "name": "livepatch", "available": False }], [], ContractStatus.UNENTITLED.value, UserFacingStatus.UNAVAILABLE.value, ), ( # available == True but unentitled means UNAVAILABLE [{ "name": "livepatch", "available": True }], [], ContractStatus.UNENTITLED.value, UserFacingStatus.UNAVAILABLE.value, ), ( # available == False and entitled means INAPPLICABLE [{ "name": "livepatch", "available": False }], [{ "type": "livepatch", "entitled": True }], ContractStatus.ENTITLED.value, UserFacingStatus.INAPPLICABLE.value, ), ), ) @mock.patch( M_PATH + "livepatch.LivepatchEntitlement.application_status", return_value=(ApplicationStatus.DISABLED, ""), ) @mock.patch("uaclient.status.get_available_resources") @mock.patch("uaclient.config.os.getuid", return_value=0) def test_root_attached( self, _m_getuid, m_get_avail_resources, _m_livepatch_status, _m_should_reboot, _m_remove_notice, avail_res, entitled_res, uf_entitled, uf_status, features_override, show_beta, FakeConfig, ): """Test we get the correct status dict when attached with basic conf""" resource_names = [resource["name"] for resource in avail_res] default_entitled = ContractStatus.UNENTITLED.value default_status = UserFacingStatus.UNAVAILABLE.value token = { "availableResources": [], "machineTokenInfo": { "machineId": "test_machine_id", "accountInfo": { "id": "acct-1", "name": "test_account", "createdAt": "2019-06-14T06:45:50Z", "externalAccountIDs": [{ "IDs": ["id1"], "Origin": "AWS" }], }, "contractInfo": { "id": "cid", "name": "test_contract", "createdAt": "2020-05-08T19:02:26Z", "effectiveFrom": "2000-05-08T19:02:26Z", "effectiveTo": "2040-05-08T19:02:26Z", "resourceEntitlements": entitled_res, "products": ["free"], }, }, } available_resource_response = [{ "name": cls.name, "available": bool({ "name": cls.name, "available": True } in avail_res), } for cls in ENTITLEMENT_CLASSES] if avail_res: token["availableResources"] = available_resource_response else: m_get_avail_resources.return_value = available_resource_response cfg = FakeConfig.for_attached_machine(machine_token=token) if features_override: cfg.override_features(features_override) expected_services = [{ "description": cls.description, "entitled": uf_entitled if cls.name in resource_names else default_entitled, "name": cls.name, "status": uf_status if cls.name in resource_names else default_status, "status_details": mock.ANY, "description_override": None, "available": mock.ANY, "blocked_by": [], } for cls in ENTITLEMENT_CLASSES if not self.check_beta(cls, show_beta, cfg)] expected = copy.deepcopy(DEFAULT_STATUS) expected.update({ "version": version.get_version(features=cfg.features), "attached": True, "machine_id": "test_machine_id", "services": expected_services, "effective": datetime.datetime(2000, 5, 8, 19, 2, 26, tzinfo=datetime.timezone.utc), "expires": datetime.datetime(2040, 5, 8, 19, 2, 26, tzinfo=datetime.timezone.utc), "contract": { "name": "test_contract", "id": "cid", "created_at": datetime.datetime(2020, 5, 8, 19, 2, 26, tzinfo=datetime.timezone.utc), "products": ["free"], "tech_support_level": "n/a", }, "account": { "name": "test_account", "id": "acct-1", "created_at": datetime.datetime(2019, 6, 14, 6, 45, 50, tzinfo=datetime.timezone.utc), "external_account_ids": [{ "IDs": ["id1"], "Origin": "AWS" }], }, }) with mock.patch( "uaclient.status._get_config_status") as m_get_cfg_status: m_get_cfg_status.return_value = DEFAULT_CFG_STATUS assert expected == status.status(cfg=cfg, show_beta=show_beta) if avail_res: assert m_get_avail_resources.call_count == 0 else: assert m_get_avail_resources.call_count == 1 # status() idempotent with mock.patch( "uaclient.status._get_config_status") as m_get_cfg_status: m_get_cfg_status.return_value = DEFAULT_CFG_STATUS assert expected == status.status(cfg=cfg, show_beta=show_beta) @mock.patch("uaclient.status.get_available_resources") @mock.patch("uaclient.config.os.getuid") def test_nonroot_unattached_is_same_as_unattached_root( self, m_getuid, m_get_available_resources, _m_should_reboot, _m_remove_notice, FakeConfig, ): m_get_available_resources.return_value = [{ "name": "esm-infra", "available": True }] m_getuid.return_value = 1000 cfg = FakeConfig() nonroot_status = status.status(cfg=cfg) m_getuid.return_value = 0 root_unattached_status = status.status(cfg=cfg) assert root_unattached_status == nonroot_status @mock.patch("uaclient.status.get_available_resources") @mock.patch("uaclient.status.os.getuid") def test_root_followed_by_nonroot( self, m_getuid, m_get_available_resources, _m_should_reboot, _m_remove_notice, tmpdir, FakeConfig, ): """Ensure that non-root run after root returns data""" cfg = UAConfig({"data_dir": tmpdir.strpath}) # Run as root m_getuid.return_value = 0 before = copy.deepcopy(status.status(cfg=cfg)) # Replicate an attach by modifying the underlying config and confirm # that we see different status other_cfg = FakeConfig.for_attached_machine() cfg.write_cache("accounts", {"accounts": other_cfg.accounts}) cfg.write_cache("machine-token", other_cfg.machine_token) cfg.delete_cache_key("status-cache") assert status._attached_status(cfg=cfg) != before # Run as regular user and confirm that we see the result from # last time we called .status() m_getuid.return_value = 1000 after = status.status(cfg=cfg) assert before == after @mock.patch("uaclient.status.get_available_resources", return_value=[]) @mock.patch("uaclient.status.os.getuid", return_value=0) def test_cache_file_is_written_world_readable( self, _m_getuid, _m_get_available_resources, _m_should_reboot, m_remove_notice, tmpdir, ): cfg = UAConfig({"data_dir": tmpdir.strpath}) status.status(cfg=cfg) assert 0o644 == stat.S_IMODE( os.lstat(cfg.data_path("status-cache")).st_mode) expected_calls = [ mock.call( "", messages.ENABLE_REBOOT_REQUIRED_TMPL.format( operation="fix operation"), ) ] assert expected_calls == m_remove_notice.call_args_list @pytest.mark.parametrize("show_beta", (True, False)) @pytest.mark.parametrize("features_override", ((None), ({ "allow_beta": False }))) @pytest.mark.parametrize( "entitlements", ( [], [{ "type": "support", "entitled": True, "affordances": { "supportLevel": "anything" }, }], ), ) @mock.patch("uaclient.status.os.getuid", return_value=0) @mock.patch( M_PATH + "fips.FIPSCommonEntitlement.application_status", return_value=(ApplicationStatus.DISABLED, ""), ) @mock.patch( M_PATH + "livepatch.LivepatchEntitlement.application_status", return_value=(ApplicationStatus.DISABLED, ""), ) @mock.patch(M_PATH + "livepatch.LivepatchEntitlement.user_facing_status") @mock.patch(M_PATH + "livepatch.LivepatchEntitlement.contract_status") @mock.patch(M_PATH + "esm.ESMAppsEntitlement.user_facing_status") @mock.patch(M_PATH + "esm.ESMAppsEntitlement.contract_status") @mock.patch(M_PATH + "repo.RepoEntitlement.user_facing_status") @mock.patch(M_PATH + "repo.RepoEntitlement.contract_status") def test_attached_reports_contract_and_service_status( self, m_repo_contract_status, m_repo_uf_status, m_esm_contract_status, m_esm_uf_status, m_livepatch_contract_status, m_livepatch_uf_status, _m_livepatch_status, _m_fips_status, _m_getuid, _m_should_reboot, m_remove_notice, entitlements, features_override, show_beta, FakeConfig, ): """When attached, return contract and service user-facing status.""" m_repo_contract_status.return_value = ContractStatus.ENTITLED m_repo_uf_status.return_value = ( UserFacingStatus.INAPPLICABLE, messages.NamedMessage("test-code", "repo details"), ) m_livepatch_contract_status.return_value = ContractStatus.ENTITLED m_livepatch_uf_status.return_value = ( UserFacingStatus.ACTIVE, messages.NamedMessage("test-code", "livepatch details"), ) m_esm_contract_status.return_value = ContractStatus.ENTITLED m_esm_uf_status.return_value = ( UserFacingStatus.ACTIVE, messages.NamedMessage("test-code", "esm-apps details"), ) token = { "availableResources": ALL_RESOURCES_AVAILABLE, "machineTokenInfo": { "machineId": "test_machine_id", "accountInfo": { "id": "1", "name": "accountname", "createdAt": "2019-06-14T06:45:50Z", "externalAccountIDs": [{ "IDs": ["id1"], "Origin": "AWS" }], }, "contractInfo": { "id": "contract-1", "name": "contractname", "createdAt": "2020-05-08T19:02:26Z", "resourceEntitlements": entitlements, "products": ["free"], }, }, } cfg = FakeConfig.for_attached_machine(account_name="accountname", machine_token=token) if features_override: cfg.override_features(features_override) if not entitlements: support_level = UserFacingStatus.INAPPLICABLE.value else: support_level = entitlements[0]["affordances"]["supportLevel"] expected = copy.deepcopy(status.DEFAULT_STATUS) expected.update({ "version": version.get_version(features=cfg.features), "attached": True, "machine_id": "test_machine_id", "contract": { "name": "contractname", "id": "contract-1", "created_at": datetime.datetime(2020, 5, 8, 19, 2, 26, tzinfo=datetime.timezone.utc), "products": ["free"], "tech_support_level": support_level, }, "account": { "name": "accountname", "id": "1", "created_at": datetime.datetime(2019, 6, 14, 6, 45, 50, tzinfo=datetime.timezone.utc), "external_account_ids": [{ "IDs": ["id1"], "Origin": "AWS" }], }, }) for cls in ENTITLEMENT_CLASSES: if cls.name == "livepatch": expected_status = UserFacingStatus.ACTIVE.value details = "livepatch details" elif cls.name == "esm-apps": expected_status = UserFacingStatus.ACTIVE.value details = "esm-apps details" else: expected_status = UserFacingStatus.INAPPLICABLE.value details = "repo details" if self.check_beta(cls, show_beta, cfg, expected_status): continue expected["services"].append({ "name": cls.name, "description": cls.description, "entitled": ContractStatus.ENTITLED.value, "status": expected_status, "status_details": details, "description_override": None, "available": mock.ANY, "blocked_by": [], }) with mock.patch( "uaclient.status._get_config_status") as m_get_cfg_status: m_get_cfg_status.return_value = DEFAULT_CFG_STATUS assert expected == status.status(cfg=cfg, show_beta=show_beta) assert len(ENTITLEMENT_CLASSES) - 2 == m_repo_uf_status.call_count assert 1 == m_livepatch_uf_status.call_count expected_calls = [ mock.call( "", messages.NOTICE_DAEMON_AUTO_ATTACH_LOCK_HELD.format( operation=".*"), ), mock.call("", messages.NOTICE_DAEMON_AUTO_ATTACH_FAILED), mock.call( "", messages.ENABLE_REBOOT_REQUIRED_TMPL.format( operation="fix operation"), ), ] assert expected_calls == m_remove_notice.call_args_list @mock.patch("uaclient.status.get_available_resources") @mock.patch("uaclient.status.os.getuid") def test_expires_handled_appropriately( self, m_getuid, _m_get_available_resources, _m_should_reboot, _m_remove_notice, FakeConfig, ): token = { "availableResources": ALL_RESOURCES_AVAILABLE, "machineTokenInfo": { "machineId": "test_machine_id", "accountInfo": { "id": "1", "name": "accountname" }, "contractInfo": { "name": "contractname", "id": "contract-1", "effectiveTo": "2020-07-18T00:00:00Z", "createdAt": "2020-05-08T19:02:26Z", "resourceEntitlements": [], "products": ["free"], }, }, } cfg = FakeConfig.for_attached_machine(account_name="accountname", machine_token=token) # Test that root's status works as expected (including the cache write) m_getuid.return_value = 0 expected_dt = datetime.datetime(2020, 7, 18, 0, 0, 0, tzinfo=datetime.timezone.utc) assert expected_dt == status.status(cfg=cfg)["expires"] # Test that the read from the status cache work properly for non-root # users m_getuid.return_value = 1000 assert expected_dt == status.status(cfg=cfg)["expires"] @mock.patch("uaclient.status.os.getuid") def test_nonroot_user_uses_cache_and_updates_if_available( self, m_getuid, _m_should_reboot, m_remove_notice, tmpdir): m_getuid.return_value = 1000 expected_status = {"pass": True} cfg = UAConfig({"data_dir": tmpdir.strpath}) cfg.write_cache("marker-reboot-cmds", "") # To indicate a reboot reqd cfg.write_cache("status-cache", expected_status) # Even non-root users can update execution_status details details = messages.ENABLE_REBOOT_REQUIRED_TMPL.format( operation="configuration changes") reboot_required = UserFacingConfigStatus.REBOOTREQUIRED.value expected_status.update({ "execution_status": reboot_required, "execution_details": details, "notices": [], "config_path": None, "config": { "data_dir": mock.ANY }, }) assert expected_status == status.status(cfg=cfg)
def simulate_status( cfg, token: str, show_beta: bool = False ) -> Tuple[Dict[str, Any], int]: """Get a status dictionary based on a token. Returns a tuple with the status dictionary and an integer value - 0 for success, 1 for failure """ ret = 0 response = copy.deepcopy(DEFAULT_STATUS) try: contract_information = get_contract_information(cfg, token) except exceptions.ContractAPIError as e: if hasattr(e, "code") and e.code == 401: raise exceptions.UserFacingError( msg=messages.ATTACH_INVALID_TOKEN.msg, msg_code=messages.ATTACH_INVALID_TOKEN.name, ) raise e contract_info = contract_information.get("contractInfo", {}) account_info = contract_information.get("accountInfo", {}) response.update( { "version": version.get_version(features=cfg.features), "contract": { "id": contract_info.get("id", ""), "name": contract_info.get("name", ""), "created_at": contract_info.get("createdAt", ""), "products": contract_info.get("products", []), }, "account": { "name": account_info.get("name", ""), "id": account_info.get("id"), "created_at": account_info.get("createdAt", ""), "external_account_ids": account_info.get( "externalAccountIDs", [] ), }, "simulated": True, } ) now = datetime.now(timezone.utc) if contract_info.get("effectiveTo"): response["expires"] = contract_info.get("effectiveTo") expiration_datetime = util.parse_rfc3339_date(response["expires"]) delta = expiration_datetime - now if delta.total_seconds() <= 0: message = messages.ATTACH_FORBIDDEN_EXPIRED.format( contract_id=response["contract"]["id"], date=expiration_datetime.strftime(ATTACH_FAIL_DATE_FORMAT), ) event.error(error_msg=message.msg, error_code=message.name) event.info("This token is not valid.\n" + message.msg + "\n") ret = 1 if contract_info.get("effectiveFrom"): response["effective"] = contract_info.get("effectiveFrom") effective_datetime = util.parse_rfc3339_date(response["effective"]) delta = now - effective_datetime if delta.total_seconds() <= 0: message = messages.ATTACH_FORBIDDEN_NOT_YET.format( contract_id=response["contract"]["id"], date=effective_datetime.strftime(ATTACH_FAIL_DATE_FORMAT), ) event.error(error_msg=message.msg, error_code=message.name) event.info("This token is not valid.\n" + message.msg + "\n") ret = 1 status_cache = cfg.read_cache("status-cache") if status_cache: resources = status_cache.get("services") else: resources = get_available_resources(cfg) entitlements = contract_info.get("resourceEntitlements", []) inapplicable_resources = [ resource["name"] for resource in sorted(resources, key=lambda x: x["name"]) if not resource["available"] ] for resource in resources: entitlement_name = resource.get("name", "") try: ent_cls = entitlement_factory(cfg=cfg, name=entitlement_name) except exceptions.EntitlementNotFoundError: continue ent = ent_cls(cfg=cfg) entitlement_information = _get_entitlement_information( entitlements, entitlement_name ) response["services"].append( { "name": resource.get("presentedAs", ent.name), "description": ent.description, "entitled": entitlement_information["entitled"], "auto_enabled": entitlement_information["auto_enabled"], "available": "yes" if ent.name not in inapplicable_resources else "no", } ) response["services"].sort(key=lambda x: x.get("name", "")) support = _get_entitlement_information(entitlements, "support") if support["entitled"]: supportLevel = support["affordances"].get("supportLevel") if supportLevel: response["contract"]["tech_support_level"] = supportLevel response.update(_get_config_status(cfg)) response = _handle_beta_resources(cfg, show_beta, response) return response, ret
def _attached_status(cfg) -> Dict[str, Any]: """Return configuration of attached status as a dictionary.""" cfg.remove_notice( "", messages.NOTICE_DAEMON_AUTO_ATTACH_LOCK_HELD.format(operation=".*"), ) cfg.remove_notice("", messages.NOTICE_DAEMON_AUTO_ATTACH_FAILED) response = copy.deepcopy(DEFAULT_STATUS) machineTokenInfo = cfg.machine_token["machineTokenInfo"] contractInfo = machineTokenInfo["contractInfo"] tech_support_level = UserFacingStatus.INAPPLICABLE.value response.update( { "version": version.get_version(features=cfg.features), "machine_id": machineTokenInfo["machineId"], "attached": True, "origin": contractInfo.get("origin"), "notices": cfg.read_cache("notices") or [], "contract": { "id": contractInfo["id"], "name": contractInfo["name"], "created_at": contractInfo.get("createdAt", ""), "products": contractInfo.get("products", []), "tech_support_level": tech_support_level, }, "account": { "name": cfg.accounts[0]["name"], "id": cfg.accounts[0]["id"], "created_at": cfg.accounts[0].get("createdAt", ""), "external_account_ids": cfg.accounts[0].get( "externalAccountIDs", [] ), }, } ) if contractInfo.get("effectiveTo"): response["expires"] = cfg.contract_expiry_datetime if contractInfo.get("effectiveFrom"): response["effective"] = contractInfo["effectiveFrom"] resources = cfg.machine_token.get("availableResources") if not resources: resources = get_available_resources(cfg) inapplicable_resources = { resource["name"]: resource.get("description") for resource in sorted(resources, key=lambda x: x.get("name", "")) if not resource.get("available") } for resource in resources: try: ent_cls = entitlement_factory( cfg=cfg, name=resource.get("name", "") ) except exceptions.EntitlementNotFoundError: continue ent = ent_cls(cfg) response["services"].append( _attached_service_status(ent, inapplicable_resources) ) response["services"].sort(key=lambda x: x.get("name", "")) support = cfg.entitlements.get("support", {}).get("entitlement") if support: supportLevel = support.get("affordances", {}).get("supportLevel") if supportLevel: response["contract"]["tech_support_level"] = supportLevel return response