def static_affordances(self) -> "Tuple[StaticAffordance, ...]": # Use a lambda so we can mock util.is_container in tests from uaclient.entitlements.fips import FIPSEntitlement from uaclient.entitlements.fips import FIPSUpdatesEntitlement fips_ent = FIPSEntitlement(self.cfg) fips_update_ent = FIPSUpdatesEntitlement(self.cfg) enabled_status = ApplicationStatus.ENABLED is_fips_enabled = bool( fips_ent.application_status()[0] == enabled_status ) is_fips_updates_enabled = bool( fips_update_ent.application_status()[0] == enabled_status ) return ( ( "Cannot install Livepatch on a container", lambda: util.is_container(), False, ), ( "Cannot enable Livepatch when FIPS is enabled", lambda: is_fips_enabled, False, ), ( "Cannot enable Livepatch when FIPS Updates is enabled", lambda: is_fips_updates_enabled, False, ), )
def messaging(self) -> MessagingOperationsDict: post_enable = None # type: Optional[MessagingOperations] if util.is_container(): pre_enable_prompt = ( messages.PROMPT_FIPS_CONTAINER_PRE_ENABLE.format( title=self.title ) ) post_enable = [messages.FIPS_RUN_APT_UPGRADE] else: pre_enable_prompt = messages.PROMPT_FIPS_UPDATES_PRE_ENABLE return { "pre_enable": [ ( util.prompt_for_confirmation, {"msg": pre_enable_prompt, "assume_yes": self.assume_yes}, ) ], "post_enable": post_enable, "pre_disable": [ ( util.prompt_for_confirmation, { "msg": messages.PROMPT_FIPS_PRE_DISABLE, "assume_yes": self.assume_yes, }, ) ], }
def static_affordances(self) -> "Tuple[StaticAffordance, ...]": # Use a lambda so we can mock util.is_container in tests from uaclient.entitlements.livepatch import LivepatchEntitlement livepatch_ent = LivepatchEntitlement(self.cfg) enabled_status = status.ApplicationStatus.ENABLED is_livepatch_enabled = bool( livepatch_ent.application_status()[0] == enabled_status ) return ( ( "Cannot install {} on a container".format(self.title), lambda: util.is_container(), False, ), ( "Cannot enable {} when Livepatch is enabled".format( self.title ), lambda: is_livepatch_enabled, False, ), )
def test_true_on_run_systemd_container(self, m_subp, tmpdir): """Return True when /run/systemd/container exists.""" m_subp.side_effect = OSError('No systemd-detect-virt utility') tmpdir.join('systemd/container').write('', ensure=True) assert True is util.is_container(run_path=tmpdir.strpath) calls = [mock.call(['systemd-detect-virt', '--quiet', '--container'])] assert calls == m_subp.call_args_list
def test_true_on_run_container_type(self, m_subp, tmpdir): """Return True when /run/container_type exists.""" m_subp.side_effect = OSError("No systemd-detect-virt utility") tmpdir.join("container_type").write("") assert True is util.is_container(run_path=tmpdir.strpath) calls = [mock.call(["systemd-detect-virt", "--quiet", "--container"])] assert calls == m_subp.call_args_list
def static_affordances(self) -> Tuple[StaticAffordance, ...]: return ( ( messages.REALTIME_ERROR_INSTALL_ON_CONTAINER, lambda: util.is_container(), False, ), )
def test_false_on_non_sytemd_detect_virt_and_no_runfiles( self, m_subp, tmpdir): """Return False when sytemd-detect-virt erros and no /run/* files.""" m_subp.side_effect = OSError('No systemd-detect-virt utility') with mock.patch('uaclient.util.os.path.exists') as m_exists: m_exists.return_value = False assert False is util.is_container(run_path=tmpdir.strpath) calls = [mock.call(['systemd-detect-virt', '--quiet', '--container'])] assert calls == m_subp.call_args_list exists_calls = [ mock.call(tmpdir.join('container_type').strpath), mock.call(tmpdir.join('systemd/container').strpath) ] assert exists_calls == m_exists.call_args_list
def conditional_packages(self): """ Dictionary of conditional packages to be installed when enabling FIPS services. For example, if we are enabling FIPS services in a machine that has openssh-client installed, we will perform two actions: 1. Upgrade the package to the FIPS version 2. Install the corresponding hmac version of that package when available. """ series = util.get_platform_info().get("series", "") if util.is_container(): return FIPS_CONTAINER_CONDITIONAL_PACKAGES.get(series, []) return FIPS_CONDITIONAL_PACKAGES.get(series, [])
def application_status( self, ) -> Tuple[ApplicationStatus, Optional[messages.NamedMessage]]: super_status, super_msg = super().application_status() if util.is_container() and not util.should_reboot(): self.cfg.remove_notice( "", messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg ) return super_status, super_msg if os.path.exists(self.FIPS_PROC_FILE): self.cfg.remove_notice( "", messages.FIPS_SYSTEM_REBOOT_REQUIRED.msg ) self.cfg.remove_notice("", messages.FIPS_REBOOT_REQUIRED_MSG) if util.load_file(self.FIPS_PROC_FILE).strip() == "1": self.cfg.remove_notice( "", messages.NOTICE_FIPS_MANUAL_DISABLE_URL ) return super_status, super_msg else: self.cfg.remove_notice( "", messages.FIPS_DISABLE_REBOOT_REQUIRED ) self.cfg.add_notice( "", messages.NOTICE_FIPS_MANUAL_DISABLE_URL ) return ( ApplicationStatus.DISABLED, messages.FIPS_PROC_FILE_ERROR.format( file_name=self.FIPS_PROC_FILE ), ) else: self.cfg.remove_notice("", messages.FIPS_DISABLE_REBOOT_REQUIRED) if super_status != ApplicationStatus.ENABLED: return super_status, super_msg return ( ApplicationStatus.ENABLED, messages.FIPS_REBOOT_REQUIRED, )
def static_affordances(self) -> Tuple[StaticAffordance, ...]: # Use a lambda so we can mock util.is_container in tests from uaclient.entitlements.fips import FIPSEntitlement fips_ent = FIPSEntitlement(self.cfg) is_fips_enabled = bool( fips_ent.application_status()[0] == ApplicationStatus.ENABLED ) return ( ( messages.LIVEPATCH_ERROR_INSTALL_ON_CONTAINER, lambda: util.is_container(), False, ), ( messages.LIVEPATCH_ERROR_WHEN_FIPS_ENABLED, lambda: is_fips_enabled, False, ), )
class LivepatchEntitlement(base.UAEntitlement): name = 'livepatch' title = 'Livepatch' description = ('Canonical Livepatch Service' ' (https://ubuntu.com/livepatch)') # Use a lambda so we can mock util.is_container in tests static_affordances = (('Cannot install Livepatch on a container', lambda: util.is_container(), False), ) 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'): print('Installing snapd...') util.subp(['apt-get', 'install', '--assume-yes', 'snapd'], capture=True) util.subp(['snap', 'wait', 'system', 'seed.loaded'], capture=True) print('Installing canonical-livepatch snap...') try: util.subp(['snap', 'install', 'canonical-livepatch'], capture=True) 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 setup_livepatch_config(self, process_directives: bool = True, process_token: bool = True) -> bool: """Processs configuration setup for livepatch directives. :param process_directives: Boolean set True when directives should be processsed. :param process_token: Boolean set True when token should be processsed. """ entitlement_cfg = self.cfg.entitlements.get(self.name) if process_directives: try: process_config_directives(entitlement_cfg) except util.ProcessExecutionError as e: msg = 'Unable to configure Livepatch: ' + str(e) print(msg) logging.error(msg) return False if process_token: livepatch_token = entitlement_cfg.get('resourceToken') if not livepatch_token: logging.debug( 'No specific resourceToken present. Using machine token as' ' %s credentials', self.title) livepatch_token = self.cfg.machine_token['machineToken'] op_status, _details = self.operational_status() if op_status == status.ACTIVE: logging.info('Disabling %s prior to re-attach with new token', self.title) try: util.subp(['/snap/bin/canonical-livepatch', 'disable']) except util.ProcessExecutionError as e: logging.error(str(e)) return False try: util.subp([ '/snap/bin/canonical-livepatch', 'enable', livepatch_token ], capture=True) except util.ProcessExecutionError as e: msg = 'Unable to enable Livepatch: ' for error_message, print_message in ERROR_MSG_MAP.items(): if error_message in str(e): msg += print_message break if msg == 'Unable to enable Livepatch: ': msg += str(e) print(msg) return False print('Canonical livepatch enabled.') return True def disable(self, silent=False, force=False): """Disable specific entitlement @return: True on success, False otherwise. """ if not self.can_disable(silent, force): return False if not util.which('/snap/bin/canonical-livepatch'): return True util.subp(['/snap/bin/canonical-livepatch', 'disable'], capture=True) logging.debug('Removing canonical-livepatch snap...') if not silent: print('Removing canonical-livepatch snap...') util.subp(['snap', 'remove', 'canonical-livepatch'], capture=True) if not silent: print(status.MESSAGE_DISABLED_TMPL.format(title=self.title)) return True def operational_status(self): """Return entitlement operational status as ACTIVE or INACTIVE.""" passed_affordances, details = self.check_affordances() if not passed_affordances: return status.INAPPLICABLE, details entitlement_cfg = self.cfg.entitlements.get(self.name) if not entitlement_cfg: return status.INAPPLICABLE, '%s is not entitled' % self.title elif entitlement_cfg['entitlement'].get('entitled', False) is False: return status.INAPPLICABLE, '%s is not entitled' % self.title operational_status = (status.ACTIVE, '') try: util.subp(['/snap/bin/canonical-livepatch', 'status']) except util.ProcessExecutionError as e: # TODO(May want to parse INACTIVE/failure assessment) logging.debug('Livepatch not enabled. %s', str(e)) operational_status = (status.INACTIVE, str(e)) return operational_status def process_contract_deltas(self, orig_access: 'Dict[str, Any]', deltas: 'Dict[str, Any]', allow_enable: bool = False) -> bool: """Process any contract access deltas for this entitlement. :param orig_access: Dictionary containing the original resourceEntitlement access details. :param deltas: Dictionary which contains only the changed access keys and values. :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. :return: True when delta operations are processed; False when noop. """ if super().process_contract_deltas(orig_access, deltas, allow_enable): return True # Already processed parent class deltas op_status, _details = self.operational_status() if op_status != status.ACTIVE: return True # only operate on changed directives when ACTIVE delta_entitlement = deltas.get('entitlement', {}) delta_directives = delta_entitlement.get('directives', {}) supported_deltas = set(['caCerts', 'remoteServer']) process_directives = bool( supported_deltas.intersection(delta_directives)) process_token = bool(deltas.get('resourceToken', False)) if any([process_directives, process_token]): logging.info("Updating '%s' on changed directives." % self.name) return self.setup_livepatch_config( process_directives=process_directives, process_token=process_token) return True
class LivepatchEntitlement(base.UAEntitlement): name = "livepatch" title = "Livepatch" description = "Canonical Livepatch Service (https://ubuntu.com/livepatch)" # Use a lambda so we can mock util.is_container in tests static_affordances = (( "Cannot install Livepatch on a container", lambda: util.is_container(), False, ), ) 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) raise exceptions.UserFacingError(msg) return self.setup_livepatch_config(process_directives=True, process_token=True) def setup_livepatch_config(self, process_directives: bool = True, process_token: bool = True) -> bool: """Processs configuration setup for livepatch directives. :param process_directives: Boolean set True when directives should be processsed. :param process_token: Boolean set True when token should be processsed. """ entitlement_cfg = self.cfg.entitlements.get(self.name) if process_directives: try: process_config_directives(entitlement_cfg) except util.ProcessExecutionError as e: msg = "Unable to configure Livepatch: " + str(e) print(msg) logging.error(msg) return False if process_token: livepatch_token = entitlement_cfg.get("resourceToken") if not livepatch_token: logging.debug( "No specific resourceToken present. Using machine token as" " %s credentials", self.title, ) livepatch_token = self.cfg.machine_token["machineToken"] application_status, _details = self.application_status() if application_status != status.ApplicationStatus.DISABLED: logging.info( "Disabling %s prior to re-attach with new token", self.title, ) try: util.subp(["/snap/bin/canonical-livepatch", "disable"]) except util.ProcessExecutionError as e: logging.error(str(e)) return False try: util.subp( [ "/snap/bin/canonical-livepatch", "enable", livepatch_token, ], capture=True, ) except util.ProcessExecutionError as e: msg = "Unable to enable Livepatch: " for error_message, print_message in ERROR_MSG_MAP.items(): if error_message in str(e): msg += print_message break if msg == "Unable to enable Livepatch: ": msg += str(e) print(msg) return False print("Canonical livepatch enabled.") return True def disable(self, silent=False): """Disable specific entitlement @return: True on success, False otherwise. """ if not self.can_disable(silent): return False if not util.which("/snap/bin/canonical-livepatch"): return True util.subp(["/snap/bin/canonical-livepatch", "disable"], capture=True) logging.debug("Removing canonical-livepatch snap") if not silent: print("Removing canonical-livepatch snap") util.subp([SNAP_CMD, "remove", "canonical-livepatch"], capture=True) if not silent: print(status.MESSAGE_DISABLED_TMPL.format(title=self.title)) return True def application_status(self) -> "Tuple[ApplicationStatus, str]": status = (ApplicationStatus.ENABLED, "") try: util.subp(["/snap/bin/canonical-livepatch", "status"]) except util.ProcessExecutionError as e: # TODO(May want to parse INACTIVE/failure assessment) logging.debug("Livepatch not enabled. %s", str(e)) status = (ApplicationStatus.DISABLED, str(e)) return status def process_contract_deltas( self, orig_access: "Dict[str, Any]", deltas: "Dict[str, Any]", allow_enable: bool = False, ) -> bool: """Process any contract access deltas for this entitlement. :param orig_access: Dictionary containing the original resourceEntitlement access details. :param deltas: Dictionary which contains only the changed access keys and values. :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. :return: True when delta operations are processed; False when noop. """ if super().process_contract_deltas(orig_access, deltas, allow_enable): return True # Already processed parent class deltas application_status, _ = self.application_status() if application_status == status.ApplicationStatus.DISABLED: return True # only operate on changed directives when ACTIVE delta_entitlement = deltas.get("entitlement", {}) delta_directives = delta_entitlement.get("directives", {}) supported_deltas = set(["caCerts", "remoteServer"]) process_directives = bool( supported_deltas.intersection(delta_directives)) process_token = bool(deltas.get("resourceToken", False)) if any([process_directives, process_token]): logging.info("Updating '%s' on changed directives.", self.name) return self.setup_livepatch_config( process_directives=process_directives, process_token=process_token, ) return True
def packages(self) -> List[str]: if util.is_container(): return [] packages = super().packages return self._replace_metapackage_on_cloud_instance(packages)
def test_true_systemd_detect_virt_success(self, m_subp): """Return True when systemd-detect virt exits success.""" m_subp.return_value = '', '' assert True is util.is_container() calls = [mock.call(['systemd-detect-virt', '--quiet', '--container'])] assert calls == m_subp.call_args_list
class LivepatchEntitlement(base.UAEntitlement): name = 'livepatch' title = 'Livepatch' description = ('Canonical Livepatch Service' ' (https://www.ubuntu.com/server/livepatch)') # Use a lambda so we can mock util.is_container in tests static_affordances = (('Cannot install Livepatch on a container', lambda: util.is_container(), False), ) def enable(self): """Enable specific entitlement. @return: True on success, False otherwise. """ if not self.can_enable(): return False if not util.which('/snap/bin/canonical-livepatch'): if not util.which('snap'): print('Installing snapd...') util.subp(['apt-get', 'install', '--assume-yes', 'snapd'], capture=True) print('Installing canonical-livepatch snap...') try: util.subp(['snap', 'install', 'canonical-livepatch'], capture=True) except util.ProcessExecutionError as e: msg = 'Unable to install Livepatch client: ' + str(e) print(msg) logging.error(msg) return False entitlement_cfg = self.cfg.entitlements.get(self.name) try: process_directives(entitlement_cfg) except util.ProcessExecutionError as e: msg = 'Unable to configure Livepatch: ' + str(e) print(msg) logging.error(msg) return False livepatch_token = entitlement_cfg.get('resourceToken') if not livepatch_token: logging.debug( 'No specific resourceToken present. Using machine token as' ' %s credentials', self.title) livepatch_token = self.cfg.machine_token['machineToken'] try: util.subp( ['/snap/bin/canonical-livepatch', 'enable', livepatch_token], capture=True) except util.ProcessExecutionError as e: msg = 'Unable to enable Livepatch: ' for error_message, print_message in ERROR_MSG_MAP.items(): if error_message in str(e): msg += print_message break if msg == 'Unable to enable Livepatch: ': msg += str(e) print(msg) return False print('Canonical livepatch enabled.') return True def disable(self, silent=False, force=False): """Disable specific entitlement @return: True on success, False otherwise. """ if not self.can_disable(silent, force): return False if not util.which('/snap/bin/canonical-livepatch'): return True util.subp(['/snap/bin/canonical-livepatch', 'disable'], capture=True) logging.debug('Removing canonical-livepatch snap...') if not silent: print('Removing canonical-livepatch snap...') util.subp(['snap', 'remove', 'canonical-livepatch'], capture=True) if not silent: print(status.MESSAGE_DISABLED_TMPL.format(title=self.title)) return True def operational_status(self): """Return entitlement operational status as ACTIVE or INACTIVE.""" passed_affordances, details = self.check_affordances() if not passed_affordances: return status.INAPPLICABLE, details operational_status = (status.ACTIVE, '') try: util.subp(['/snap/bin/canonical-livepatch', 'status']) except util.ProcessExecutionError as e: # TODO(May want to parse INACTIVE/failure assessment) logging.debug('Livepatch not enabled. %s', str(e)) operational_status = (status.INACTIVE, str(e)) return operational_status