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,
                    },
                )
            ],
        }
Esempio n. 3
0
    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,
            ),
        )
Esempio n. 4
0
    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
Esempio n. 5
0
    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
Esempio n. 6
0
 def static_affordances(self) -> Tuple[StaticAffordance, ...]:
     return (
         (
             messages.REALTIME_ERROR_INSTALL_ON_CONTAINER,
             lambda: util.is_container(),
             False,
         ),
     )
Esempio n. 7
0
    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)
Esempio n. 14
0
 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