Exemple #1
0
def attach_with_token(
    cfg: config.UAConfig, token: str, allow_enable: bool
) -> None:
    """
    Common functionality to take a token and attach via contract backend
    :raise UrlError: On unexpected connectivity issues to contract
        server or inability to access identity doc from metadata service.
    :raise ContractAPIError: On unexpected errors when talking to the contract
        server.
    """
    from uaclient.jobs.update_messaging import update_apt_and_motd_messages

    try:
        contract.request_updated_contract(
            cfg, token, allow_enable=allow_enable
        )
    except exceptions.UrlError as exc:
        # Persist updated status in the event of partial attach
        ua_status.status(cfg=cfg)
        update_apt_and_motd_messages(cfg)
        raise exc
    except exceptions.UserFacingError as exc:
        # Persist updated status in the event of partial attach
        ua_status.status(cfg=cfg)
        update_apt_and_motd_messages(cfg)
        raise exc

    current_iid = identity.get_instance_id()
    if current_iid:
        cfg.write_cache("instance-id", current_iid)

    update_apt_and_motd_messages(cfg)
    def can_enable(self, silent: bool = False) -> bool:
        """
        Report whether or not enabling is possible for the entitlement.

        :param silent: if True, suppress output
        """
        if self.is_access_expired():
            logging.debug(
                "Updating contract on service '%s' expiry", self.name
            )
            contract.request_updated_contract(self.cfg)
        if not self.contract_status() == ContractStatus.ENTITLED:
            if not silent:
                print(status.MESSAGE_UNENTITLED_TMPL.format(title=self.title))
            return False
        application_status, _ = self.application_status()
        if application_status != status.ApplicationStatus.DISABLED:
            if not silent:
                print(
                    status.MESSAGE_ALREADY_ENABLED_TMPL.format(
                        title=self.title
                    )
                )
            return False
        applicability_status, details = self.applicability_status()
        if applicability_status == status.ApplicabilityStatus.INAPPLICABLE:
            if not silent:
                print(details)
            return False
        return True
def refresh_contract(cfg):
    try:
        contract.request_updated_contract(cfg)
    except exceptions.UrlError as exc:
        logging.exception(exc)
        logging.warning(messages.REFRESH_CONTRACT_FAILURE)
        sys.exit(1)
def _attach_with_token(cfg: config.UAConfig, token: str,
                       allow_enable: bool) -> int:
    """Common functionality to take a token and attach via contract backend"""
    try:
        contract.request_updated_contract(cfg,
                                          token,
                                          allow_enable=allow_enable)
    except util.UrlError as exc:
        with util.disable_log_to_console():
            logging.exception(exc)
        print(ua_status.MESSAGE_ATTACH_FAILURE)
        cfg.status()  # Persist updated status in the event of partial attach
        return 1
    except exceptions.UserFacingError as exc:
        logging.warning(exc.msg)
        cfg.status()  # Persist updated status in the event of partial attach
        return 1
    contract_name = cfg.machine_token["machineTokenInfo"]["contractInfo"][
        "name"]
    print(
        ua_status.MESSAGE_ATTACH_SUCCESS_TMPL.format(
            contract_name=contract_name))

    action_status(args=None, cfg=cfg)
    return 0
Exemple #5
0
    def test_user_facing_error_on_service_token_refresh_failure(
        self, client, get_machine_id, FakeConfig
    ):
        """When attaching, error on any failed specific service refresh."""

        machine_token = {
            "machineToken": "mToken",
            "machineTokenInfo": {
                "contractInfo": {
                    "id": "cid",
                    "resourceEntitlements": [
                        {"entitled": True, "type": "ent2"},
                        {"entitled": True, "type": "ent1"},
                    ],
                }
            },
        }

        def fake_contract_client(cfg):
            fake_client = FakeContractClient(cfg)
            fake_client._responses = {self.refresh_route: machine_token}
            return fake_client

        client.side_effect = fake_contract_client
        cfg = FakeConfig.for_attached_machine(machine_token=machine_token)
        with mock.patch(M_PATH + "process_entitlement_delta") as m_process:
            m_process.side_effect = (
                exceptions.UserFacingError("broken ent1"),
                exceptions.UserFacingError("broken ent2"),
            )
            with pytest.raises(exceptions.UserFacingError) as exc:
                request_updated_contract(cfg)

        assert MESSAGE_ATTACH_FAILURE_DEFAULT_SERVICES == str(exc.value)
Exemple #6
0
    def test_invalid_token_user_facing_error_on_invalid_token_refresh_failure(
        self, client, get_machine_id
    ):
        """When attaching, invalid token errors result in proper user error."""

        def fake_contract_client(cfg):
            fake_client = FakeContractClient(cfg)
            fake_client._responses = {
                API_V1_CONTEXT_MACHINE_TOKEN: ContractAPIError(
                    util.UrlError(
                        "Server error", code=500, url="http://me", headers={}
                    ),
                    error_response={
                        "message": "invalid token: checksum error"
                    },
                )
            }
            return fake_client

        client.side_effect = fake_contract_client
        cfg = FakeConfig()
        with pytest.raises(exceptions.UserFacingError) as exc:
            request_updated_contract(cfg, contract_token="yep")

        assert MESSAGE_ATTACH_INVALID_TOKEN == str(exc.value)
    def test_invalid_token_user_facing_error_on_invalid_token_refresh_failure(
        self,
        client,
        get_machine_id,
        FakeConfig,
        error_code,
        error_msg,
        error_response,
    ):
        """When attaching, invalid token errors result in proper user error."""
        def fake_contract_client(cfg):
            fake_client = FakeContractClient(cfg)
            fake_client._responses = {
                API_V1_CONTEXT_MACHINE_TOKEN:
                exceptions.ContractAPIError(
                    exceptions.UrlError(
                        "Server error",
                        code=error_code,
                        url="http://me",
                        headers={},
                    ),
                    error_response=json.loads(
                        error_response, cls=util.DatetimeAwareJSONDecoder),
                )
            }
            return fake_client

        client.side_effect = fake_contract_client
        cfg = FakeConfig()
        with pytest.raises(exceptions.UserFacingError) as exc:
            request_updated_contract(cfg, contract_token="yep")

        assert error_msg.msg == str(exc.value.msg)
def action_attach(args, cfg):
    if cfg.is_attached:
        print("This machine is already attached to '{}'.".format(
            cfg.accounts[0]["name"]))
        return 0
    if os.getuid() != 0:
        raise exceptions.NonRootUserError()
    contract_token = args.token
    if not contract_token:
        print("No valid contract token available")
        return 1
    try:
        contract.request_updated_contract(cfg,
                                          contract_token,
                                          allow_enable=args.auto_enable)
    except util.UrlError as exc:
        with util.disable_log_to_console():
            logging.exception(exc.msg)
        print(
            ua_status.MESSAGE_ATTACH_FAILURE_TMPL.format(url=cfg.contract_url))
        return 1
    except exceptions.UserFacingError as exc:
        logging.warning(exc.msg)
        action_status(args=None, cfg=cfg)
        return 1
    contract_name = cfg.machine_token["machineTokenInfo"]["contractInfo"][
        "name"]
    print(
        ua_status.MESSAGE_ATTACH_SUCCESS_TMPL.format(
            contract_name=contract_name))

    action_status(args=None, cfg=cfg)
    return 0
Exemple #9
0
def action_refresh(args, cfg):
    try:
        contract.request_updated_contract(cfg)
    except util.UrlError as exc:
        with util.disable_log_to_console():
            logging.exception(exc)
        raise exceptions.UserFacingError(ua_status.MESSAGE_REFRESH_FAILURE)
    print(ua_status.MESSAGE_REFRESH_SUCCESS)
    return 0
Exemple #10
0
def action_enable(args, cfg):
    """Perform the enable action on a named entitlement.

    @return: 0 on success, 1 otherwise
    """
    print(ua_status.MESSAGE_REFRESH_ENABLE)
    try:
        contract.request_updated_contract(cfg)
    except (util.UrlError, exceptions.UserFacingError):
        # Inability to refresh is not a critical issue during enable
        logging.debug(ua_status.MESSAGE_REFRESH_FAILURE, exc_info=True)
    return 0 if _perform_enable(args.name, cfg) else 1
    def test_attached_config_and_contract_token_runtime_error(self, client):
        """When attached, error if called with a contract_token."""
        def fake_contract_client(cfg):
            return FakeContractClient(cfg)

        client.side_effect = fake_contract_client
        cfg = FakeConfig.for_attached_machine()
        with pytest.raises(RuntimeError) as exc:
            request_updated_contract(cfg, contract_token="something")

        expected_msg = (
            "Got unexpected contract_token on an already attached machine")
        assert expected_msg == str(exc.value)
Exemple #12
0
def action_enable(args, cfg, **kwargs):
    """Perform the enable action on a named entitlement.

    @return: 0 on success, 1 otherwise
    """
    print(ua_status.MESSAGE_REFRESH_ENABLE)
    try:
        contract.request_updated_contract(cfg)
    except (util.UrlError, exceptions.UserFacingError):
        # Inability to refresh is not a critical issue during enable
        logging.debug(ua_status.MESSAGE_REFRESH_FAILURE, exc_info=True)

    names = getattr(args, "service", [])
    entitlements_found, entitlements_not_found = get_valid_entitlement_names(
        names
    )
    ret = True

    for entitlement in entitlements_found:
        try:
            ret &= _perform_enable(
                entitlement,
                cfg,
                assume_yes=args.assume_yes,
                allow_beta=args.beta,
            )
        except exceptions.BetaServiceError:
            entitlements_not_found.append(entitlement)
        except exceptions.UserFacingError as e:
            print(e)

    if entitlements_not_found:
        if args.beta:
            valid_names = entitlements.ALL_ENTITLEMENTS_STR
        else:
            valid_names = entitlements.RELEASED_ENTITLEMENTS_STR
        service_msg = "\n".join(
            textwrap.wrap(
                "Try " + valid_names, width=80, break_long_words=False
            )
        )
        tmpl = ua_status.MESSAGE_INVALID_SERVICE_OP_FAILURE_TMPL
        raise exceptions.UserFacingError(
            tmpl.format(
                operation="enable",
                name=", ".join(entitlements_not_found),
                service_msg=service_msg,
            )
        )

    return 0 if ret else 1
    def test_attached_config_refresh_errors_on_expired_contract(
            self, client, get_machine_id, process_entitlement_delta):
        """Error when refreshing contract parses an expired contract token."""

        machine_token = {
            "machineToken": "mToken",
            "machineTokenInfo": {
                "contractInfo": {
                    "effectiveTo":
                    "2018-07-18T00:00:00Z",  # Expired date
                    "id":
                    "cid",
                    "resourceEntitlements": [
                        {
                            "entitled": False,
                            "type": "ent2"
                        },
                        {
                            "entitled": True,
                            "type": "ent1"
                        },
                    ],
                }
            },
        }

        def fake_contract_client(cfg):
            client = FakeContractClient(cfg)
            # Note ent2 access route is not called
            client._responses = {
                self.refresh_route: machine_token,
                self.access_route_ent1: {
                    "entitlement": {
                        "entitled": True,
                        "type": "ent1",
                        "new": "newval",
                    }
                },
            }
            return client

        client.side_effect = fake_contract_client
        cfg = FakeConfig.for_attached_machine(machine_token=machine_token)
        with pytest.raises(exceptions.UserFacingError) as exc:
            request_updated_contract(cfg)
        assert MESSAGE_CONTRACT_EXPIRED_ERROR == str(exc.value)
        assert machine_token == cfg.read_cache("machine-token")

        # No deltas are processed when contract is expired
        assert 0 == process_entitlement_delta.call_count
Exemple #14
0
    def test_attached_config_refresh_machine_token_and_services(
        self, client, get_machine_id, process_entitlement_delta, FakeConfig
    ):
        """When attached, refresh machine token and entitled services.

        Processing service deltas are processed in a sorted order based on
        name to ensure operations occur the same regardless of dict ordering.
        """

        # resourceEntitlements specifically ordered reverse alphabetically
        # to ensure proper sorting for process_contract_delta calls below
        machine_token = {
            "machineToken": "mToken",
            "machineTokenInfo": {
                "contractInfo": {
                    "id": "cid",
                    "resourceEntitlements": [
                        {"entitled": False, "type": "ent2"},
                        {"entitled": True, "type": "ent1"},
                    ],
                }
            },
        }
        new_token = copy.deepcopy(machine_token)
        new_token["machineTokenInfo"]["contractInfo"]["resourceEntitlements"][
            1
        ]["new"] = "newval"

        def fake_contract_client(cfg):
            client = FakeContractClient(cfg)
            client._responses = {self.refresh_route: new_token}
            return client

        client.side_effect = fake_contract_client
        cfg = FakeConfig.for_attached_machine(machine_token=machine_token)
        assert None is request_updated_contract(cfg)
        assert new_token == cfg.read_cache("machine-token")

        # Deltas are processed in a sorted fashion so that if enableByDefault
        # is true, the order of enablement operations is the same regardless
        # of dict key ordering.
        process_calls = [
            mock.call(
                {"entitlement": {"entitled": True, "type": "ent1"}},
                {
                    "entitlement": {
                        "entitled": True,
                        "type": "ent1",
                        "new": "newval",
                    }
                },
                allow_enable=False,
            ),
            mock.call(
                {"entitlement": {"entitled": False, "type": "ent2"}},
                {"entitlement": {"entitled": False, "type": "ent2"}},
                allow_enable=False,
            ),
        ]
        assert process_calls == process_entitlement_delta.call_args_list
def action_refresh(args, cfg):
    if contract.request_updated_contract(cfg):
        print(ua_status.MESSAGE_REFRESH_SUCCESS)
        logging.debug(ua_status.MESSAGE_REFRESH_SUCCESS)
        return 0
    logging.error(ua_status.MESSAGE_REFRESH_FAILURE)
    return 1
    def test_user_facing_error_on_machine_token_refresh_failure(
            self, client, get_machine_id, FakeConfig):
        """When attaching, error on failure to refresh the machine token."""
        def fake_contract_client(cfg):
            fake_client = FakeContractClient(cfg)
            fake_client._responses = {
                self.refresh_route:
                exceptions.UserFacingError("Machine token refresh fail")
            }
            return fake_client

        client.side_effect = fake_contract_client
        cfg = FakeConfig.for_attached_machine()
        with pytest.raises(exceptions.UserFacingError) as exc:
            request_updated_contract(cfg)

        assert "Machine token refresh fail" == str(exc.value)
Exemple #17
0
    def test_user_facing_error_due_to_unexpected_process_entitlement_delta(
        self,
        client,
        get_machine_id,
        process_entitlement_delta,
        first_error,
        second_error,
        ux_error_msg,
        FakeConfig,
    ):
        """Unexpected errors from process_entitlement_delta are raised.

        Remaining entitlements are processed regardless of error and error is
        raised at the end.

        Unexpected exceptions take priority over the handled UserFacingErrors.
        """
        # Fail first and succeed second call to process_entitlement_delta
        process_entitlement_delta.side_effect = (
            first_error,
            second_error,
            None,
        )

        # resourceEntitlements specifically ordered reverse alphabetically
        # to ensure proper sorting for process_contract_delta calls below
        machine_token = {
            "machineToken": "mToken",
            "machineTokenInfo": {
                "contractInfo": {
                    "id": "cid",
                    "resourceEntitlements": [
                        {"entitled": False, "type": "ent3"},
                        {"entitled": False, "type": "ent2"},
                        {"entitled": True, "type": "ent1"},
                    ],
                }
            },
        }

        cfg = FakeConfig.for_attached_machine(machine_token=machine_token)
        fake_client = FakeContractClient(cfg)
        fake_client._responses = {
            self.refresh_route: machine_token,
            self.access_route_ent1: {
                "entitlement": {
                    "entitled": True,
                    "type": "ent1",
                    "new": "newval",
                }
            },
        }

        client.return_value = fake_client
        with pytest.raises(exceptions.UserFacingError) as exc:
            assert None is request_updated_contract(cfg)
        assert 3 == process_entitlement_delta.call_count
        assert ux_error_msg == str(exc.value)
def action_enable(args, cfg):
    """Perform the enable action on a named entitlement.

    @return: 0 on success, 1 otherwise
    """
    logging.debug(ua_status.MESSAGE_REFRESH_ENABLE)
    if not contract.request_updated_contract(cfg):
        logging.debug(ua_status.MESSAGE_REFRESH_FAILURE)
    return 0 if _perform_enable(args.name, cfg) else 1
    def test_user_facing_error_on_service_token_refresh_failure(
            self, client, get_machine_id):
        """When attaching, error on any failed specific service refresh."""

        machine_token = {
            "machineToken": "mToken",
            "machineTokenInfo": {
                "contractInfo": {
                    "id":
                    "cid",
                    "resourceEntitlements": [
                        {
                            "entitled": True,
                            "type": "ent2"
                        },
                        {
                            "entitled": True,
                            "type": "ent1"
                        },
                    ],
                }
            },
        }

        def fake_contract_client(cfg):
            fake_client = FakeContractClient(cfg)
            fake_client._responses = {
                self.refresh_route:
                machine_token,
                self.access_route_ent1:
                exceptions.UserFacingError("Broken ent1 route"),
                self.access_route_ent2:
                exceptions.UserFacingError("Broken ent2 route"),
            }
            return fake_client

        client.side_effect = fake_contract_client
        cfg = FakeConfig.for_attached_machine(machine_token=machine_token)
        with pytest.raises(exceptions.UserFacingError) as exc:
            request_updated_contract(cfg)

        assert "Broken ent1 route" == str(exc.value)
Exemple #20
0
    def test_attached_config_refresh_machine_token_and_services(
            self, client, get_machine_id, process_entitlement_delta):
        """When attached, refresh machine token and entitled services.

        Processing service deltas are processed in a sorted order based on
        name to ensure operations occur the same regardless of dict ordering.
        """

        refresh_route = API_V1_TMPL_CONTEXT_MACHINE_TOKEN_REFRESH.format(
            contract='cid', machine='mid')
        access_route_ent1 = API_V1_TMPL_RESOURCE_MACHINE_ACCESS.format(
            resource='ent1', machine='mid')

        # resourceEntitlements specifically ordered reverse alphabetically
        # to ensure proper sorting for process_contract_delta calls below
        machine_token = {
            'machineToken': 'mToken',
            'machineTokenInfo': {'contractInfo': {
                'id': 'cid', 'resourceEntitlements': [
                    {'entitled': False, 'type': 'ent2'},
                    {'entitled': True, 'type': 'ent1'}]}}}

        def fake_contract_client(cfg):
            client = FakeContractClient(cfg)
            # Note ent2 access route is not called
            client._responses = {
                refresh_route: machine_token,
                access_route_ent1: {
                    'entitlement': {
                        'entitled': True, 'type': 'ent1', 'new': 'newval'}}}
            return client

        client.side_effect = fake_contract_client
        cfg = FakeConfig.for_attached_machine(machine_token=machine_token)
        assert True is request_updated_contract(cfg)
        assert machine_token == cfg.read_cache('machine-token')
        # Redact public content
        assert (
            '<REDACTED>' == cfg.read_cache(
                'public-machine-token')['machineToken'])

        # Deltas are processed in a sorted fashion so that if enableByDefault
        # is true, the order of enablement operations is the same regardless
        # of dict key ordering.
        process_calls = [
            mock.call({'entitlement': {'entitled': True, 'type': 'ent1'}},
                      {'entitlement': {'entitled': True, 'type': 'ent1',
                                       'new': 'newval'}},
                      allow_enable=False),
            mock.call({'entitlement': {'entitled': False, 'type': 'ent2'}},
                      {'entitlement': {'entitled': False, 'type': 'ent2'}},
                      allow_enable=False)]
        assert process_calls == process_entitlement_delta.call_args_list
def action_attach(args, cfg):
    if cfg.is_attached:
        print("This machine is already attached to '%s'." %
              cfg.accounts[0]['name'])
        return 0
    if os.getuid() != 0:
        print(ua_status.MESSAGE_NONROOT_USER)
        return 1
    contract_client = contract.UAContractClient(cfg)
    if not args.token:
        bound_macaroon_bytes = sso.discharge_root_macaroon(contract_client)
        if bound_macaroon_bytes is None:
            print('Could not attach machine. Unable to obtain authenticated'
                  ' user token')
            return 1
        bound_macaroon = bound_macaroon_bytes.decode('utf-8')
        cfg.write_cache('bound-macaroon', bound_macaroon)
        try:
            contract_client.request_accounts(macaroon_token=bound_macaroon)
            contract_token = contract.get_contract_token_for_account(
                contract_client, bound_macaroon, cfg.accounts[0]['id'])
        except (sso.SSOAuthError, util.UrlError) as e:
            logging.error(str(e))
            print('Could not attach machine. Unable to obtain authenticated'
                  ' contract token')
            return 1
    else:
        contract_token = args.token
    if not contract_token:
        print('No valid contract token available')
        return 1
    if not contract.request_updated_contract(
            cfg, contract_token, allow_enable=True):
        print(
            ua_status.MESSAGE_ATTACH_FAILURE_TMPL.format(url=cfg.contract_url))
        return 1
    contract_name = (
        cfg.machine_token['machineTokenInfo']['contractInfo']['name'])
    print(
        ua_status.MESSAGE_ATTACH_SUCCESS_TMPL.format(
            contract_name=contract_name))

    action_status(args=None, cfg=cfg)
    return 0
Exemple #22
0
def action_refresh(args, cfg):
    if contract.request_updated_contract(cfg):
        print(ua_status.MESSAGE_REFRESH_SUCCESS)
        logging.debug(ua_status.MESSAGE_REFRESH_SUCCESS)
        return 0
    raise exceptions.UserFacingError(ua_status.MESSAGE_REFRESH_FAILURE)
Exemple #23
0
    def can_enable(self) -> Tuple[bool, Optional[CanEnableFailure]]:
        """
        Report whether or not enabling is possible for the entitlement.

        :return:
            (True, None) if can enable
            (False, CanEnableFailure) if can't enable
        """

        if self.is_access_expired():
            logging.debug(
                "Updating contract on service '%s' expiry", self.name
            )
            contract.request_updated_contract(self.cfg)

        if not self.contract_status() == ContractStatus.ENTITLED:
            return (
                False,
                CanEnableFailure(
                    CanEnableFailureReason.NOT_ENTITLED,
                    message=messages.UNENTITLED.format(title=self.title),
                ),
            )

        application_status, _ = self.application_status()
        if application_status != ApplicationStatus.DISABLED:
            return (
                False,
                CanEnableFailure(
                    CanEnableFailureReason.ALREADY_ENABLED,
                    message=messages.ALREADY_ENABLED.format(title=self.title),
                ),
            )

        if not self.valid_service:
            return (False, CanEnableFailure(CanEnableFailureReason.IS_BETA))

        applicability_status, details = self.applicability_status()
        if applicability_status == ApplicabilityStatus.INAPPLICABLE:
            return (
                False,
                CanEnableFailure(
                    CanEnableFailureReason.INAPPLICABLE, message=details
                ),
            )

        if self.incompatible_services:
            if self.detect_incompatible_services():
                return (
                    False,
                    CanEnableFailure(
                        CanEnableFailureReason.INCOMPATIBLE_SERVICE
                    ),
                )

        if self.required_services:
            if not self.check_required_services_active():
                return (
                    False,
                    CanEnableFailure(
                        CanEnableFailureReason.INACTIVE_REQUIRED_SERVICES
                    ),
                )

        return (True, None)