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)
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'])
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
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
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)
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 )
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")
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))
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, ))
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
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)
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
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
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)
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)
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)
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()
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)
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
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
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
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
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
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 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
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
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"]
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)
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()
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