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 test_report_machine_activity( self, get_machine_id, request_url, activity_id, enabled_services, FakeConfig, ): """POST machine activity report to the server.""" machine_id = "machineId" get_machine_id.return_value = machine_id request_url.return_value = ( { "activityToken": "test-token", "activityID": "test-id", "activityPingInterval": 5, }, None, ) cfg = FakeConfig.for_attached_machine() client = UAContractClient(cfg) def entitlement_user_facing_status(self): if self.name in enabled_services: return (UserFacingStatus.ACTIVE, "") return (UserFacingStatus.INACTIVE, "") with mock.patch.object(type(cfg), "activity_id", activity_id): with mock.patch.object( UAEntitlement, "user_facing_status", new=entitlement_user_facing_status, ): with mock.patch("uaclient.config.UAConfig.write_cache" ) as m_write_cache: client.report_machine_activity() expected_write_calls = 1 assert expected_write_calls == m_write_cache.call_count expected_activity_id = activity_id if activity_id else machine_id params = { "headers": { "user-agent": "UA-Client/{}".format(get_version()), "accept": "application/json", "content-type": "application/json", "Authorization": "Bearer not-null", }, "data": { "activityToken": None, "activityID": expected_activity_id, "resources": enabled_services, }, } assert [ mock.call("/v1/contracts/cid/machine-activity/machineId", **params) ] == request_url.call_args_list
def test_get_version_returns_matching_git_describe_long( self, m_exists, m_subp): m_subp.return_value = ("24.1-5-g12345678", "") assert "24.1-5-g12345678" == get_version() assert [ mock.call( ["git", "describe", "--abbrev=8", "--match=[0-9]*", "--long"]) ] == m_subp.call_args_list top_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) top_dir_git = os.path.join(top_dir, ".git") assert [mock.call(top_dir_git)] == m_exists.call_args_list
def test_get_version_returns_matching_git_describe_long( self, m_exists, m_subp, features, suffix): m_subp.return_value = ("24.1-5-g12345678", "") with mock.patch("uaclient.version.PACKAGED_VERSION", "@@PACKAGED_VERSION"): assert "24.1-5-g12345678" + suffix == get_version( features=features) assert [ mock.call( ["git", "describe", "--abbrev=8", "--match=[0-9]*", "--long"]) ] == m_subp.call_args_list top_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) top_dir_git = os.path.join(top_dir, ".git") assert [mock.call(top_dir_git)] == m_exists.call_args_list
def test__request_contract_machine_attach( self, m_platform_data, get_machine_id, request_url, machine_id_response, machine_id_param, FakeConfig, ): def fake_platform_data(machine_id): machine_id = "machine-id" if not machine_id else machine_id return {"machineId": machine_id} m_platform_data.side_effect = fake_platform_data machine_token = {"machineTokenInfo": {}} if machine_id_response: machine_token["machineTokenInfo"][ "machineId"] = machine_id_response request_url.return_value = (machine_token, {}) contract_token = "mToken" params = { "data": fake_platform_data(machine_id_param), "headers": { "user-agent": "UA-Client/{}".format(get_version()), "accept": "application/json", "content-type": "application/json", "Authorization": "Bearer mToken", }, } cfg = FakeConfig() client = UAContractClient(cfg) client.request_contract_machine_attach(contract_token=contract_token, machine_id=machine_id_param) assert [mock.call("/v1/context/machines/token", **params)] == request_url.call_args_list expected_machine_id = "contract-machine-id" if not machine_id_response: if machine_id_param: expected_machine_id = machine_id_param else: expected_machine_id = "machine-id" assert expected_machine_id == cfg.read_cache("machine-id")
def test_request_contract_information(self, _m_machine_id, m_request_url, FakeConfig): m_request_url.return_value = ("response", {}) cfg = FakeConfig.for_attached_machine() client = UAContractClient(cfg) params = { "headers": { "user-agent": "UA-Client/{}".format(get_version()), "accept": "application/json", "content-type": "application/json", "Authorization": "Bearer some_token", } } assert "response" == client.request_contract_information("some_token") assert [mock.call("/v1/contract", **params)] == m_request_url.call_args_list
def test__request_machine_token_update( self, get_platform_info, get_machine_id, request_url, detach, expected_http_method, FakeConfig, ): """POST or DELETE to ua-contracts and write machine-token cache. Setting detach=True will result in a DELETE operation. """ get_platform_info.return_value = {"arch": "arch", "kernel": "kernel"} get_machine_id.return_value = "machineId" request_url.return_value = ("newtoken", {}) cfg = FakeConfig.for_attached_machine() client = UAContractClient(cfg) kwargs = {"machine_token": "mToken", "contract_id": "cId"} if detach is not None: kwargs["detach"] = detach client._request_machine_token_update(**kwargs) if not detach: # Then we have written the updated cache assert "newtoken" == cfg.read_cache("machine-token") params = { "headers": { "user-agent": "UA-Client/{}".format(get_version()), "accept": "application/json", "content-type": "application/json", "Authorization": "Bearer mToken", }, "method": expected_http_method, } if expected_http_method != "DELETE": params["data"] = { "machineId": "machineId", "architecture": "arch", "os": { "kernel": "kernel" }, } assert [ mock.call("/v1/contracts/cId/context/machines/machineId", **params) ] == request_url.call_args_list
def test_request_resource_machine_access(self, get_machine_id, request_url, FakeConfig): """GET from resource-machine-access route to "enable" a service""" get_machine_id.return_value = "machineId" request_url.return_value = ("response", {}) cfg = FakeConfig.for_attached_machine() client = UAContractClient(cfg) kwargs = {"machine_token": "mToken", "resource": "cis"} assert "response" == client.request_resource_machine_access(**kwargs) assert "response" == cfg.read_cache("machine-access-cis") params = { "headers": { "user-agent": "UA-Client/{}".format(get_version()), "accept": "application/json", "content-type": "application/json", "Authorization": "Bearer mToken", } } assert [ mock.call("/v1/resources/cis/context/machines/machineId", **params) ] == request_url.call_args_list
def test_returns_dpkg_parsechangelog_on_git_ubuntu_pkg_branch( self, m_exists, m_subp): """Call dpkg-parsechangelog if git describe fails to --match=[0-9]*""" def fake_subp(cmd): if cmd[0] == "git": # Not matching tag on git-ubuntu pkg branches raise exceptions.ProcessExecutionError( "fatal: No names found, cannot describe anything.") if cmd[0] == "dpkg-parsechangelog": return ("24.1\n", "") assert False, "Unexpected subp cmd {}".format(cmd) m_subp.side_effect = fake_subp assert "24.1" == get_version() expected_calls = [ mock.call( ["git", "describe", "--abbrev=8", "--match=[0-9]*", "--long"]), mock.call(["dpkg-parsechangelog", "-S", "version"]), ] assert expected_calls == m_subp.call_args_list top_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) top_dir_git = os.path.join(top_dir, ".git") assert [mock.call(top_dir_git)] == m_exists.call_args_list
def get_version(_args=None, _cfg=None): if _cfg is None: _cfg = config.UAConfig() return version.get_version(features=_cfg.features)
def _get_version(): parts = version.get_version().split("-") if len(parts) == 1: return parts[0] major_minor, _subrev, _commitish = parts return major_minor
def test_get_version_returns_packaged_version(self, m_exists, m_subp, features, suffix): with mock.patch("uaclient.version.PACKAGED_VERSION", "24.1~18.04.1"): assert "24.1~18.04.1" + suffix == get_version(features=features) assert 0 == m_subp.call_count
def headers(self): return { 'user-agent': 'UA-Client/%s' % version.get_version(), 'accept': 'application/json', 'content-type': 'application/json' }
def test_get_version_returns_packaged_version(self, m_subp): assert "24.1~18.04.1" == get_version() assert 0 == m_subp.call_count
def test_attached_formats( self, _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, use_all, environ, format_type, event_logger_mode, capsys, FakeConfig, event, ): """Check that unattached status json output is emitted to console""" cfg = FakeConfig.for_attached_machine() args = mock.MagicMock(format=format_type, all=use_all, simulate_with_token=None) with mock.patch.object(os, "environ", environ): with mock.patch.object(event, "_event_logger_mode", event_logger_mode), mock.patch.object( event, "_command", "status"): assert 0 == action_status(args, cfg=cfg) expected_environment = [] if environ: expected_environment = [ { "name": "UA_CONFIG_FILE", "value": "config_file" }, { "name": "UA_DATA_DIR", "value": "data_dir" }, { "name": "UA_FEATURES_ALLOW_BETA", "value": "true" }, ] if use_all: services = SERVICES_JSON_ALL else: services = [ svc for svc in SERVICES_JSON_ALL if svc["name"] not in BETA_SVC_NAMES ] inapplicable_services = [ service["name"] for service in RESPONSE_AVAILABLE_SERVICES if not service["available"] ] filtered_services = [ service for service in services if service["name"] not in inapplicable_services ] if format_type == "json": contract_created_at = "2020-05-08T19:02:26+00:00" account_created_at = "2019-06-14T06:45:50+00:00" expires = "2040-05-08T19:02:26+00:00" effective = "2000-05-08T19:02:26+00:00" else: contract_created_at = datetime.datetime( 2020, 5, 8, 19, 2, 26, tzinfo=datetime.timezone.utc) account_created_at = datetime.datetime( 2019, 6, 14, 6, 45, 50, tzinfo=datetime.timezone.utc) expires = datetime.datetime(2040, 5, 8, 19, 2, 26, tzinfo=datetime.timezone.utc) effective = datetime.datetime(2000, 5, 8, 19, 2, 26, tzinfo=datetime.timezone.utc) tech_support_level = status.UserFacingStatus.INAPPLICABLE.value expected = { "_doc": ("Content provided in json response is currently " "considered Experimental and may change"), "_schema_version": "0.1", "version": version.get_version(features=cfg.features), "execution_status": status.UserFacingConfigStatus.INACTIVE.value, "execution_details": messages.NO_ACTIVE_OPERATIONS, "attached": True, "machine_id": "test_machine_id", "effective": effective, "expires": expires, "notices": [], "services": filtered_services, "environment_vars": expected_environment, "contract": { "id": "cid", "name": "test_contract", "created_at": contract_created_at, "products": ["free"], "tech_support_level": tech_support_level, }, "account": { "id": "acct-1", "name": "test_account", "created_at": account_created_at, "external_account_ids": [{ "IDs": ["id1"], "Origin": "AWS" }], }, "config_path": None, "config": { "data_dir": mock.ANY }, "simulated": False, "errors": [], "warnings": [], "result": "success", } if format_type == "json": assert expected == json.loads(capsys.readouterr()[0]) else: yaml_output = yaml.safe_load(capsys.readouterr()[0]) # On earlier versions of pyyaml, we don't add the timezone # info when converting a date string into a datetime object. # Since we only want to test if we are producing a valid # yaml file in the status output, we can manually add # the timezone info to make the test work as expected for key, value in yaml_output.items(): if isinstance(value, datetime.datetime): yaml_output[key] = value.replace( tzinfo=datetime.timezone.utc) elif isinstance(value, dict): for inner_key, inner_value in value.items(): if isinstance(inner_value, datetime.datetime): yaml_output[key][inner_key] = inner_value.replace( tzinfo=datetime.timezone.utc) assert expected == yaml_output
def test__request_machine_token_update( self, get_platform_info, get_machine_id, request_url, detach, expected_http_method, machine_id_response, activity_id, FakeConfig, ): """POST or DELETE to ua-contracts and write machine-token cache. Setting detach=True will result in a DELETE operation. """ get_platform_info.return_value = {"arch": "arch", "kernel": "kernel"} get_machine_id.return_value = "machineId" machine_token = {"machineTokenInfo": {}} if machine_id_response: machine_token["machineTokenInfo"][ "machineId"] = machine_id_response request_url.return_value = (machine_token, {}) cfg = FakeConfig.for_attached_machine() client = UAContractClient(cfg) kwargs = {"machine_token": "mToken", "contract_id": "cId"} if detach is not None: kwargs["detach"] = detach enabled_services = ["esm-apps", "livepatch"] def entitlement_user_facing_status(self): if self.name in enabled_services: return (UserFacingStatus.ACTIVE, "") return (UserFacingStatus.INACTIVE, "") with mock.patch.object(type(cfg), "activity_id", activity_id): with mock.patch.object( UAEntitlement, "user_facing_status", new=entitlement_user_facing_status, ): client._request_machine_token_update(**kwargs) if not detach: # Then we have written the updated cache assert machine_token == cfg.read_cache("machine-token") expected_machine_id = "machineId" if machine_id_response: expected_machine_id = machine_id_response assert expected_machine_id == cfg.read_cache("machine-id") params = { "headers": { "user-agent": "UA-Client/{}".format(get_version()), "accept": "application/json", "content-type": "application/json", "Authorization": "Bearer mToken", }, "method": expected_http_method, } if expected_http_method != "DELETE": expected_activity_id = activity_id if activity_id else "machineId" params["data"] = { "machineId": "machineId", "architecture": "arch", "os": { "kernel": "kernel" }, "activityInfo": { "activityToken": None, "activityID": expected_activity_id, "resources": enabled_services, }, } assert request_url.call_args_list == [ mock.call("/v1/contracts/cId/context/machines/machineId", **params) ]
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
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 test_simulated_formats( self, _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, use_all, format_type, event_logger_mode, capsys, FakeConfig, event, ): """Check that simulated status json output is emitted to console""" cfg = FakeConfig() args = mock.MagicMock(format=format_type, all=use_all, simulate_with_token="some_token") with mock.patch.object(event, "_event_logger_mode", event_logger_mode), mock.patch.object( event, "_command", "status"): assert 0 == action_status(args, cfg=cfg) expected_services = [ { "auto_enabled": "yes", "available": "yes", "description": "UA Apps: Extended Security Maintenance (ESM)", "entitled": "no", "name": "esm-apps", }, { "auto_enabled": "yes", "available": "no", "description": "UA Infra: Extended Security Maintenance (ESM)", "entitled": "yes", "name": "esm-infra", }, { "auto_enabled": "no", "available": "no", "description": "NIST-certified core packages", "entitled": "no", "name": "fips", }, { "auto_enabled": "no", "available": "no", "description": "NIST-certified core packages with priority" " security updates", "entitled": "no", "name": "fips-updates", }, { "auto_enabled": "no", "available": "yes", "description": "Canonical Livepatch service", "entitled": "yes", "name": "livepatch", }, { "auto_enabled": "no", "available": "no", "description": "Beta-version Ubuntu Kernel with PREEMPT_RT" " patches", "entitled": "no", "name": "realtime-kernel", }, { "auto_enabled": "no", "available": "no", "description": "Security Updates for the Robot Operating" " System", "entitled": "no", "name": "ros", }, { "auto_enabled": "no", "available": "no", "description": "All Updates for the Robot Operating System", "entitled": "no", "name": "ros-updates", }, ] if not use_all: expected_services = expected_services[1:-3] expected = { "_doc": "Content provided in json response is currently considered" " Experimental and may change", "_schema_version": "0.1", "attached": False, "machine_id": None, "notices": [], "account": { "created_at": "2019-06-14T06:45:50Z", "external_account_ids": [], "id": "some_id", "name": "Name", }, "contract": { "created_at": "2021-05-21T20:00:53Z", "id": "some_id", "name": "Name", "products": ["uai-essential-virtual"], "tech_support_level": "essential", }, "environment_vars": [], "execution_status": "inactive", "execution_details": "No Ubuntu Advantage operations are running", "expires": "9999-12-31T00:00:00Z", "effective": None, "services": expected_services, "simulated": True, "version": version.get_version(features=cfg.features), "config_path": None, "config": { "data_dir": mock.ANY }, "errors": [], "warnings": [], "result": "success", } if format_type == "json": assert expected == json.loads(capsys.readouterr()[0]) else: assert expected == yaml.safe_load(capsys.readouterr()[0])
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 get_parser(): service_line_tmpl = " - {name}: {description}{url}" description_lines = [__doc__] sorted_classes = sorted(entitlements.ENTITLEMENT_CLASS_BY_NAME.items()) for name, ent_cls in sorted_classes: if ent_cls.help_doc_url: url = " ({})".format(ent_cls.help_doc_url) else: url = "" service_line = service_line_tmpl.format( name=name, description=ent_cls.description, url=url) if len(service_line) <= 80: description_lines.append(service_line) else: wrapped_words = [] line = service_line while len(line) > 80: [line, wrapped_word] = line.rsplit(" ", 1) wrapped_words.insert(0, wrapped_word) description_lines.extend([line, " " + " ".join(wrapped_words)]) parser = argparse.ArgumentParser( prog=NAME, formatter_class=argparse.RawDescriptionHelpFormatter, description="\n".join(description_lines), usage=USAGE_TMPL.format(name=NAME, command="[command]"), epilog=EPILOG_TMPL.format(name=NAME, command="[command]"), ) parser.add_argument( "--debug", action="store_true", help="show all debug log messages to console", ) parser.add_argument( "--version", action="version", version=version.get_version(), help="show version of {}".format(NAME), ) parser._optionals.title = "Flags" subparsers = parser.add_subparsers(title="Available Commands", dest="command", metavar="") subparsers.required = True parser_status = subparsers.add_parser( "status", help="current status of all Ubuntu Advantage services") parser_status.set_defaults(action=action_status) status_parser(parser_status) parser_attach = subparsers.add_parser( "attach", help="attach this machine to an Ubuntu Advantage subscription", ) attach_parser(parser_attach) parser_attach.set_defaults(action=action_attach) parser_auto_attach = subparsers.add_parser( "auto-attach", help="automatically attach Ubuntu Advantage on supported platforms", ) auto_attach_parser(parser_auto_attach) parser_auto_attach.set_defaults(action=action_auto_attach) parser_detach = subparsers.add_parser( "detach", help="remove this machine from an Ubuntu Advantage subscription", ) detach_parser(parser_detach) parser_detach.set_defaults(action=action_detach) parser_enable = subparsers.add_parser( "enable", help="enable a specific Ubuntu Advantage service on this machine", ) enable_parser(parser_enable) parser_enable.set_defaults(action=action_enable) parser_disable = subparsers.add_parser( "disable", help="disable a specific Ubuntu Advantage service on this machine", ) disable_parser(parser_disable) parser_disable.set_defaults(action=action_disable) parser_refresh = subparsers.add_parser( "refresh", help="refresh Ubuntu Advantage services from contracts server", ) parser_refresh.set_defaults(action=action_refresh) parser_version = subparsers.add_parser( "version", help="show version of {}".format(NAME)) parser_version.set_defaults(action=print_version) parser_help = subparsers.add_parser("help", help="show this help message and exit") parser_help.set_defaults(action=action_help) return parser
def print_version(_args=None, _cfg=None): print(version.get_version())
def test_unattached_formats( self, _m_getuid, _m_get_contract_information, _m_get_avail_resources, _m_should_reboot, _m_remove_notice, _m_contract_changed, use_all, environ, format_type, event_logger_mode, capsys, FakeConfig, event, ): """Check that unattached status json output is emitted to console""" cfg = FakeConfig() args = mock.MagicMock(format=format_type, all=use_all, simulate_with_token=None) with mock.patch.object(os, "environ", environ): with mock.patch.object(event, "_event_logger_mode", event_logger_mode), mock.patch.object( event, "_command", "status"): assert 0 == action_status(args, cfg=cfg) expected_environment = [] if environ: expected_environment = [ { "name": "UA_CONFIG_FILE", "value": "config_file" }, { "name": "UA_DATA_DIR", "value": "data_dir" }, { "name": "UA_FEATURES_ALLOW_BETA", "value": "true" }, ] expected_services = [ { "name": "esm-apps", "description": "UA Apps: Extended Security Maintenance (ESM)", "available": "yes", }, { "name": "livepatch", "description": "Canonical Livepatch service", "available": "yes", }, ] if not use_all: expected_services.pop(0) expected = { "_doc": ("Content provided in json response is currently " "considered Experimental and may change"), "_schema_version": "0.1", "version": version.get_version(features=cfg.features), "execution_status": status.UserFacingConfigStatus.INACTIVE.value, "execution_details": messages.NO_ACTIVE_OPERATIONS, "attached": False, "machine_id": None, "effective": None, "expires": None, "notices": [], "services": expected_services, "environment_vars": expected_environment, "contract": { "id": "", "name": "", "created_at": "", "products": [], "tech_support_level": "n/a", }, "account": { "name": "", "id": "", "created_at": "", "external_account_ids": [], }, "config_path": None, "config": { "data_dir": mock.ANY }, "simulated": False, "errors": [], "warnings": [], "result": "success", } if format_type == "json": assert expected == json.loads(capsys.readouterr()[0]) else: assert expected == yaml.safe_load(capsys.readouterr()[0])
def headers(self): return { "user-agent": "UA-Client/{}".format(version.get_version()), "accept": "application/json", "content-type": "application/json", }
STATUS_SIMULATED_TMPL = """\ {name: <17}{available: <11}{entitled: <11}{auto_enabled: <14}{description}""" STATUS_HEADER = "SERVICE ENTITLED STATUS DESCRIPTION" # The widths listed below for entitled and status are actually 9 characters # less than reality because we colorize the values in entitled and status # columns. Colorizing has an opening and closing set of unprintable characters # that factor into formats len() calculations STATUS_TMPL = "{name: <17}{entitled: <19}{status: <19}{description}" DEFAULT_STATUS = { "_doc": "Content provided in json response is currently considered" " Experimental and may change", "_schema_version": "0.1", "version": version.get_version(), "machine_id": None, "attached": False, "effective": None, "expires": None, # TODO Will this break something? "origin": None, "services": [], "execution_status": UserFacingConfigStatus.INACTIVE.value, "execution_details": messages.NO_ACTIVE_OPERATIONS, "notices": [], "contract": { "id": "", "name": "", "created_at": "", "products": [], "tech_support_level": UserFacingStatus.INAPPLICABLE.value,