def write_esm_announcement_message(cfg: config.UAConfig, series: str) -> None:
    """Write human-readable messages if ESM is offered on this LTS release.

    Do not write ESM announcements if esm-apps is enabled or beta.

    :param cfg: UAConfig instance for this environment.
    :param series: string of Ubuntu release series: 'xenial'.
    """
    apps_cls = entitlements.entitlement_factory(cfg=cfg, name="esm-apps")
    apps_inst = apps_cls(cfg)
    enabled_status = ApplicationStatus.ENABLED
    apps_not_enabled = apps_inst.application_status()[0] != enabled_status
    config_allow_beta = util.is_config_value_true(
        config=cfg.cfg, path_to_value="features.allow_beta")
    apps_not_beta = bool(config_allow_beta or not apps_cls.is_beta)

    msg_dir = os.path.join(cfg.data_dir, "messages")
    esm_news_file = os.path.join(msg_dir, ExternalMessage.ESM_ANNOUNCE.value)
    platform_info = util.get_platform_info()
    is_active_esm = util.is_active_esm(platform_info["series"])
    if is_active_esm:
        ua_esm_url = defaults.EOL_UA_URL_TMPL.format(
            hyphenatedrelease=platform_info["release"].replace(".", "-"))
    else:
        ua_esm_url = defaults.BASE_ESM_URL
    if apps_not_beta and apps_not_enabled:
        util.write_file(esm_news_file,
                        "\n" + ANNOUNCE_ESM_TMPL.format(url=ua_esm_url))
    else:
        util.remove_file(esm_news_file)
def _handle_beta_resources(cfg, show_beta, response) -> Dict[str, Any]:
    """Remove beta services from response dict if needed"""
    config_allow_beta = util.is_config_value_true(
        config=cfg.cfg, path_to_value="features.allow_beta"
    )
    show_beta |= config_allow_beta
    if show_beta:
        return response

    new_response = copy.deepcopy(response)

    released_resources = []
    for resource in new_response.get("services", {}):
        resource_name = resource["name"]
        try:
            ent_cls = entitlement_factory(cfg=cfg, name=resource_name)
        except exceptions.EntitlementNotFoundError:
            """
            Here we cannot know the status of a service,
            since it is not listed as a valid entitlement.
            Therefore, we keep this service in the list, since
            we cannot validate if it is a beta service or not.
            """
            released_resources.append(resource)
            continue

        enabled_status = UserFacingStatus.ACTIVE.value
        if not ent_cls.is_beta or resource.get("status", "") == enabled_status:
            released_resources.append(resource)

    if released_resources:
        new_response["services"] = released_resources

    return new_response
def _unattached_status(cfg: UAConfig) -> Dict[str, Any]:
    """Return unattached status as a dict."""

    response = copy.deepcopy(DEFAULT_STATUS)
    response["version"] = version.get_version(features=cfg.features)

    resources = get_available_resources(cfg)
    for resource in resources:
        if resource.get("available"):
            available = UserFacingAvailability.AVAILABLE.value
        else:
            available = UserFacingAvailability.UNAVAILABLE.value
        try:
            ent_cls = entitlement_factory(
                cfg=cfg, name=resource.get("name", "")
            )
        except exceptions.EntitlementNotFoundError:
            LOG.debug(
                messages.AVAILABILITY_FROM_UNKNOWN_SERVICE.format(
                    service=resource.get("name", "without a 'name' key")
                )
            )
            continue

        response["services"].append(
            {
                "name": resource.get("presentedAs", resource["name"]),
                "description": ent_cls.description,
                "available": available,
            }
        )
    response["services"].sort(key=lambda x: x.get("name", ""))

    return response
Example #4
0
    def _enable_required_services(
        self,
    ) -> Tuple[bool, Optional[messages.NamedMessage]]:
        """
        Prompt user when required services are found during enable.

        When enabling a service, we may find that there are required services
        that must be enabled first. In that situation, we can ask the user
        if the required service should be enabled before proceeding.
        """
        from uaclient.entitlements import entitlement_factory

        for required_service in self.required_services:
            try:
                ent_cls = entitlement_factory(
                    cfg=self.cfg, name=required_service
                )
            except exceptions.EntitlementNotFoundError:
                msg = messages.REQUIRED_SERVICE_NOT_FOUND.format(
                    service=required_service
                )
                return False, msg

            ent = ent_cls(self.cfg, allow_beta=True)

            is_service_disabled = (
                ent.application_status()[0] == ApplicationStatus.DISABLED
            )

            if is_service_disabled:
                user_msg = messages.REQUIRED_SERVICE.format(
                    service_being_enabled=self.title,
                    required_service=ent.title,
                )

                e_msg = messages.REQUIRED_SERVICE_STOPS_ENABLE.format(
                    service_being_enabled=self.title,
                    required_service=ent.title,
                )

                if not util.prompt_for_confirmation(
                    msg=user_msg, assume_yes=self.assume_yes
                ):
                    return False, e_msg

                event.info("Enabling required service: {}".format(ent.title))
                ret, fail = ent.enable(silent=True)
                if not ret:
                    error_msg = ""
                    if fail.message and fail.message.msg:
                        error_msg = "\n" + fail.message.msg

                    msg = messages.ERROR_ENABLING_REQUIRED_SERVICE.format(
                        error=error_msg, service=ent.title
                    )
                    return ret, msg

        return True, None
Example #5
0
def _get_service_for_pocket(pocket: str, cfg: UAConfig):
    service_to_check = "no-service-needed"
    if pocket == UA_INFRA_POCKET:
        service_to_check = "esm-infra"
    elif pocket == UA_APPS_POCKET:
        service_to_check = "esm-apps"

    ent_cls = entitlement_factory(cfg=cfg, name=service_to_check)
    return ent_cls(cfg) if ent_cls else None
Example #6
0
    def test_entitlement_factory(self, FakeConfig):
        m_cls_1 = mock.MagicMock()
        m_cls_1.return_value.valid_names = ["ent1", "othername"]

        m_cls_2 = mock.MagicMock()
        m_cls_2.return_value.valid_names = ["ent2"]

        ents = {m_cls_1, m_cls_2}
        cfg = FakeConfig()

        with mock.patch.object(entitlements, "ENTITLEMENT_CLASSES", ents):
            assert m_cls_1 == entitlements.entitlement_factory(
                cfg=cfg, name="othername"
            )
            assert m_cls_2 == entitlements.entitlement_factory(
                cfg=cfg, name="ent2"
            )
        with pytest.raises(exceptions.EntitlementNotFoundError):
            entitlements.entitlement_factory(cfg=cfg, name="nonexistent")
def help(cfg, name):
    """Return help information from an uaclient service as a dict

    :param name: Name of the service for which to return help data.

    :raises: UserFacingError when no help is available.
    """
    resources = get_available_resources(cfg)
    help_resource = None

    # We are using an OrderedDict here to guarantee
    # that if we need to print the result of this
    # dict, the order of insertion will always be respected
    response_dict = OrderedDict()
    response_dict["name"] = name

    for resource in resources:
        if resource["name"] == name or resource.get("presentedAs") == name:
            try:
                help_ent_cls = entitlement_factory(
                    cfg=cfg, name=resource["name"]
                )
            except exceptions.EntitlementNotFoundError:
                continue
            help_resource = resource
            help_ent = help_ent_cls(cfg)
            break

    if help_resource is None:
        raise exceptions.UserFacingError(
            "No help available for '{}'".format(name)
        )

    if cfg.is_attached:
        service_status = _attached_service_status(help_ent, {})
        status_msg = service_status["status"]

        response_dict["entitled"] = service_status["entitled"]
        response_dict["status"] = status_msg

        if status_msg == "enabled" and help_ent_cls.is_beta:
            response_dict["beta"] = True

    else:
        if help_resource["available"]:
            available = UserFacingAvailability.AVAILABLE.value
        else:
            available = UserFacingAvailability.UNAVAILABLE.value

        response_dict["available"] = available

    response_dict["help"] = help_ent.help_info
    return response_dict
Example #8
0
    def _check_any_service_is_active(self, services: Tuple[str, ...]) -> bool:
        from uaclient.entitlements import (
            EntitlementNotFoundError,
            entitlement_factory,
        )

        for service in services:
            try:
                ent_cls = entitlement_factory(cfg=self.cfg, name=service)
            except EntitlementNotFoundError:
                continue
            ent_status, _ = ent_cls(self.cfg).application_status()
            if ent_status == ApplicationStatus.ENABLED:
                return True

        return False
Example #9
0
def enable_entitlement_by_name(
    cfg: config.UAConfig,
    name: str,
    *,
    assume_yes: bool = False,
    allow_beta: bool = False
):
    """
    Constructs an entitlement based on the name provided. Passes kwargs onto
    the entitlement constructor.
    :raise EntitlementNotFoundError: If no entitlement with the given name is
        found, then raises this error.
    """
    ent_cls = entitlements.entitlement_factory(cfg=cfg, name=name)
    entitlement = ent_cls(
        cfg, assume_yes=assume_yes, allow_beta=allow_beta, called_name=name
    )
    return entitlement.enable()
Example #10
0
    def check_required_services_active(self):
        """
        Check if all required services are active

        :return:
            True if all required services are active
            False is at least one of the required services is disabled
        """
        from uaclient.entitlements import entitlement_factory

        for required_service in self.required_services:
            try:
                ent_cls = entitlement_factory(
                    cfg=self.cfg, name=required_service
                )
                ent_status, _ = ent_cls(self.cfg).application_status()
                if ent_status != ApplicationStatus.ENABLED:
                    return False
            except exceptions.EntitlementNotFoundError:
                pass

        return True
Example #11
0
    def _disable_dependent_services(
        self, silent: bool
    ) -> Tuple[bool, Optional[messages.NamedMessage]]:
        """
        Disable dependent services

        When performing a disable operation, we might have
        other services that depend on the original services.
        If that is true, we will alert the user about this
        and prompt for confirmation to disable these services
        as well.

        @param silent: Boolean set True to silence print/log of messages
        """
        from uaclient.entitlements import entitlement_factory

        for dependent_service in self.dependent_services:
            try:
                ent_cls = entitlement_factory(
                    cfg=self.cfg, name=dependent_service
                )
            except exceptions.EntitlementNotFoundError:
                msg = messages.DEPENDENT_SERVICE_NOT_FOUND.format(
                    service=dependent_service
                )
                event.info(info_msg=msg.msg, file_type=sys.stderr)
                return False, msg

            ent = ent_cls(cfg=self.cfg, assume_yes=True)

            is_service_enabled = (
                ent.application_status()[0] == ApplicationStatus.ENABLED
            )

            if is_service_enabled:
                user_msg = messages.DEPENDENT_SERVICE.format(
                    dependent_service=ent.title,
                    service_being_disabled=self.title,
                )

                e_msg = messages.DEPENDENT_SERVICE_STOPS_DISABLE.format(
                    service_being_disabled=self.title,
                    dependent_service=ent.title,
                )

                if not util.prompt_for_confirmation(
                    msg=user_msg, assume_yes=self.assume_yes
                ):
                    return False, e_msg

                if not silent:
                    event.info(
                        messages.DISABLING_DEPENDENT_SERVICE.format(
                            required_service=ent.title
                        )
                    )

                ret, fail = ent.disable(silent=True)
                if not ret:
                    error_msg = ""
                    if fail.message and fail.message.msg:
                        error_msg = "\n" + fail.message.msg

                    msg = messages.FAILED_DISABLING_DEPENDENT_SERVICE.format(
                        error=error_msg, required_service=ent.title
                    )
                    return False, msg

        return True, None
def write_apt_and_motd_templates(cfg: config.UAConfig, series: str) -> None:
    """Write messaging templates about available esm packages.

    :param cfg: UAConfig instance for this environment.
    :param series: string of Ubuntu release series: 'xenial'.
    """
    apps_no_pkg_file = ExternalMessage.APT_PRE_INVOKE_APPS_NO_PKGS.value
    apps_pkg_file = ExternalMessage.APT_PRE_INVOKE_APPS_PKGS.value
    infra_no_pkg_file = ExternalMessage.APT_PRE_INVOKE_INFRA_NO_PKGS.value
    infra_pkg_file = ExternalMessage.APT_PRE_INVOKE_INFRA_PKGS.value
    motd_apps_no_pkg_file = ExternalMessage.MOTD_APPS_NO_PKGS.value
    motd_apps_pkg_file = ExternalMessage.MOTD_APPS_PKGS.value
    motd_infra_no_pkg_file = ExternalMessage.MOTD_INFRA_NO_PKGS.value
    motd_infra_pkg_file = ExternalMessage.MOTD_INFRA_PKGS.value
    no_warranty_file = ExternalMessage.UBUNTU_NO_WARRANTY.value
    msg_dir = os.path.join(cfg.data_dir, "messages")

    apps_cls = entitlements.entitlement_factory(cfg=cfg, name="esm-apps")
    apps_inst = apps_cls(cfg)
    config_allow_beta = util.is_config_value_true(
        config=cfg.cfg, path_to_value="features.allow_beta")
    apps_valid = bool(config_allow_beta or not apps_cls.is_beta)
    infra_cls = entitlements.entitlement_factory(cfg=cfg, name="esm-infra")
    infra_inst = infra_cls(cfg)

    expiry_status, remaining_days = get_contract_expiry_status(cfg)

    enabled_status = ApplicationStatus.ENABLED
    msg_esm_apps = False
    msg_esm_infra = False
    if util.is_active_esm(series):
        no_warranty_msg = ""
        if expiry_status in (
                ContractExpiryStatus.EXPIRED,
                ContractExpiryStatus.NONE,
        ):
            no_warranty_msg = UBUNTU_NO_WARRANTY
        if infra_inst.application_status()[0] != enabled_status:
            msg_esm_infra = True
            no_warranty_msg = UBUNTU_NO_WARRANTY
        elif remaining_days <= defaults.CONTRACT_EXPIRY_PENDING_DAYS:
            msg_esm_infra = True
        _write_template_or_remove(no_warranty_msg,
                                  os.path.join(msg_dir, no_warranty_file))
    if not msg_esm_infra:
        # write_apt_and_motd_templates is only called if util.is_lts(series)
        msg_esm_apps = apps_valid

    if msg_esm_infra:
        _write_esm_service_msg_templates(
            cfg,
            infra_inst,
            expiry_status,
            remaining_days,
            infra_pkg_file,
            infra_no_pkg_file,
            motd_infra_pkg_file,
            motd_infra_no_pkg_file,
        )
    else:
        _remove_msg_templates(
            msg_dir=os.path.join(cfg.data_dir, "messages"),
            msg_template_names=[
                infra_pkg_file,
                infra_no_pkg_file,
                motd_infra_pkg_file,
                motd_infra_no_pkg_file,
            ],
        )

    if msg_esm_apps:
        _write_esm_service_msg_templates(
            cfg,
            apps_inst,
            expiry_status,
            remaining_days,
            apps_pkg_file,
            apps_no_pkg_file,
            motd_apps_pkg_file,
            motd_apps_no_pkg_file,
        )
    else:
        _remove_msg_templates(
            msg_dir=os.path.join(cfg.data_dir, "messages"),
            msg_template_names=[
                apps_pkg_file,
                apps_no_pkg_file,
                motd_apps_pkg_file,
                motd_apps_no_pkg_file,
            ],
        )
Example #13
0
class TestStatus:
    esm_desc = entitlement_factory(cfg=UAConfig(),
                                   name="esm-infra").description
    ros_desc = entitlement_factory(cfg=UAConfig(), name="ros").description

    def check_beta(self, cls, show_beta, uacfg=None, status=""):
        if not show_beta:
            if status == "enabled":
                return False

            if uacfg:
                allow_beta = uacfg.cfg.get("features",
                                           {}).get("allow_beta", False)

                if allow_beta:
                    return False

            return cls.is_beta

        return False

    @pytest.mark.parametrize(
        "show_beta,expected_services",
        (
            (
                True,
                [
                    {
                        "available": "yes",
                        "name": "esm-infra",
                        "description": esm_desc,
                    },
                    {
                        "available": "no",
                        "name": "ros",
                        "description": ros_desc,
                    },
                ],
            ),
            (
                False,
                [{
                    "available": "yes",
                    "name": "esm-infra",
                    "description": esm_desc,
                }],
            ),
        ),
    )
    @mock.patch("uaclient.status.get_available_resources")
    @mock.patch("uaclient.status.os.getuid", return_value=0)
    def test_root_unattached(
        self,
        _m_getuid,
        m_get_available_resources,
        _m_should_reboot,
        m_remove_notice,
        show_beta,
        expected_services,
        FakeConfig,
    ):
        """Test we get the correct status dict when unattached"""
        cfg = FakeConfig()
        m_get_available_resources.return_value = [
            {
                "name": "esm-infra",
                "available": True
            },
            {
                "name": "ros",
                "available": False
            },
        ]
        expected = copy.deepcopy(DEFAULT_STATUS)
        expected["version"] = mock.ANY
        expected["services"] = expected_services
        with mock.patch(
                "uaclient.status._get_config_status") as m_get_cfg_status:
            m_get_cfg_status.return_value = DEFAULT_CFG_STATUS
            assert expected == status.status(cfg=cfg, show_beta=show_beta)

            expected_calls = [
                mock.call(
                    "",
                    messages.ENABLE_REBOOT_REQUIRED_TMPL.format(
                        operation="fix operation"),
                )
            ]

            assert expected_calls == m_remove_notice.call_args_list

    @pytest.mark.parametrize("show_beta", (True, False))
    @pytest.mark.parametrize("features_override", ((None), ({
        "allow_beta": True
    })))
    @pytest.mark.parametrize(
        "avail_res,entitled_res,uf_entitled,uf_status",
        (
            (  # Empty lists means UNENTITLED and UNAVAILABLE
                [],
                [],
                ContractStatus.UNENTITLED.value,
                UserFacingStatus.UNAVAILABLE.value,
            ),
            (  # available == False means UNAVAILABLE
                [{
                    "name": "livepatch",
                    "available": False
                }],
                [],
                ContractStatus.UNENTITLED.value,
                UserFacingStatus.UNAVAILABLE.value,
            ),
            (  # available == True but unentitled means UNAVAILABLE
                [{
                    "name": "livepatch",
                    "available": True
                }],
                [],
                ContractStatus.UNENTITLED.value,
                UserFacingStatus.UNAVAILABLE.value,
            ),
            (  # available == False and entitled means INAPPLICABLE
                [{
                    "name": "livepatch",
                    "available": False
                }],
                [{
                    "type": "livepatch",
                    "entitled": True
                }],
                ContractStatus.ENTITLED.value,
                UserFacingStatus.INAPPLICABLE.value,
            ),
        ),
    )
    @mock.patch(
        M_PATH + "livepatch.LivepatchEntitlement.application_status",
        return_value=(ApplicationStatus.DISABLED, ""),
    )
    @mock.patch("uaclient.status.get_available_resources")
    @mock.patch("uaclient.config.os.getuid", return_value=0)
    def test_root_attached(
        self,
        _m_getuid,
        m_get_avail_resources,
        _m_livepatch_status,
        _m_should_reboot,
        _m_remove_notice,
        avail_res,
        entitled_res,
        uf_entitled,
        uf_status,
        features_override,
        show_beta,
        FakeConfig,
    ):
        """Test we get the correct status dict when attached with basic conf"""
        resource_names = [resource["name"] for resource in avail_res]
        default_entitled = ContractStatus.UNENTITLED.value
        default_status = UserFacingStatus.UNAVAILABLE.value
        token = {
            "availableResources": [],
            "machineTokenInfo": {
                "machineId": "test_machine_id",
                "accountInfo": {
                    "id": "acct-1",
                    "name": "test_account",
                    "createdAt": "2019-06-14T06:45:50Z",
                    "externalAccountIDs": [{
                        "IDs": ["id1"],
                        "Origin": "AWS"
                    }],
                },
                "contractInfo": {
                    "id": "cid",
                    "name": "test_contract",
                    "createdAt": "2020-05-08T19:02:26Z",
                    "effectiveFrom": "2000-05-08T19:02:26Z",
                    "effectiveTo": "2040-05-08T19:02:26Z",
                    "resourceEntitlements": entitled_res,
                    "products": ["free"],
                },
            },
        }

        available_resource_response = [{
            "name":
            cls.name,
            "available":
            bool({
                "name": cls.name,
                "available": True
            } in avail_res),
        } for cls in ENTITLEMENT_CLASSES]
        if avail_res:
            token["availableResources"] = available_resource_response
        else:
            m_get_avail_resources.return_value = available_resource_response

        cfg = FakeConfig.for_attached_machine(machine_token=token)
        if features_override:
            cfg.override_features(features_override)

        expected_services = [{
            "description":
            cls.description,
            "entitled":
            uf_entitled if cls.name in resource_names else default_entitled,
            "name":
            cls.name,
            "status":
            uf_status if cls.name in resource_names else default_status,
            "status_details":
            mock.ANY,
            "description_override":
            None,
            "available":
            mock.ANY,
            "blocked_by": [],
        } for cls in ENTITLEMENT_CLASSES
                             if not self.check_beta(cls, show_beta, cfg)]
        expected = copy.deepcopy(DEFAULT_STATUS)
        expected.update({
            "version":
            version.get_version(features=cfg.features),
            "attached":
            True,
            "machine_id":
            "test_machine_id",
            "services":
            expected_services,
            "effective":
            datetime.datetime(2000,
                              5,
                              8,
                              19,
                              2,
                              26,
                              tzinfo=datetime.timezone.utc),
            "expires":
            datetime.datetime(2040,
                              5,
                              8,
                              19,
                              2,
                              26,
                              tzinfo=datetime.timezone.utc),
            "contract": {
                "name":
                "test_contract",
                "id":
                "cid",
                "created_at":
                datetime.datetime(2020,
                                  5,
                                  8,
                                  19,
                                  2,
                                  26,
                                  tzinfo=datetime.timezone.utc),
                "products": ["free"],
                "tech_support_level":
                "n/a",
            },
            "account": {
                "name":
                "test_account",
                "id":
                "acct-1",
                "created_at":
                datetime.datetime(2019,
                                  6,
                                  14,
                                  6,
                                  45,
                                  50,
                                  tzinfo=datetime.timezone.utc),
                "external_account_ids": [{
                    "IDs": ["id1"],
                    "Origin": "AWS"
                }],
            },
        })
        with mock.patch(
                "uaclient.status._get_config_status") as m_get_cfg_status:
            m_get_cfg_status.return_value = DEFAULT_CFG_STATUS
            assert expected == status.status(cfg=cfg, show_beta=show_beta)
        if avail_res:
            assert m_get_avail_resources.call_count == 0
        else:
            assert m_get_avail_resources.call_count == 1
        # status() idempotent
        with mock.patch(
                "uaclient.status._get_config_status") as m_get_cfg_status:
            m_get_cfg_status.return_value = DEFAULT_CFG_STATUS
            assert expected == status.status(cfg=cfg, show_beta=show_beta)

    @mock.patch("uaclient.status.get_available_resources")
    @mock.patch("uaclient.config.os.getuid")
    def test_nonroot_unattached_is_same_as_unattached_root(
        self,
        m_getuid,
        m_get_available_resources,
        _m_should_reboot,
        _m_remove_notice,
        FakeConfig,
    ):
        m_get_available_resources.return_value = [{
            "name": "esm-infra",
            "available": True
        }]
        m_getuid.return_value = 1000
        cfg = FakeConfig()
        nonroot_status = status.status(cfg=cfg)

        m_getuid.return_value = 0
        root_unattached_status = status.status(cfg=cfg)

        assert root_unattached_status == nonroot_status

    @mock.patch("uaclient.status.get_available_resources")
    @mock.patch("uaclient.status.os.getuid")
    def test_root_followed_by_nonroot(
        self,
        m_getuid,
        m_get_available_resources,
        _m_should_reboot,
        _m_remove_notice,
        tmpdir,
        FakeConfig,
    ):
        """Ensure that non-root run after root returns data"""
        cfg = UAConfig({"data_dir": tmpdir.strpath})

        # Run as root
        m_getuid.return_value = 0
        before = copy.deepcopy(status.status(cfg=cfg))

        # Replicate an attach by modifying the underlying config and confirm
        # that we see different status
        other_cfg = FakeConfig.for_attached_machine()
        cfg.write_cache("accounts", {"accounts": other_cfg.accounts})
        cfg.write_cache("machine-token", other_cfg.machine_token)
        cfg.delete_cache_key("status-cache")
        assert status._attached_status(cfg=cfg) != before

        # Run as regular user and confirm that we see the result from
        # last time we called .status()
        m_getuid.return_value = 1000
        after = status.status(cfg=cfg)

        assert before == after

    @mock.patch("uaclient.status.get_available_resources", return_value=[])
    @mock.patch("uaclient.status.os.getuid", return_value=0)
    def test_cache_file_is_written_world_readable(
        self,
        _m_getuid,
        _m_get_available_resources,
        _m_should_reboot,
        m_remove_notice,
        tmpdir,
    ):
        cfg = UAConfig({"data_dir": tmpdir.strpath})
        status.status(cfg=cfg)

        assert 0o644 == stat.S_IMODE(
            os.lstat(cfg.data_path("status-cache")).st_mode)

        expected_calls = [
            mock.call(
                "",
                messages.ENABLE_REBOOT_REQUIRED_TMPL.format(
                    operation="fix operation"),
            )
        ]

        assert expected_calls == m_remove_notice.call_args_list

    @pytest.mark.parametrize("show_beta", (True, False))
    @pytest.mark.parametrize("features_override", ((None), ({
        "allow_beta": False
    })))
    @pytest.mark.parametrize(
        "entitlements",
        (
            [],
            [{
                "type": "support",
                "entitled": True,
                "affordances": {
                    "supportLevel": "anything"
                },
            }],
        ),
    )
    @mock.patch("uaclient.status.os.getuid", return_value=0)
    @mock.patch(
        M_PATH + "fips.FIPSCommonEntitlement.application_status",
        return_value=(ApplicationStatus.DISABLED, ""),
    )
    @mock.patch(
        M_PATH + "livepatch.LivepatchEntitlement.application_status",
        return_value=(ApplicationStatus.DISABLED, ""),
    )
    @mock.patch(M_PATH + "livepatch.LivepatchEntitlement.user_facing_status")
    @mock.patch(M_PATH + "livepatch.LivepatchEntitlement.contract_status")
    @mock.patch(M_PATH + "esm.ESMAppsEntitlement.user_facing_status")
    @mock.patch(M_PATH + "esm.ESMAppsEntitlement.contract_status")
    @mock.patch(M_PATH + "repo.RepoEntitlement.user_facing_status")
    @mock.patch(M_PATH + "repo.RepoEntitlement.contract_status")
    def test_attached_reports_contract_and_service_status(
        self,
        m_repo_contract_status,
        m_repo_uf_status,
        m_esm_contract_status,
        m_esm_uf_status,
        m_livepatch_contract_status,
        m_livepatch_uf_status,
        _m_livepatch_status,
        _m_fips_status,
        _m_getuid,
        _m_should_reboot,
        m_remove_notice,
        entitlements,
        features_override,
        show_beta,
        FakeConfig,
    ):
        """When attached, return contract and service user-facing status."""
        m_repo_contract_status.return_value = ContractStatus.ENTITLED
        m_repo_uf_status.return_value = (
            UserFacingStatus.INAPPLICABLE,
            messages.NamedMessage("test-code", "repo details"),
        )
        m_livepatch_contract_status.return_value = ContractStatus.ENTITLED
        m_livepatch_uf_status.return_value = (
            UserFacingStatus.ACTIVE,
            messages.NamedMessage("test-code", "livepatch details"),
        )
        m_esm_contract_status.return_value = ContractStatus.ENTITLED
        m_esm_uf_status.return_value = (
            UserFacingStatus.ACTIVE,
            messages.NamedMessage("test-code", "esm-apps details"),
        )
        token = {
            "availableResources": ALL_RESOURCES_AVAILABLE,
            "machineTokenInfo": {
                "machineId": "test_machine_id",
                "accountInfo": {
                    "id": "1",
                    "name": "accountname",
                    "createdAt": "2019-06-14T06:45:50Z",
                    "externalAccountIDs": [{
                        "IDs": ["id1"],
                        "Origin": "AWS"
                    }],
                },
                "contractInfo": {
                    "id": "contract-1",
                    "name": "contractname",
                    "createdAt": "2020-05-08T19:02:26Z",
                    "resourceEntitlements": entitlements,
                    "products": ["free"],
                },
            },
        }
        cfg = FakeConfig.for_attached_machine(account_name="accountname",
                                              machine_token=token)
        if features_override:
            cfg.override_features(features_override)
        if not entitlements:
            support_level = UserFacingStatus.INAPPLICABLE.value
        else:
            support_level = entitlements[0]["affordances"]["supportLevel"]
        expected = copy.deepcopy(status.DEFAULT_STATUS)
        expected.update({
            "version": version.get_version(features=cfg.features),
            "attached": True,
            "machine_id": "test_machine_id",
            "contract": {
                "name":
                "contractname",
                "id":
                "contract-1",
                "created_at":
                datetime.datetime(2020,
                                  5,
                                  8,
                                  19,
                                  2,
                                  26,
                                  tzinfo=datetime.timezone.utc),
                "products": ["free"],
                "tech_support_level":
                support_level,
            },
            "account": {
                "name":
                "accountname",
                "id":
                "1",
                "created_at":
                datetime.datetime(2019,
                                  6,
                                  14,
                                  6,
                                  45,
                                  50,
                                  tzinfo=datetime.timezone.utc),
                "external_account_ids": [{
                    "IDs": ["id1"],
                    "Origin": "AWS"
                }],
            },
        })
        for cls in ENTITLEMENT_CLASSES:
            if cls.name == "livepatch":
                expected_status = UserFacingStatus.ACTIVE.value
                details = "livepatch details"
            elif cls.name == "esm-apps":
                expected_status = UserFacingStatus.ACTIVE.value
                details = "esm-apps details"
            else:
                expected_status = UserFacingStatus.INAPPLICABLE.value
                details = "repo details"

            if self.check_beta(cls, show_beta, cfg, expected_status):
                continue

            expected["services"].append({
                "name": cls.name,
                "description": cls.description,
                "entitled": ContractStatus.ENTITLED.value,
                "status": expected_status,
                "status_details": details,
                "description_override": None,
                "available": mock.ANY,
                "blocked_by": [],
            })
        with mock.patch(
                "uaclient.status._get_config_status") as m_get_cfg_status:
            m_get_cfg_status.return_value = DEFAULT_CFG_STATUS
            assert expected == status.status(cfg=cfg, show_beta=show_beta)

        assert len(ENTITLEMENT_CLASSES) - 2 == m_repo_uf_status.call_count
        assert 1 == m_livepatch_uf_status.call_count

        expected_calls = [
            mock.call(
                "",
                messages.NOTICE_DAEMON_AUTO_ATTACH_LOCK_HELD.format(
                    operation=".*"),
            ),
            mock.call("", messages.NOTICE_DAEMON_AUTO_ATTACH_FAILED),
            mock.call(
                "",
                messages.ENABLE_REBOOT_REQUIRED_TMPL.format(
                    operation="fix operation"),
            ),
        ]

        assert expected_calls == m_remove_notice.call_args_list

    @mock.patch("uaclient.status.get_available_resources")
    @mock.patch("uaclient.status.os.getuid")
    def test_expires_handled_appropriately(
        self,
        m_getuid,
        _m_get_available_resources,
        _m_should_reboot,
        _m_remove_notice,
        FakeConfig,
    ):
        token = {
            "availableResources": ALL_RESOURCES_AVAILABLE,
            "machineTokenInfo": {
                "machineId": "test_machine_id",
                "accountInfo": {
                    "id": "1",
                    "name": "accountname"
                },
                "contractInfo": {
                    "name": "contractname",
                    "id": "contract-1",
                    "effectiveTo": "2020-07-18T00:00:00Z",
                    "createdAt": "2020-05-08T19:02:26Z",
                    "resourceEntitlements": [],
                    "products": ["free"],
                },
            },
        }
        cfg = FakeConfig.for_attached_machine(account_name="accountname",
                                              machine_token=token)

        # Test that root's status works as expected (including the cache write)
        m_getuid.return_value = 0
        expected_dt = datetime.datetime(2020,
                                        7,
                                        18,
                                        0,
                                        0,
                                        0,
                                        tzinfo=datetime.timezone.utc)
        assert expected_dt == status.status(cfg=cfg)["expires"]

        # Test that the read from the status cache work properly for non-root
        # users
        m_getuid.return_value = 1000
        assert expected_dt == status.status(cfg=cfg)["expires"]

    @mock.patch("uaclient.status.os.getuid")
    def test_nonroot_user_uses_cache_and_updates_if_available(
            self, m_getuid, _m_should_reboot, m_remove_notice, tmpdir):
        m_getuid.return_value = 1000

        expected_status = {"pass": True}
        cfg = UAConfig({"data_dir": tmpdir.strpath})
        cfg.write_cache("marker-reboot-cmds", "")  # To indicate a reboot reqd
        cfg.write_cache("status-cache", expected_status)

        # Even non-root users can update execution_status details
        details = messages.ENABLE_REBOOT_REQUIRED_TMPL.format(
            operation="configuration changes")
        reboot_required = UserFacingConfigStatus.REBOOTREQUIRED.value
        expected_status.update({
            "execution_status": reboot_required,
            "execution_details": details,
            "notices": [],
            "config_path": None,
            "config": {
                "data_dir": mock.ANY
            },
        })

        assert expected_status == status.status(cfg=cfg)
def simulate_status(
    cfg, token: str, show_beta: bool = False
) -> Tuple[Dict[str, Any], int]:
    """Get a status dictionary based on a token.

    Returns a tuple with the status dictionary and an integer value - 0 for
    success, 1 for failure
    """
    ret = 0
    response = copy.deepcopy(DEFAULT_STATUS)

    try:
        contract_information = get_contract_information(cfg, token)
    except exceptions.ContractAPIError as e:
        if hasattr(e, "code") and e.code == 401:
            raise exceptions.UserFacingError(
                msg=messages.ATTACH_INVALID_TOKEN.msg,
                msg_code=messages.ATTACH_INVALID_TOKEN.name,
            )
        raise e

    contract_info = contract_information.get("contractInfo", {})
    account_info = contract_information.get("accountInfo", {})

    response.update(
        {
            "version": version.get_version(features=cfg.features),
            "contract": {
                "id": contract_info.get("id", ""),
                "name": contract_info.get("name", ""),
                "created_at": contract_info.get("createdAt", ""),
                "products": contract_info.get("products", []),
            },
            "account": {
                "name": account_info.get("name", ""),
                "id": account_info.get("id"),
                "created_at": account_info.get("createdAt", ""),
                "external_account_ids": account_info.get(
                    "externalAccountIDs", []
                ),
            },
            "simulated": True,
        }
    )

    now = datetime.now(timezone.utc)
    if contract_info.get("effectiveTo"):
        response["expires"] = contract_info.get("effectiveTo")
        expiration_datetime = util.parse_rfc3339_date(response["expires"])
        delta = expiration_datetime - now
        if delta.total_seconds() <= 0:
            message = messages.ATTACH_FORBIDDEN_EXPIRED.format(
                contract_id=response["contract"]["id"],
                date=expiration_datetime.strftime(ATTACH_FAIL_DATE_FORMAT),
            )
            event.error(error_msg=message.msg, error_code=message.name)
            event.info("This token is not valid.\n" + message.msg + "\n")
            ret = 1
    if contract_info.get("effectiveFrom"):
        response["effective"] = contract_info.get("effectiveFrom")
        effective_datetime = util.parse_rfc3339_date(response["effective"])
        delta = now - effective_datetime
        if delta.total_seconds() <= 0:
            message = messages.ATTACH_FORBIDDEN_NOT_YET.format(
                contract_id=response["contract"]["id"],
                date=effective_datetime.strftime(ATTACH_FAIL_DATE_FORMAT),
            )
            event.error(error_msg=message.msg, error_code=message.name)
            event.info("This token is not valid.\n" + message.msg + "\n")
            ret = 1

    status_cache = cfg.read_cache("status-cache")
    if status_cache:
        resources = status_cache.get("services")
    else:
        resources = get_available_resources(cfg)

    entitlements = contract_info.get("resourceEntitlements", [])

    inapplicable_resources = [
        resource["name"]
        for resource in sorted(resources, key=lambda x: x["name"])
        if not resource["available"]
    ]

    for resource in resources:
        entitlement_name = resource.get("name", "")
        try:
            ent_cls = entitlement_factory(cfg=cfg, name=entitlement_name)
        except exceptions.EntitlementNotFoundError:
            continue
        ent = ent_cls(cfg=cfg)
        entitlement_information = _get_entitlement_information(
            entitlements, entitlement_name
        )
        response["services"].append(
            {
                "name": resource.get("presentedAs", ent.name),
                "description": ent.description,
                "entitled": entitlement_information["entitled"],
                "auto_enabled": entitlement_information["auto_enabled"],
                "available": "yes"
                if ent.name not in inapplicable_resources
                else "no",
            }
        )
    response["services"].sort(key=lambda x: x.get("name", ""))

    support = _get_entitlement_information(entitlements, "support")
    if support["entitled"]:
        supportLevel = support["affordances"].get("supportLevel")
        if supportLevel:
            response["contract"]["tech_support_level"] = supportLevel

    response.update(_get_config_status(cfg))
    response = _handle_beta_resources(cfg, show_beta, response)

    return response, ret
def _attached_status(cfg) -> Dict[str, Any]:
    """Return configuration of attached status as a dictionary."""

    cfg.remove_notice(
        "",
        messages.NOTICE_DAEMON_AUTO_ATTACH_LOCK_HELD.format(operation=".*"),
    )
    cfg.remove_notice("", messages.NOTICE_DAEMON_AUTO_ATTACH_FAILED)

    response = copy.deepcopy(DEFAULT_STATUS)
    machineTokenInfo = cfg.machine_token["machineTokenInfo"]
    contractInfo = machineTokenInfo["contractInfo"]
    tech_support_level = UserFacingStatus.INAPPLICABLE.value
    response.update(
        {
            "version": version.get_version(features=cfg.features),
            "machine_id": machineTokenInfo["machineId"],
            "attached": True,
            "origin": contractInfo.get("origin"),
            "notices": cfg.read_cache("notices") or [],
            "contract": {
                "id": contractInfo["id"],
                "name": contractInfo["name"],
                "created_at": contractInfo.get("createdAt", ""),
                "products": contractInfo.get("products", []),
                "tech_support_level": tech_support_level,
            },
            "account": {
                "name": cfg.accounts[0]["name"],
                "id": cfg.accounts[0]["id"],
                "created_at": cfg.accounts[0].get("createdAt", ""),
                "external_account_ids": cfg.accounts[0].get(
                    "externalAccountIDs", []
                ),
            },
        }
    )
    if contractInfo.get("effectiveTo"):
        response["expires"] = cfg.contract_expiry_datetime
    if contractInfo.get("effectiveFrom"):
        response["effective"] = contractInfo["effectiveFrom"]

    resources = cfg.machine_token.get("availableResources")
    if not resources:
        resources = get_available_resources(cfg)

    inapplicable_resources = {
        resource["name"]: resource.get("description")
        for resource in sorted(resources, key=lambda x: x.get("name", ""))
        if not resource.get("available")
    }

    for resource in resources:
        try:
            ent_cls = entitlement_factory(
                cfg=cfg, name=resource.get("name", "")
            )
        except exceptions.EntitlementNotFoundError:
            continue
        ent = ent_cls(cfg)
        response["services"].append(
            _attached_service_status(ent, inapplicable_resources)
        )
    response["services"].sort(key=lambda x: x.get("name", ""))

    support = cfg.entitlements.get("support", {}).get("entitlement")
    if support:
        supportLevel = support.get("affordances", {}).get("supportLevel")
        if supportLevel:
            response["contract"]["tech_support_level"] = supportLevel
    return response