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
def attach_with_token( cfg: config.UAConfig, token: str, allow_enable: bool ) -> None: """ Common functionality to take a token and attach via contract backend :raise UrlError: On unexpected connectivity issues to contract server or inability to access identity doc from metadata service. :raise ContractAPIError: On unexpected errors when talking to the contract server. """ from uaclient.jobs.update_messaging import update_apt_and_motd_messages try: contract.request_updated_contract( cfg, token, allow_enable=allow_enable ) except exceptions.UrlError as exc: # Persist updated status in the event of partial attach ua_status.status(cfg=cfg) update_apt_and_motd_messages(cfg) raise exc except exceptions.UserFacingError as exc: # Persist updated status in the event of partial attach ua_status.status(cfg=cfg) update_apt_and_motd_messages(cfg) raise exc current_iid = identity.get_instance_id() if current_iid: cfg.write_cache("instance-id", current_iid) update_apt_and_motd_messages(cfg)
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"]
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
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
def test_status_updated_when_auto_enable_fails( self, request_updated_contract, m_update_apt_and_motd_msgs, _m_get_available_resources, _m_should_reboot, _m_remove_notice, _m_get_uid, error_class, error_str, FakeConfig, event, ): """If auto-enable of a service fails, attach status is updated.""" token = "contract-token" args = mock.MagicMock(token=token, attach_config=None) cfg = FakeConfig() status.status(cfg=cfg) # persist unattached status # read persisted status cache from disk orig_unattached_status = cfg.read_cache("status-cache") def fake_request_updated_contract(cfg, contract_token, allow_enable): cfg.write_cache("machine-token", ENTITLED_MACHINE_TOKEN) raise error_class(error_str) request_updated_contract.side_effect = fake_request_updated_contract with pytest.raises(SystemExit) as excinfo: main_error_handler(action_attach)(args, cfg) assert 1 == excinfo.value.code assert cfg.is_attached # Assert updated status cache is written to disk assert orig_unattached_status != cfg.read_cache( "status-cache" ), "Did not persist on disk status during attach failure" assert [mock.call(cfg)] == m_update_apt_and_motd_msgs.call_args_list
def status( cfg: config.UAConfig, *, simulate_with_token: Optional[str] = None, show_beta: bool = False ): """ Construct the current UA status dictionary. """ if simulate_with_token: status, ret = ua_status.simulate_status( cfg=cfg, token=simulate_with_token, show_beta=show_beta ) else: status = ua_status.status(cfg=cfg, show_beta=show_beta) ret = 0 return status, ret
def get_ua_info(cfg: UAConfig) -> Dict[str, Any]: """Returns the UA information based on the config object.""" ua_info = { "attached": False, "enabled_services": [], "entitled_services": [], } # type: Dict[str, Any] status_dict = status(cfg=cfg, show_beta=True) if status_dict["attached"]: ua_info["attached"] = True for service in status_dict["services"]: if service["name"] in ESM_SERVICES: if service["entitled"] == "yes": ua_info["entitled_services"].append(service["name"]) if service["status"] == "enabled": ua_info["enabled_services"].append(service["name"]) return ua_info
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
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 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
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)
def update_status(cfg: UAConfig) -> bool: if cfg.is_attached: status(cfg=cfg) return True