コード例 #1
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)
コード例 #2
0
def discharge_root_macaroon(contract_client: UAContractClient) -> bytes:
    """Prompt for SSO authentication to create an discharge macaroon from SSO

    Extract contract client's root_macaroon caveat for login.ubuntu.com and
    prompt authentication to SSO to provide a discharge macaroon. Bind that
    discharge macaroon to the root macaroon to provide an authentication
    token for accessing authenticated UA Contract routes.

    @param contract_client: UAContractClient instance for talking to contract
        service routes.

    @return: The serialized bound root macaroon
    """
    cfg = contract_client.cfg
    try:
        root_macaroon = contract_client.request_root_macaroon()
        caveat_id = extract_macaroon_caveat_id(root_macaroon['macaroon'])
        discharge_macaroon = prompt_request_macaroon(cfg, caveat_id)
    except (util.UrlError) as e:
        raise exceptions.UserFacingError(
            'Could not reach URL {} to authenticate'.format(e.url))
    except (MacaroonFormatError) as e:
        raise exceptions.UserFacingError('Invalid root macaroon: {}'.format(e))

    return bind_discharge_macarooon_to_root_macaroon(
        discharge_macaroon['discharge_macaroon'], root_macaroon['macaroon'])
コード例 #3
0
def cloud_instance_factory() -> clouds.AutoAttachCloudInstance:
    from uaclient.clouds import aws
    from uaclient.clouds import azure

    cloud_instance_map = {
        "aws": aws.UAAutoAttachAWSInstance,
        "aws-china": aws.UAAutoAttachAWSInstance,
        "aws-gov": aws.UAAutoAttachAWSInstance,
        "azure": azure.UAAutoAttachAzureInstance,
    }

    cloud_type = get_cloud_type()
    if not cloud_type:
        raise exceptions.UserFacingError(
            status.MESSAGE_UNABLE_TO_DETERMINE_CLOUD_TYPE)
    cls = cloud_instance_map.get(cloud_type)
    if not cls:
        raise exceptions.NonAutoAttachImageError(
            status.MESSAGE_UNSUPPORTED_AUTO_ATTACH_CLOUD_TYPE.format(
                cloud_type=cloud_type))
    instance = cls()
    if not instance.is_viable:
        raise exceptions.UserFacingError(
            status.MESSAGE_UNSUPPORTED_AUTO_ATTACH)
    return instance
コード例 #4
0
 def fake_contract_client(cfg):
     fake_client = FakeContractClient(cfg)
     fake_client._responses = {
         self.refresh_route:
         exceptions.UserFacingError("Machine token refresh fail"),
         self.access_route_ent1:
         exceptions.UserFacingError("Broken ent1 route"),
     }
     return fake_client
コード例 #5
0
def request_updated_contract(cfg,
                             contract_token: "Optional[str]" = None,
                             allow_enable=False):
    """Request contract refresh from ua-contracts service.

    Compare original token to new token and react to entitlement deltas.

    :param cfg: Instance of UAConfig for this machine.
    :param contract_token: String contraining an optional contract token.
    :param allow_enable: Boolean set True if allowed to perform the enable
        operation. When False, a message will be logged to inform the user
        about the recommended enabled service.

    :raise UserFacingError: on failure to update contract or error processing
        contract deltas
    :raise UrlError: On failure to contact the server
    """
    orig_token = cfg.machine_token
    orig_entitlements = cfg.entitlements
    if orig_token and contract_token:
        raise RuntimeError(
            "Got unexpected contract_token on an already attached machine")
    contract_client = UAContractClient(cfg)
    if contract_token:  # We are a mid ua-attach and need to get machinetoken
        try:
            new_token = contract_client.request_contract_machine_attach(
                contract_token=contract_token)
        except util.UrlError as e:
            if isinstance(e, ContractAPIError):
                if hasattr(e, "code"):
                    if e.code == 401:
                        raise exceptions.UserFacingError(
                            status.MESSAGE_ATTACH_INVALID_TOKEN)
                    elif e.code == 403:
                        raise exceptions.UserFacingError(
                            status.MESSAGE_ATTACH_EXPIRED_TOKEN)
                raise e
            with util.disable_log_to_console():
                logging.exception(str(e))
            raise exceptions.UserFacingError(status.MESSAGE_CONNECTIVITY_ERROR)
    else:
        machine_token = orig_token["machineToken"]
        contract_id = orig_token["machineTokenInfo"]["contractInfo"]["id"]
        new_token = contract_client.request_machine_token_update(
            machine_token=machine_token, contract_id=contract_id)
    expiry = new_token["machineTokenInfo"]["contractInfo"].get("effectiveTo")
    if expiry:
        if datetime.strptime(expiry, "%Y-%m-%dT%H:%M:%SZ") < datetime.utcnow():
            raise exceptions.UserFacingError(
                status.MESSAGE_CONTRACT_EXPIRED_ERROR)

    process_entitlements_delta(orig_entitlements, cfg.entitlements,
                               allow_enable)
コード例 #6
0
    def enable(self, *, silent_if_inapplicable: bool = False) -> bool:
        """Enable specific entitlement.

        :param silent_if_inapplicable:
            Don't emit any messages until after it has been determined that
            this entitlement is applicable to the current machine.

        @return: True on success, False otherwise.
        """
        if not self.can_enable(silent=silent_if_inapplicable):
            return False
        if not util.which("/snap/bin/canonical-livepatch"):
            if not util.which(SNAP_CMD):
                print("Installing snapd")
                print(status.MESSAGE_APT_UPDATING_LISTS)
                try:
                    apt.run_apt_command(
                        ["apt-get", "update"], status.MESSAGE_APT_UPDATE_FAILED
                    )
                except exceptions.UserFacingError as e:
                    logging.debug(
                        "Trying to install snapd."
                        " Ignoring apt-get update failure: %s",
                        str(e),
                    )
                util.subp(
                    ["apt-get", "install", "--assume-yes", "snapd"],
                    capture=True,
                    retry_sleeps=apt.APT_RETRIES,
                )
            elif "snapd" not in apt.get_installed_packages():
                raise exceptions.UserFacingError(
                    "/usr/bin/snap is present but snapd is not installed;"
                    " cannot enable {}".format(self.title)
                )
            util.subp(
                [SNAP_CMD, "wait", "system", "seed.loaded"], capture=True
            )
            print("Installing canonical-livepatch snap")
            try:
                util.subp(
                    [SNAP_CMD, "install", "canonical-livepatch"],
                    capture=True,
                    retry_sleeps=SNAP_INSTALL_RETRIES,
                )
            except util.ProcessExecutionError as e:
                msg = "Unable to install Livepatch client: " + str(e)
                raise exceptions.UserFacingError(msg)
        return self.setup_livepatch_config(
            process_directives=True, process_token=True
        )
コード例 #7
0
def assert_valid_apt_credentials(repo_url, username, password):
    """Validate apt credentials for a PPA.

    @param repo_url: private-ppa url path
    @param username: PPA login username.
    @param password: PPA login password or resource token.

    @raises: UserFacingError for invalid credentials, timeout or unexpected
        errors.
    """
    protocol, repo_path = repo_url.split("://")
    if not os.path.exists("/usr/lib/apt/apt-helper"):
        return
    try:
        util.subp(
            [
                "/usr/lib/apt/apt-helper",
                "download-file",
                "{}://{}:{}@{}/ubuntu/pool/".format(
                    protocol, username, password, repo_path
                ),
                "/tmp/uaclient-apt-test",
            ],
            timeout=APT_HELPER_TIMEOUT,
        )
    except util.ProcessExecutionError as e:
        if e.exit_code == 100:
            stderr = str(e.stderr).lower()
            if re.search(r"401\s+unauthorized|httperror401", stderr):
                raise exceptions.UserFacingError(
                    "Invalid APT credentials provided for {}".format(repo_url)
                )
            elif re.search(r"connection timed out", stderr):
                raise exceptions.UserFacingError(
                    "Timeout trying to access APT repository at {}".format(
                        repo_url
                    )
                )
        raise exceptions.UserFacingError(
            "Unexpected APT error. See /var/log/ubuntu-advantage.log"
        )
    except subprocess.TimeoutExpired:
        raise exceptions.UserFacingError(
            "Cannot validate credentials for APT repo."
            " Timeout after {} seconds trying to reach {}.".format(
                APT_HELPER_TIMEOUT, repo_path
            )
        )
    finally:
        if os.path.exists("/tmp/uaclient-apt-test"):
            os.unlink("/tmp/uaclient-apt-test")
コード例 #8
0
    def parse_machine_token_overlay(self, machine_token_overlay_path):
        if not os.path.exists(machine_token_overlay_path):
            raise exceptions.UserFacingError(
                status.INVALID_PATH_FOR_MACHINE_TOKEN_OVERLAY.format(
                    file_path=machine_token_overlay_path))

        try:
            machine_token_overlay_content = util.load_file(
                machine_token_overlay_path)

            return json.loads(machine_token_overlay_content)
        except ValueError as e:
            raise exceptions.UserFacingError(
                status.ERROR_JSON_DECODING_IN_FILE.format(
                    error=str(e), file_path=machine_token_overlay_path))
コード例 #9
0
def is_config_value_true(config: "Dict[str, Any]", path_to_value: str):
    """Check if value parameter can be translated into a boolean 'True' value.

    @param config: A config dict representing
                   /etc/ubuntu-advantange/uaclient.conf
    @param path_to_value: The path from where the value parameter was
                          extracted.
    @return: A boolean value indicating if the value paramater corresponds
             to a 'True' boolean value.
    @raises exceptions.UserFacingError when the value provide by the
            path_to_value parameter can not be translated into either
            a 'False' or 'True' boolean value.
    """
    value = config
    default_value = {}  # type: Any
    paths = path_to_value.split(".")
    leaf_value = paths[-1]
    for key in paths:
        if key == leaf_value:
            default_value = "false"

        value = value.get(key, default_value)

    value_str = str(value)
    if value_str.lower() == "true":
        return True
    elif value_str.lower() == "false":
        return False
    else:
        raise exceptions.UserFacingError(
            status.ERROR_INVALID_CONFIG_VALUE.format(
                path_to_value=path_to_value,
                expected_value="boolean string: true or false",
                value=value_str,
            ))
コード例 #10
0
def run_apt_command(
    cmd: "List[str]", error_msg: str, env: "Optional[Dict[str, str]]" = {}
) -> str:
    """Run an apt command, retrying upon failure APT_RETRIES times.

    :param cmd: List containing the apt command to run, passed to subp.
    :param error_msg: The string to raise as UserFacingError when all retries
       are exhausted in failure.
    :param env: Optional dictionary of environment variables to pass to subp.

    :return: stdout from successful run of the apt command.
    :raise UserFacingError: on issues running apt-cache policy.
    """
    try:
        out, _err = util.subp(
            cmd, capture=True, retry_sleeps=APT_RETRIES, env=env
        )
    except util.ProcessExecutionError as e:
        if "Could not get lock /var/lib/dpkg/lock" in str(e.stderr):
            error_msg += " Another process is running APT."
        else:
            """
            Treat errors where one of the APT repositories
            is invalid or unreachable. In that situation, we alert
            which repository is causing the error
            """
            error_msg += _parse_apt_update_for_invalid_apt_config(e.stderr)

        raise exceptions.UserFacingError(error_msg)
    return out
コード例 #11
0
    def enable(self, *, silent_if_inapplicable: bool = False) -> bool:
        """Enable specific entitlement.

        :param silent_if_inapplicable:
            Don't emit any messages until after it has been determined that
            this entitlement is applicable to the current machine.

        @return: True on success, False otherwise.
        """
        if not self.can_enable(silent=silent_if_inapplicable):
            return False
        if not util.which('/snap/bin/canonical-livepatch'):
            if not util.which(SNAP_CMD):
                print('Installing snapd')
                util.subp(['apt-get', 'install', '--assume-yes', 'snapd'],
                          capture=True,
                          retry_sleeps=apt.APT_RETRIES)
                util.subp([SNAP_CMD, 'wait', 'system', 'seed.loaded'],
                          capture=True)
            elif 'snapd' not in apt.get_installed_packages():
                raise exceptions.UserFacingError(
                    '/usr/bin/snap is present but snapd is not installed;'
                    ' cannot enable {}'.format(self.title))
            print('Installing canonical-livepatch snap')
            try:
                util.subp([SNAP_CMD, 'install', 'canonical-livepatch'],
                          capture=True,
                          retry_sleeps=SNAP_INSTALL_RETRIES)
            except util.ProcessExecutionError as e:
                msg = 'Unable to install Livepatch client: ' + str(e)
                print(msg)
                logging.error(msg)
                return False
        return self.setup_livepatch_config(process_directives=True,
                                           process_token=True)
コード例 #12
0
def action_disable(args, cfg, **kwargs):
    """Perform the disable action on a list of entitlements.

    @return: 0 on success, 1 otherwise
    """
    names = getattr(args, "service", [])
    entitlements_found, entitlements_not_found = get_valid_entitlement_names(
        names
    )
    tmpl = ua_status.MESSAGE_INVALID_SERVICE_OP_FAILURE_TMPL
    ret = True

    for entitlement in entitlements_found:
        ret &= _perform_disable(entitlement, cfg, assume_yes=args.assume_yes)

    if entitlements_not_found:
        valid_names = "Try " + entitlements.ALL_ENTITLEMENTS_STR
        service_msg = "\n".join(
            textwrap.wrap(valid_names, width=80, break_long_words=False)
        )
        raise exceptions.UserFacingError(
            tmpl.format(
                operation="disable",
                name=", ".join(entitlements_not_found),
                service_msg=service_msg,
            )
        )

    return 0 if ret else 1
コード例 #13
0
def _perform_enable(entitlement_name: str,
                    cfg: config.UAConfig,
                    *,
                    assume_yes: bool = False,
                    silent_if_inapplicable: bool = False,
                    allow_beta: bool = False) -> bool:
    """Perform the enable action on a named entitlement.

    (This helper excludes any messaging, so that different enablement code
    paths can message themselves.)

    :param entitlement_name: the name of the entitlement to enable
    :param cfg: the UAConfig to pass to the entitlement
    :param assume_yes:
        Assume a yes response for any prompts during service enable
    :param silent_if_inapplicable:
        don't output messages when determining if an entitlement can be
        enabled on this system
    :param allow_beta: Allow enabling beta services

    @return: True on success, False otherwise
    """
    ent_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME[entitlement_name]
    if not allow_beta and ent_cls.is_beta:
        tmpl = ua_status.MESSAGE_INVALID_SERVICE_OP_FAILURE_TMPL
        raise exceptions.UserFacingError(
            tmpl.format(operation="enable", name=entitlement_name))

    entitlement = ent_cls(cfg, assume_yes=assume_yes)
    ret = entitlement.enable(silent_if_inapplicable=silent_if_inapplicable)
    cfg.status()  # Update the status cache
    return ret
コード例 #14
0
def action_attach(args, cfg):
    if not args.token:
        raise exceptions.UserFacingError(
            ua_status.MESSAGE_ATTACH_REQUIRES_TOKEN)
    return _attach_with_token(cfg,
                              token=args.token,
                              allow_enable=args.auto_enable)
コード例 #15
0
def export_gpg_key_from_keyring(
    key_id: str, source_keyring_file: str, destination_keyfile: str
) -> None:
    """Export a specific key from source_keyring_file into destination_keyfile

    :param key_id: Long fingerprint of key to export.
    :param source_keyring_file: The keyring file from which to export.
    :param destination_keyfile: The filename created with the single exported
        key.

    :raise UserFacingError: Any GPG errors or if specific key does not exist in
        the source_keyring_file.
    """
    export_cmd = [
        "gpg",
        "--output",
        destination_keyfile,
        "--yes",
        "--no-auto-check-trustdb",
        "--no-default-keyring",
        "--keyring",
        source_keyring_file,
        "--export",
        key_id,
    ]
    logging.debug("Exporting GPG key %s from %s", key_id, source_keyring_file)
    try:
        out, err = util.subp(export_cmd)
    except util.ProcessExecutionError as exc:
        with util.disable_log_to_console():
            logging.error(str(exc))
        raise exceptions.UserFacingError(
            "Unable to export GPG keys from keyring {}".format(
                source_keyring_file
            )
        )
    if "nothing exported" in err:
        raise exceptions.UserFacingError(
            "GPG key '{}' not found in {}".format(key_id, source_keyring_file)
        )
    if not os.path.exists(destination_keyfile):
        msg = "Unexpected error exporting GPG key '{}' from {}".format(
            key_id, source_keyring_file
        )
        with util.disable_log_to_console():
            logging.error(msg + " Error: {}".format(err))
        raise exceptions.UserFacingError(msg)
コード例 #16
0
 def new_f(args, cfg):
     if hasattr(args, "name"):
         name = args.name
         tmpl = ua_status.MESSAGE_INVALID_SERVICE_OP_FAILURE_TMPL
         if name not in entitlements.ENTITLEMENT_CLASS_BY_NAME:
             raise exceptions.UserFacingError(
                 tmpl.format(operation=operation, name=name))
     return f(args, cfg)
コード例 #17
0
    def test_install_packages_dont_fail_if_conditional_pkgs_not_installed(
            self, m_run_apt_install, m_installed_pkgs,
            fips_entitlement_factory):

        conditional_pkgs = ["b", "c"]
        m_installed_pkgs.return_value = conditional_pkgs
        packages = ["a"]
        entitlement = fips_entitlement_factory(additional_packages=packages)

        m_run_apt_install.side_effect = [
            True,
            exceptions.UserFacingError("error"),
            exceptions.UserFacingError("error"),
        ]

        fake_stdout = io.StringIO()
        with contextlib.redirect_stdout(fake_stdout):
            with mock.patch.object(type(entitlement), "conditional_packages",
                                   conditional_pkgs):
                entitlement.install_packages()

        install_cmds = []
        all_pkgs = packages + conditional_pkgs
        for pkg in all_pkgs:
            install_cmds.append(
                mock.call(
                    packages=[pkg],
                    apt_options=[
                        "--allow-downgrades",
                        '-o Dpkg::Options::="--force-confdef"',
                        '-o Dpkg::Options::="--force-confold"',
                    ],
                    error_msg="Could not enable {}.".format(entitlement.title),
                    env={"DEBIAN_FRONTEND": "noninteractive"},
                ))

        expected_msg = "\n".join([
            "Installing {} packages".format(entitlement.title),
            messages.FIPS_PACKAGE_NOT_AVAILABLE.format(
                service=entitlement.title, pkg="b"),
            messages.FIPS_PACKAGE_NOT_AVAILABLE.format(
                service=entitlement.title, pkg="c"),
        ])

        assert install_cmds == m_run_apt_install.call_args_list
        assert expected_msg.strip() in fake_stdout.getvalue().strip()
コード例 #18
0
def process_entitlements_delta(
    past_entitlements: "Dict[str, Any]",
    new_entitlements: "Dict[str, Any]",
    allow_enable: bool,
    series_overrides: bool = True,
) -> None:
    """Iterate over all entitlements in new_entitlement and apply any delta
    found according to past_entitlements.

    :param past_entitlements: dict containing the last valid information
        regarding service entitlements.
    :param new_entitlements: dict containing the current information regarding
        service entitlements.
    :param allow_enable: Boolean set True if allowed to perform the enable
        operation. When False, a message will be logged to inform the user
        about the recommended enabled service.
    :param series_overrides: Boolean set True if series overrides should be
        applied to the new_access dict.
    """
    delta_error = False
    unexpected_error = False
    for name, new_entitlement in sorted(new_entitlements.items()):
        try:
            process_entitlement_delta(
                past_entitlements.get(name, {}),
                new_entitlement,
                allow_enable=allow_enable,
                series_overrides=series_overrides,
            )
        except exceptions.UserFacingError:
            delta_error = True
            with util.disable_log_to_console():
                logging.exception(
                    "Failed to process contract delta for {name}:"
                    " {delta}".format(name=name, delta=new_entitlement))
        except Exception:
            unexpected_error = True
            with util.disable_log_to_console():
                logging.exception(
                    "Unexpected error processing contract delta for {name}:"
                    " {delta}".format(name=name, delta=new_entitlement))
    if unexpected_error:
        raise exceptions.UserFacingError(status.MESSAGE_UNEXPECTED_ERROR)
    elif delta_error:
        raise exceptions.UserFacingError(
            status.MESSAGE_ATTACH_FAILURE_DEFAULT_SERVICES)
コード例 #19
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
コード例 #20
0
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
コード例 #21
0
    def test_refresh_contract_error_on_failure_to_update_contract(
            self, request_updated_contract, logging_error, getuid):
        """On failure in request_updates_contract emit an error."""
        request_updated_contract.side_effect = exceptions.UserFacingError(
            "Failure to refresh")

        cfg = FakeConfig.for_attached_machine()

        with pytest.raises(exceptions.UserFacingError) as excinfo:
            action_refresh(mock.MagicMock(), cfg)

        assert "Failure to refresh" == excinfo.value.msg
コード例 #22
0
def prompt_request_macaroon(cfg: UAConfig, caveat_id: str) -> dict:
    discharge_macaroon = cfg.read_cache('macaroon')
    if discharge_macaroon:
        # TODO(invalidate cached macaroon on root-macaroon or discharge expiry)
        return discharge_macaroon
    email = input('Email: ')
    password = getpass.getpass('Password: '******'email': email, 'password': password, 'caveat_id': caveat_id}
    sso_client = UbuntuSSOClient(cfg)
    content = None
    twofactor_retries = 0
    while True:
        try:
            content = sso_client.request_discharge_macaroon(**args)
        except SSOAuthError as e:
            if API_ERROR_2FA_REQUIRED in e:
                args['otp'] = input('Second-factor auth: ')
                continue
            elif API_ERROR_INVALID_CREDENTIALS in e:
                # This is arguably bug in canonical-identity-provider code
                # that the error 'code' is 'invalid-credentials' when docs
                # clearly designates a 'twofactor-error' code that should be
                # emitted when the 2FA token is invalid. There are no plans for
                # changes to the error codes or messages as it might break
                # existing clients. As a result, we have to distinguish
                # email/password invalid-credentials errors from 2-factor
                # errors by searching the attached error 'message' field for
                # 2-factor.
                if '2-factor' in e[API_ERROR_INVALID_CREDENTIALS]:
                    if twofactor_retries < TWOFACTOR_RETRIES:
                        args['otp'] = input(
                            'Invalid second-factor auth, try again: ')
                        twofactor_retries += 1
                        continue
            raise exceptions.UserFacingError(str(e))
        break
    if not content:
        raise exceptions.UserFacingError('SSO server returned empty content')
    return content
コード例 #23
0
def run_apt_command(cmd, error_msg) -> str:
    """Run an apt command, retrying upon failure APT_RETRIES times.

    :return: stdout from successful run of the apt command.
    :raise UserFacingError: on issues running apt-cache policy.
    """
    try:
        out, _err = util.subp(cmd, capture=True, retry_sleeps=APT_RETRIES)
    except util.ProcessExecutionError as e:
        if "Could not get lock /var/lib/dpkg/lock" in str(e.stderr):
            error_msg += " Another process is running APT."
        raise exceptions.UserFacingError(error_msg)
    return out
コード例 #24
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
コード例 #25
0
    def help(self, 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.
        """
        from uaclient.contract import get_available_resources
        from uaclient.entitlements import ENTITLEMENT_CLASS_BY_NAME

        resources = get_available_resources(self)
        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 and name in ENTITLEMENT_CLASS_BY_NAME:
                help_resource = resource
                help_ent_cls = ENTITLEMENT_CLASS_BY_NAME.get(name)
                help_ent = help_ent_cls(self)
                break

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

        if self.is_attached:
            service_status = self._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 = status.UserFacingAvailability.AVAILABLE.value
            else:
                available = status.UserFacingAvailability.UNAVAILABLE.value

            response_dict["available"] = available

        response_dict["help"] = help_ent.help_info
        return response_dict
コード例 #26
0
class TestAttachWithToken:
    @pytest.mark.parametrize(
        "request_updated_contract_side_effect, expected_error_class,"
        " expect_status_call",
        [
            (None, None, False),
            (exceptions.UrlError("cause"), exceptions.UrlError, True),
            (
                exceptions.UserFacingError("test"),
                exceptions.UserFacingError,
                True,
            ),
        ],
    )
    @mock.patch(M_PATH + "identity.get_instance_id", return_value="my-iid")
    @mock.patch("uaclient.jobs.update_messaging.update_apt_and_motd_messages")
    @mock.patch("uaclient.status.status")
    @mock.patch(M_PATH + "contract.request_updated_contract")
    @mock.patch(M_PATH + "config.UAConfig.write_cache")
    def test_attach_with_token(
        self,
        m_write_cache,
        m_request_updated_contract,
        m_status,
        m_update_apt_and_motd_msgs,
        _m_get_instance_id,
        request_updated_contract_side_effect,
        expected_error_class,
        expect_status_call,
        FakeConfig,
    ):
        cfg = FakeConfig()
        m_request_updated_contract.side_effect = (
            request_updated_contract_side_effect
        )
        if expected_error_class:
            with pytest.raises(expected_error_class):
                attach_with_token(cfg, "token", False)
        else:
            attach_with_token(cfg, "token", False)
        if expect_status_call:
            assert [mock.call(cfg=cfg)] == m_status.call_args_list
        if not expect_status_call:
            assert [
                mock.call("instance-id", "my-iid")
            ] == m_write_cache.call_args_list

        assert [mock.call(cfg)] == m_update_apt_and_motd_msgs.call_args_list
コード例 #27
0
def _get_contract_token_from_cloud_identity(cfg: config.UAConfig) -> str:
    """Detect cloud_type and request a contract token from identity info.

    :param cfg: a ``config.UAConfig`` instance

    :raise NonAutoAttachImageError: When not on an auto-attach image type.
    :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.
    :raise NonAutoAttachImageError: If this cloud type does not have
        auto-attach support.

    :return: contract token obtained from identity doc
    """
    try:
        instance = identity.cloud_instance_factory()
    except exceptions.UserFacingError as e:
        if cfg.is_attached:
            # We are attached on non-Pro Image, just report already attached
            raise exceptions.AlreadyAttachedError(cfg)
        # Unattached on non-Pro return UserFacing error msg details
        raise e
    current_iid = identity.get_instance_id()
    if cfg.is_attached:
        prev_iid = cfg.read_cache("instance-id")
        if current_iid == prev_iid:
            raise exceptions.AlreadyAttachedError(cfg)
        print("Re-attaching Ubuntu Advantage subscription on new instance")
        if _detach(cfg, assume_yes=True) != 0:
            raise exceptions.UserFacingError(
                ua_status.MESSAGE_DETACH_AUTOMATION_FAILURE
            )
    contract_client = contract.UAContractClient(cfg)
    try:
        tokenResponse = contract_client.request_auto_attach_contract_token(
            instance=instance
        )
    except contract.ContractAPIError as e:
        if e.code and 400 <= e.code < 500:
            raise exceptions.NonAutoAttachImageError(
                ua_status.MESSAGE_UNSUPPORTED_AUTO_ATTACH
            )
        raise e
    if current_iid:
        cfg.write_cache("instance-id", current_iid)

    return tokenResponse["contractToken"]
コード例 #28
0
def export_gpg_key(source_keyfile: str, destination_keyfile: str) -> None:
    """Copy a specific key from source_keyring_dir into destination_keyfile

    :param source_keyfile: Path of source keyring file to export.
    :param destination_keyfile: The filename created with the single exported
        key.

    :raise UserFacingError: Any GPG errors or if specific key does not exist in
        the source_keyring_file.
    """
    logging.debug("Exporting GPG key %s", source_keyfile)
    if not os.path.exists(source_keyfile):
        raise exceptions.UserFacingError(
            "GPG key '{}' not found".format(source_keyfile))
    shutil.copy(source_keyfile, destination_keyfile)
    os.chmod(destination_keyfile, 0o644)
コード例 #29
0
    def test_failed_run_update_command_clean_apt_cache_policy_cache(
            self, m_subp):
        m_subp.side_effect = [
            ("policy1", ""),
            exceptions.UserFacingError("test"),
            ("policy2", ""),
        ]

        assert "policy1" == run_apt_cache_policy_command()
        # Confirming that caching is happening
        assert "policy1" == run_apt_cache_policy_command()

        with pytest.raises(exceptions.UserFacingError):
            run_apt_update_command()

        # Confirm cache was cleared
        assert "policy2" == run_apt_cache_policy_command()
        run_apt_cache_policy_command.cache_clear()
コード例 #30
0
def run_apt_update_command(env: Optional[Dict[str, str]] = {}) -> str:
    try:
        out = run_apt_command(cmd=["apt-get", "update"], env=env)
    except exceptions.APTProcessConflictError:
        raise exceptions.APTUpdateProcessConflictError()
    except exceptions.APTInvalidRepoError as e:
        raise exceptions.APTUpdateInvalidRepoError(repo_msg=e.msg)
    except exceptions.UserFacingError as e:
        raise exceptions.UserFacingError(
            msg=messages.APT_UPDATE_FAILED.msg + "\n" + e.msg,
            msg_code=messages.APT_UPDATE_FAILED.name,
        )
    finally:
        # Whenever we run an apt-get update command, we must invalidate
        # the existing apt-cache policy cache. Otherwise, we could provide
        # users with incorrect values.
        run_apt_cache_policy_command.cache_clear()

    return out