def _detach(cfg: config.UAConfig, assume_yes: bool) -> int: """Detach the machine from the active Ubuntu Advantage subscription, :param cfg: a ``config.UAConfig`` instance :param assume_yes: Assume a yes answer to any prompts requested. In this case, it means automatically disable any service during detach. @return: 0 on success, 1 otherwise """ to_disable = [] for ent_cls in entitlements.ENTITLEMENT_CLASSES: ent = ent_cls(cfg) if ent.can_disable(silent=True): to_disable.append(ent) if to_disable: suffix = "s" if len(to_disable) > 1 else "" print("Detach will disable the following service{}:".format(suffix)) for ent in to_disable: print(" {}".format(ent.name)) if not util.prompt_for_confirmation(assume_yes=assume_yes): return 1 for ent in to_disable: ent.disable(silent=True) contract_client = contract.UAContractClient(cfg) machine_token = cfg.machine_token["machineToken"] contract_id = cfg.machine_token["machineTokenInfo"]["contractInfo"]["id"] contract_client.detach_machine_from_contract(machine_token, contract_id) cfg.delete_cache() print(ua_status.MESSAGE_DETACH_SUCCESS) return 0
def auto_attach( cfg: config.UAConfig, cloud: clouds.AutoAttachCloudInstance ) -> None: """ :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. """ contract_client = contract.UAContractClient(cfg) try: tokenResponse = contract_client.request_auto_attach_contract_token( instance=cloud ) except exceptions.ContractAPIError as e: if e.code and 400 <= e.code < 500: raise exceptions.NonAutoAttachImageError( messages.UNSUPPORTED_AUTO_ATTACH ) raise e token = tokenResponse["contractToken"] attach_with_token(cfg, token=token, allow_enable=True)
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. :return: contract token obtained from identity doc """ cloud_type = identity.get_cloud_type() if cloud_type not in ("aws", ): # TODO(avoid hard-coding supported types) raise exceptions.NonAutoAttachImageError( ua_status.MESSAGE_UNSUPPORTED_AUTO_ATTACH_CLOUD_TYPE.format( cloud_type=cloud_type)) instance = identity.cloud_instance_factory() contract_client = contract.UAContractClient(cfg) pkcs7 = instance.identity_doc try: # TODO(make this logic cloud-agnostic if possible) tokenResponse = contract_client.request_aws_contract_token(pkcs7) except contract.ContractAPIError as e: if contract.API_ERROR_MISSING_INSTANCE_INFORMATION in e: raise exceptions.NonAutoAttachImageError( ua_status.MESSAGE_UNSUPPORTED_AUTO_ATTACH) raise e return tokenResponse["contractToken"]
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(): token = self.cfg.machine_token['machineToken'] contract_client = contract.UAContractClient(self.cfg) contract_client.request_resource_machine_access(token, self.name) 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 _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 """ instance = identity.cloud_instance_factory() contract_client = contract.UAContractClient(cfg) try: tokenResponse = contract_client.request_auto_attach_contract_token( instance=instance) except contract.ContractAPIError as e: if contract.API_ERROR_MISSING_INSTANCE_INFORMATION in e: raise exceptions.NonAutoAttachImageError( ua_status.MESSAGE_UNSUPPORTED_AUTO_ATTACH) raise e return tokenResponse["contractToken"]
def action_detach(args, cfg): """Perform the detach action for this machine. @return: 0 on success, 1 otherwise """ to_disable = [] for ent_cls in entitlements.ENTITLEMENT_CLASSES: ent = ent_cls(cfg) if ent.can_disable(silent=True): to_disable.append(ent) if to_disable: suffix = "s" if len(to_disable) > 1 else "" print("Detach will disable the following service{}:".format(suffix)) for ent in to_disable: print(" {}".format(ent.name)) if not args.assume_yes and not util.prompt_for_confirmation(): return 1 for ent in to_disable: ent.disable(silent=True) contract_client = contract.UAContractClient(cfg) machine_token = cfg.machine_token["machineToken"] contract_id = cfg.machine_token["machineTokenInfo"]["contractInfo"]["id"] contract_client.detach_machine_from_contract(machine_token, contract_id) cfg.delete_cache() print(ua_status.MESSAGE_DETACH_SUCCESS) return 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"]
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
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(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 machine_token_response = contract_client.request_contract_machine_attach( contract_token=contract_token) contractInfo = machine_token_response['machineTokenInfo']['contractInfo'] for entitlement in contractInfo['resourceEntitlements']: if entitlement.get('entitled'): # Obtain each entitlement's accessContext for this machine entitlement_name = entitlement['type'] contract_client.request_resource_machine_access( cfg.machine_token['machineToken'], entitlement_name) print("This machine is now attached to '%s'.\n" % machine_token_response['machineTokenInfo']['contractInfo']['name']) action_status(args=None, cfg=cfg) return 0
def can_enable(self): """Report whether or not enabling is possible for the entitlement.""" if os.getuid() != 0: print(status.MESSAGE_NONROOT_USER) return False if not self.cfg.is_attached: print(status.MESSAGE_UNATTACHED) return False if self.is_access_expired(): token = self.cfg.machine_token['machineToken'] contract_client = contract.UAContractClient(self.cfg) contract_client.request_resource_machine_access( token, self.name) if not self.contract_status() == status.ENTITLED: print(status.MESSAGE_UNENTITLED_TMPL.format(title=self.title)) return False op_status, op_status_details = self.operational_status() if op_status == status.ACTIVE: print(status.MESSAGE_ALREADY_ENABLED_TMPL.format(title=self.title)) return False if op_status == status.INAPPLICABLE: print(op_status_details) return False return True
def setup_apt_config(self, silent: bool = False) -> None: """Setup apt config based on the resourceToken and directives. Also sets up apt proxy if necessary. :raise UserFacingError: on failure to setup any aspect of this apt configuration """ http_proxy = None # type: Optional[str] https_proxy = None # type: Optional[str] scope = None # type: Optional[apt.AptProxyScope] if self.cfg.global_apt_http_proxy or self.cfg.global_apt_https_proxy: http_proxy = util.validate_proxy( "http", self.cfg.global_apt_http_proxy, util.PROXY_VALIDATION_APT_HTTP_URL, ) https_proxy = util.validate_proxy( "https", self.cfg.global_apt_https_proxy, util.PROXY_VALIDATION_APT_HTTPS_URL, ) scope = apt.AptProxyScope.GLOBAL elif self.cfg.ua_apt_http_proxy or self.cfg.ua_apt_https_proxy: http_proxy = util.validate_proxy( "http", self.cfg.ua_apt_http_proxy, util.PROXY_VALIDATION_APT_HTTP_URL, ) https_proxy = util.validate_proxy( "https", self.cfg.ua_apt_https_proxy, util.PROXY_VALIDATION_APT_HTTPS_URL, ) scope = apt.AptProxyScope.UACLIENT apt.setup_apt_proxy(http_proxy=http_proxy, https_proxy=https_proxy, proxy_scope=scope) repo_filename = self.repo_list_file_tmpl.format(name=self.name) resource_cfg = self.cfg.entitlements.get(self.name) directives = resource_cfg["entitlement"].get("directives", {}) obligations = resource_cfg["entitlement"].get("obligations", {}) token = resource_cfg.get("resourceToken") if not token: machine_token = self.cfg.machine_token["machineToken"] if not obligations.get("enableByDefault"): # services that are not enableByDefault need to obtain specific # resource access for tokens. We want to refresh this every # enable call because it is not refreshed by `ua refresh`. client = contract.UAContractClient(self.cfg) machine_access = client.request_resource_machine_access( machine_token, self.name) if machine_access: token = machine_access.get("resourceToken") if not token: token = machine_token logging.warning( "No resourceToken present in contract for service %s." " Using machine token as credentials", self.title, ) aptKey = directives.get("aptKey") if not aptKey: raise exceptions.UserFacingError( "Ubuntu Advantage server provided no aptKey directive for" " {}.".format(self.name)) repo_url = directives.get("aptURL") if not repo_url: raise exceptions.MissingAptURLDirective(self.name) repo_suites = directives.get("suites") if not repo_suites: raise exceptions.UserFacingError( "Empty {} apt suites directive from {}".format( self.name, self.cfg.contract_url)) if self.repo_pin_priority: if not self.origin: raise exceptions.UserFacingError( "Cannot setup apt pin. Empty apt repo origin value '{}'.\n" "{}".format( self.origin, messages.ENABLED_FAILED.format(title=self.title).msg, )) repo_pref_file = self.repo_pref_file_tmpl.format(name=self.name) if self.repo_pin_priority != "never": apt.add_ppa_pinning( repo_pref_file, repo_url, self.origin, self.repo_pin_priority, ) elif os.path.exists(repo_pref_file): os.unlink(repo_pref_file) # Remove disabling apt pref file prerequisite_pkgs = [] if not os.path.exists(apt.APT_METHOD_HTTPS_FILE): prerequisite_pkgs.append("apt-transport-https") if not os.path.exists(apt.CA_CERTIFICATES_FILE): prerequisite_pkgs.append("ca-certificates") if prerequisite_pkgs: if not silent: event.info("Installing prerequisites: {}".format( ", ".join(prerequisite_pkgs))) try: apt.run_apt_install_command(packages=prerequisite_pkgs) except exceptions.UserFacingError: self.remove_apt_config() raise apt.add_auth_apt_repo(repo_filename, repo_url, token, repo_suites, self.repo_key_file) # Run apt-update on any repo-entitlement enable because the machine # probably wants access to the repo that was just enabled. # Side-effect is that apt policy will now report the repo as accessible # which allows ua status to report correct info if not silent: event.info(messages.APT_UPDATING_LISTS) try: apt.run_apt_update_command() except exceptions.UserFacingError: self.remove_apt_config(run_apt_update=False) raise