def install_packages(self, package_list: "List[str]" = None) -> None: """Install contract recommended packages for the entitlement. :param package_list: Optional package list to use instead of self.packages. """ print("Installing {title} packages".format(title=self.title)) if self.apt_noninteractive: env = {"DEBIAN_FRONTEND": "noninteractive"} apt_options = [ '-o Dpkg::Options::="--force-confdef"', '-o Dpkg::Options::="--force-confold"', ] else: env = {} apt_options = [] if not package_list: package_list = self.packages try: apt.run_apt_command( ["apt-get", "install", "--assume-yes"] + apt_options + package_list, status.MESSAGE_ENABLED_FAILED_TMPL.format(title=self.title), env=env, ) except exceptions.UserFacingError: self._cleanup() raise
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. @raises: UserFacingError on failure to install suggested packages """ if not self.can_enable(silent=silent_if_inapplicable): return False self.setup_apt_config() if self.packages: try: print("Installing {title} packages".format(title=self.title)) for msg in self.messaging.get("pre_install", []): print(msg) apt.run_apt_command( ["apt-get", "install", "--assume-yes"] + self.packages, status.MESSAGE_ENABLED_FAILED_TMPL.format( title=self.title ), ) except exceptions.UserFacingError: self._cleanup() raise print(status.MESSAGE_ENABLED_TMPL.format(title=self.title)) for msg in self.messaging.get("post_enable", []): print(msg) return True
def upgrade_packages_and_attach( cfg: UAConfig, upgrade_pkgs: List[str], pocket: str, dry_run: bool ) -> bool: """Upgrade available packages to fix a CVE. Upgrade all packages in upgrades_packages and, if necessary, prompt regarding system attach prior to upgrading UA packages. :return: True if package upgrade completed or unneeded, False otherwise. """ if not upgrade_pkgs: return True # If we are running on --dry-run mode, we don't need to be root # to understand what will happen with the system if os.getuid() != 0 and not dry_run: print(messages.SECURITY_APT_NON_ROOT) return False if pocket != UBUNTU_STANDARD_UPDATES_POCKET: # We are now using status-cache because non-root users won't # have access to the private machine_token.json file. We # can use the status-cache as a proxy for the attached # information status_cache = cfg.read_cache("status-cache") or {} if not status_cache.get("attached", False): if not _check_attached(cfg, dry_run): return False elif _check_subscription_is_expired( status_cache=status_cache, cfg=cfg, dry_run=dry_run ): return False if not _check_subscription_for_required_service(pocket, cfg, dry_run): # User subscription does not have required service enabled return False print( colorize_commands( [ ["apt", "update", "&&"] + ["apt", "install", "--only-upgrade", "-y"] + sorted(upgrade_pkgs) ] ) ) if not dry_run: apt.run_apt_update_command() apt.run_apt_command( cmd=["apt-get", "install", "--only-upgrade", "-y"] + upgrade_pkgs, error_msg=messages.APT_INSTALL_FAILED.msg, env={"DEBIAN_FRONTEND": "noninteractive"}, ) return True
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 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. @raises: UserFacingError on failure to install suggested packages """ msg_ops = self.messaging.get("pre_enable", []) if not handle_message_operations(msg_ops): return False if not self.can_enable(silent=silent_if_inapplicable): return False self.setup_apt_config() if self.packages: try: print("Installing {title} packages".format(title=self.title)) msg_ops = self.messaging.get("pre_install", []) if not handle_message_operations(msg_ops): return False if self.apt_noninteractive: env = {"DEBIAN_FRONTEND": "noninteractive"} apt_options = [ '-o Dpkg::Options::="--force-confdef"', '-o Dpkg::Options::="--force-confold"', ] else: env = {} apt_options = [] apt.run_apt_command( ["apt-get", "install", "--assume-yes"] + apt_options + self.packages, status.MESSAGE_ENABLED_FAILED_TMPL.format( title=self.title ), env=env, ) except exceptions.UserFacingError: self._cleanup() raise print(status.MESSAGE_ENABLED_TMPL.format(title=self.title)) msg_ops = self.messaging.get("post_enable", []) if not handle_message_operations(msg_ops): return False return True
def test_run_apt_command_with_invalid_repositories( self, m_subp, error_list, output_list ): error_msg = "\n".join(error_list) m_subp.side_effect = util.ProcessExecutionError( cmd="apt update", stderr=error_msg ) with pytest.raises(exceptions.UserFacingError) as excinfo: run_apt_command( cmd=["apt", "update"], error_msg=status.MESSAGE_APT_UPDATE_FAILED, ) expected_message = "\n".join(output_list) assert expected_message == excinfo.value.msg
def remove_apt_config(self, run_apt_update=True): """Remove any repository apt configuration files. :param run_apt_update: If after removing the apt update command after removing the apt files. """ series = util.get_platform_info()["series"] repo_filename = self.repo_list_file_tmpl.format(name=self.name) entitlement = self.cfg.entitlements[self.name].get("entitlement", {}) access_directives = entitlement.get("directives", {}) repo_url = access_directives.get("aptURL") if not repo_url: raise exceptions.MissingAptURLDirective(self.name) if self.disable_apt_auth_only: # We only remove the repo from the apt auth file, because ESM Infra # is a special-case: we want to be able to report on the # available ESM Infra updates even when it's disabled apt.remove_repo_from_apt_auth_file(repo_url) apt.restore_commented_apt_list_file(repo_filename) else: apt.remove_auth_apt_repo( repo_filename, repo_url, self.repo_key_file ) apt.remove_apt_list_files(repo_url, series) if self.repo_pin_priority: repo_pref_file = self.repo_pref_file_tmpl.format(name=self.name) if self.repo_pin_priority == "never": # Disable the repo with a pinning file 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) if run_apt_update: print(status.MESSAGE_APT_UPDATING_LISTS) apt.run_apt_command( ["apt-get", "update"], status.MESSAGE_APT_UPDATE_FAILED )
def setup_apt_config(self, silent: bool = False) -> None: """Setup apt config based on the resourceToken and directives. FIPS-specifically handle apt-mark unhold :raise UserFacingError: on failure to setup any aspect of this apt configuration """ cmd = ["apt-mark", "showholds"] holds = apt.run_apt_command(cmd, " ".join(cmd) + " failed.") unholds = [] for hold in holds.splitlines(): if hold in self.fips_pro_package_holds: unholds.append(hold) if unholds: unhold_cmd = ["apt-mark", "unhold"] + unholds holds = apt.run_apt_command( unhold_cmd, " ".join(unhold_cmd) + " failed." ) super().setup_apt_config(silent=silent)
def application_status(self) -> 'Tuple[ApplicationStatus, str]': entitlement_cfg = self.cfg.entitlements.get(self.name, {}) directives = entitlement_cfg.get( 'entitlement', {}).get('directives', {}) repo_url = directives.get('aptURL') if not repo_url: repo_url = self.repo_url protocol, repo_path = repo_url.split('://') policy = apt.run_apt_command( ['apt-cache', 'policy'], status.MESSAGE_APT_POLICY_FAILED) match = re.search(r'(?P<pin>(-)?\d+) %s[^-]' % repo_url, policy) if match and match.group('pin') != APT_DISABLED_PIN: return ApplicationStatus.ENABLED, '%s is active' % self.title return ApplicationStatus.DISABLED, '%s is not configured' % self.title
def remove_packages(self) -> None: """Remove fips meta package to disable the service. FIPS meta-package will unset grub config options which will deactivate FIPS on any related packages. """ installed_packages = set(apt.get_installed_packages()) fips_metapackage = set(self.packages).difference( set(self.conditional_packages) ) remove_packages = fips_metapackage.intersection(installed_packages) if remove_packages: env = {"DEBIAN_FRONTEND": "noninteractive"} apt_options = [ '-o Dpkg::Options::="--force-confdef"', '-o Dpkg::Options::="--force-confold"', ] apt.run_apt_command( ["apt-get", "remove", "--assume-yes"] + apt_options + list(remove_packages), messages.DISABLE_FAILED_TMPL.format(title=self.title), env=env, )
def application_status(self) -> "Tuple[ApplicationStatus, str]": entitlement_cfg = self.cfg.entitlements.get(self.name, {}) directives = entitlement_cfg.get("entitlement", {}).get( "directives", {} ) repo_url = directives.get("aptURL") if not repo_url: repo_url = self.repo_url protocol, repo_path = repo_url.split("://") policy = apt.run_apt_command( ["apt-cache", "policy"], status.MESSAGE_APT_POLICY_FAILED ) match = re.search(r"(?P<pin>(-)?\d+) {}[^-]".format(repo_url), policy) if match and match.group("pin") != APT_DISABLED_PIN: return ApplicationStatus.ENABLED, "{} is active".format(self.title) return ( ApplicationStatus.DISABLED, "{} is not configured".format(self.title), )
def setup_apt_config(self) -> None: """Setup apt config based on the resourceToken and directives. :raise UserFacingError: on failure to setup any aspect of this apt configuration """ series = util.get_platform_info()["series"] repo_filename = self.repo_list_file_tmpl.format( name=self.name, series=series ) resource_cfg = self.cfg.entitlements.get(self.name) directives = resource_cfg["entitlement"].get("directives", {}) token = resource_cfg.get("resourceToken") if not token: logging.debug( "No specific resourceToken present. Using machine token" " as %s credentials", self.title, ) token = self.cfg.machine_token["machineToken"] 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, status.MESSAGE_ENABLED_FAILED_TMPL.format( title=self.title ), ) ) repo_pref_file = self.repo_pref_file_tmpl.format( name=self.name, series=series ) 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: print( "Installing prerequisites: {}".format( ", ".join(prerequisite_pkgs) ) ) try: apt.run_apt_command( ["apt-get", "install", "--assume-yes"] + prerequisite_pkgs, status.MESSAGE_APT_INSTALL_FAILED, ) 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 print(status.MESSAGE_APT_UPDATING_LISTS) try: apt.run_apt_command( ["apt-get", "update"], status.MESSAGE_APT_UPDATE_FAILED ) except exceptions.UserFacingError: self.remove_apt_config() raise
def setup_apt_config(self) -> None: """Setup apt config based on the resourceToken and directives. :raise UserFacingError: on failure to setup any aspect of this apt configuration """ series = util.get_platform_info()['series'] repo_filename = self.repo_list_file_tmpl.format( name=self.name, series=series) resource_cfg = self.cfg.entitlements.get(self.name) directives = resource_cfg['entitlement'].get('directives', {}) token = resource_cfg.get('resourceToken') if not token: logging.debug( 'No specific resourceToken present. Using machine token' ' as %s credentials', self.title) token = self.cfg.machine_token['machineToken'] if directives.get('aptKey'): logging.debug( "Ignoring aptKey directive '%s'", directives.get('aptKey')) keyring_file = os.path.join(apt.KEYRINGS_DIR, self.repo_key_file) repo_url = directives.get('aptURL') if not repo_url: repo_url = self.repo_url repo_suites = directives.get('suites') if not repo_suites: raise exceptions.UserFacingError( 'Empty %s apt suites directive from %s' % (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 '%s'.\n" "%s" % (self.origin, status.MESSAGE_ENABLED_FAILED_TMPL.format( title=self.title))) repo_pref_file = self.repo_pref_file_tmpl.format( name=self.name, series=series) 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: print('Installing prerequisites: {}'.format( ', '.join(prerequisite_pkgs))) try: apt.run_apt_command( ['apt-get', 'install', '--assume-yes'] + prerequisite_pkgs, status.MESSAGE_APT_INSTALL_FAILED) except exceptions.UserFacingError: self.remove_apt_config() raise apt.add_auth_apt_repo(repo_filename, repo_url, token, repo_suites, keyring_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 print('Updating package lists') try: apt.run_apt_command( ['apt-get', 'update'], status.MESSAGE_APT_UPDATE_FAILED) except exceptions.UserFacingError: self.remove_apt_config() raise