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
Пример #3
0
 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
Пример #7
0
    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
Пример #10
0
def get_version(_args=None, _cfg=None):
    if _cfg is None:
        _cfg = config.UAConfig()

    return version.get_version(features=_cfg.features)
Пример #11
0
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'
     }
Пример #14
0
 def test_get_version_returns_packaged_version(self, m_subp):
     assert "24.1~18.04.1" == get_version()
     assert 0 == m_subp.call_count
Пример #15
0
    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
Пример #18
0
    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
Пример #19
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)
Пример #20
0
    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
Пример #22
0
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
Пример #23
0
def print_version(_args=None, _cfg=None):
    print(version.get_version())
Пример #24
0
    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,