示例#1
0
def get_version(_args=None, features={}):
    """Return the packaged version as a string

    Prefer the binary PACKAGED_VESION set by debian/rules to DEB_VERSION.
    If unavailable, check for a .git development environments:
      a. If run in our upstream repo `git describe` will gives a leading
         XX.Y so return the --long version to allow daily build recipes
         to count commit offset from upstream's XX.Y signed tag.
      b. If run in a git-ubuntu pkg repo, upstream tags aren't visible,
         parse the debian/changelog in that case
    """
    feature_suffix = ""
    for key, value in sorted(features.items()):
        feature_suffix += " {enabled}{name}".format(
            enabled="+" if value else "-", name=key)
    if not PACKAGED_VERSION.startswith("@@PACKAGED_VERSION"):
        return VERSION_TMPL.format(version=PACKAGED_VERSION,
                                   feature_suffix=feature_suffix)
    topdir = os.path.dirname(os.path.dirname(__file__))
    if os.path.exists(os.path.join(topdir, ".git")):
        cmd = ["git", "describe", "--abbrev=8", "--match=[0-9]*", "--long"]
        try:
            out, _ = util.subp(cmd)
            return out.strip() + feature_suffix
        except exceptions.ProcessExecutionError:
            # Rely on debian/changelog because we are in a git-ubuntu or other
            # packaging repo
            cmd = ["dpkg-parsechangelog", "-S", "version"]
            out, _ = util.subp(cmd)
            return VERSION_TMPL.format(version=out.strip(),
                                       feature_suffix=feature_suffix)
    return VERSION_TMPL.format(version=__VERSION__,
                               feature_suffix=feature_suffix)
def configure_livepatch_proxy(
    http_proxy: Optional[str] = None,
    https_proxy: Optional[str] = None,
    retry_sleeps: Optional[List[float]] = None,
) -> None:
    """
    Configure livepatch to use http and https proxies.

    :param http_proxy: http proxy to be used by livepatch. If None, it will
                       not be configured
    :param https_proxy: https proxy to be used by livepatch. If None, it will
                        not be configured
    :@param retry_sleeps: Optional list of sleep lengths to apply between
                               snap calls
    """
    if http_proxy or https_proxy:
        event.info(
            messages.SETTING_SERVICE_PROXY.format(
                service=LivepatchEntitlement.title
            )
        )

    if http_proxy:
        util.subp(
            [LIVEPATCH_CMD, "config", "http-proxy={}".format(http_proxy)],
            retry_sleeps=retry_sleeps,
        )

    if https_proxy:
        util.subp(
            [LIVEPATCH_CMD, "config", "https-proxy={}".format(https_proxy)],
            retry_sleeps=retry_sleeps,
        )
示例#3
0
 def _cleanup(self) -> None:
     """Clean up the entitlement without checks or messaging"""
     self.remove_apt_config()
     try:
         util.subp(["apt-get", "remove", "--assume-yes"] + self.packages)
     except util.ProcessExecutionError:
         pass
def update_apt_and_motd_messages(cfg: config.UAConfig) -> bool:
    """Emit templates and human-readable status messages in msg_dir.

    These structured messages will be sourced by both /etc/update.motd.d
    and APT UA-configured hooks. APT hook content will orginate from
    apt-hook/hook.cc

    Call esm-apt-hook process-templates to render final human-readable
    messages.

    :param cfg: UAConfig instance for this environment.
    """
    logging.debug("Updating UA messages for APT and MOTD.")
    msg_dir = os.path.join(cfg.data_dir, "messages")
    if not os.path.exists(msg_dir):
        os.makedirs(msg_dir)

    series = util.get_platform_info()["series"]
    if not util.is_lts(series):
        # ESM is only on LTS releases. Remove all messages and templates.
        for msg_enum in ExternalMessage:
            msg_path = os.path.join(msg_dir, msg_enum.value)
            util.remove_file(msg_path)
            if msg_path.endswith(".tmpl"):
                util.remove_file(msg_path.replace(".tmpl", ""))
        return True

    # Announce ESM availabilty on active ESM LTS releases
    write_esm_announcement_message(cfg, series)
    write_apt_and_motd_templates(cfg, series)
    # Now that we've setup/cleanedup templates render them with apt-hook
    util.subp(["/usr/lib/ubuntu-advantage/apt-esm-hook", "process-templates"])
    return True
    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'):
            print('Installing canonical-livepatch snap...')
            util.subp(['snap', 'install', 'canonical-livepatch'], capture=True)
        entitlement_cfg = self.cfg.entitlements.get(self.name)
        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 process_config_directives(cfg):
    """Process livepatch configuration directives.

    We process caCerts before remoteServer because changing remote-server
    in the canonical-livepatch CLI performs a PUT against the new server name.
    If new caCerts were required for the new remoteServer, this
    canonical-livepatch client PUT could fail on unmatched old caCerts.

    @raises: ProcessExecutionError if unable to configure livepatch.
    """
    if not cfg:
        return
    directives = cfg.get('entitlement', {}).get('directives', {})
    ca_certs = directives.get('caCerts')
    if ca_certs:
        util.subp([
            '/snap/bin/canonical-livepatch', 'config',
            'ca-certs=%s' % ca_certs
        ],
                  capture=True)
    remote_server = directives.get('remoteServer', '')
    if remote_server.endswith('/'):
        remote_server = remote_server[:-1]
    if remote_server:
        util.subp([
            '/snap/bin/canonical-livepatch', 'config',
            'remote-server=%s' % remote_server
        ],
                  capture=True)
示例#7
0
def version_cmp_le(version1: str, version2: str) -> bool:
    """Return True when version1 is less than or equal to version2."""
    try:
        util.subp(["dpkg", "--compare-versions", version1, "le", version2])
        return True
    except exceptions.ProcessExecutionError:
        return False
示例#8
0
    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 self.setup_apt_config():
            return False
        if self.packages:
            try:
                print(
                    'Installing {title} packages ...'.format(title=self.title))
                util.subp(['apt-get', 'install', '--assume-yes'] +
                          self.packages,
                          capture=True)
            except util.ProcessExecutionError:
                self.disable(silent=True, force=True)
                logging.error(
                    status.MESSAGE_ENABLED_FAILED_TMPL.format(
                        title=self.title))
                return False
        self.cfg.local_enabled_manager.set(self.name, True)
        print(status.MESSAGE_ENABLED_TMPL.format(title=self.title))
        for msg in self.messaging.get('post_enable', []):
            print(msg)
        return True
示例#9
0
def get_version(_args=None):
    """Return the packaged version as a string

         Prefer the binary PACKAGED_VESION set by debian/rules to DEB_VERSION.
         If unavailable, check for a .git development environments:
           a. If run in our upstream repo `git describe` will gives a leading
              XX.Y so return the --long version to allow daily build recipes
              to count commit offset from upstream's XX.Y signed tag.
           b. If run in a git-ubuntu pkg repo, upstream tags aren't visible,
              parse the debian/changelog in that case
    """
    if not PACKAGED_VERSION.startswith("@@PACKAGED_VERSION"):
        return PACKAGED_VERSION
    topdir = os.path.dirname(os.path.dirname(__file__))
    if os.path.exists(os.path.join(topdir, ".git")):
        cmd = ["git", "describe", "--abbrev=8", "--match=[0-9]*", "--long"]
        try:
            out, _ = util.subp(cmd)
            return out.strip()
        except util.ProcessExecutionError:
            # Rely on debian/changelog because we are in a git-ubuntu or other
            # packaging repo
            cmd = ["dpkg-parsechangelog", "-S", "version"]
            out, _ = util.subp(cmd)
            return out.strip()
    return __VERSION__
def process_config_directives(cfg):
    """Process livepatch configuration directives.

    We process caCerts before remoteServer because changing remote-server
    in the canonical-livepatch CLI performs a PUT against the new server name.
    If new caCerts were required for the new remoteServer, this
    canonical-livepatch client PUT could fail on unmatched old caCerts.

    @raises: ProcessExecutionError if unable to configure livepatch.
    """
    if not cfg:
        return
    directives = cfg.get("entitlement", {}).get("directives", {})
    ca_certs = directives.get("caCerts")
    if ca_certs:
        util.subp(
            [
                "/snap/bin/canonical-livepatch",
                "config",
                "ca-certs={}".format(ca_certs),
            ],
            capture=True,
        )
    remote_server = directives.get("remoteServer", "")
    if remote_server.endswith("/"):
        remote_server = remote_server[:-1]
    if remote_server:
        util.subp(
            [
                "/snap/bin/canonical-livepatch",
                "config",
                "remote-server={}".format(remote_server),
            ],
            capture=True,
        )
示例#11
0
    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
        series = util.get_platform_info('series')
        repo_filename = self.repo_list_file_tmpl.format(name=self.name,
                                                        series=series)
        keyring_file = os.path.join(apt.APT_KEYS_DIR, self.repo_key_file)
        entitlement_cfg = self.cfg.read_cache('machine-access-%s' %
                                              self.name)['entitlement']
        access_directives = entitlement_cfg.get('directives', {})
        repo_url = access_directives.get('aptURL', self.repo_url)
        if not repo_url:
            repo_url = self.repo_url
        apt.remove_auth_apt_repo(repo_filename, repo_url, keyring_file)
        apt.remove_apt_list_files(repo_url, series)
        print('Removing packages: %s' % ', '.join(self.packages))
        try:
            util.subp(['apt-get', 'remove', '--assume-yes'] + self.packages)
        except util.ProcessExecutionError:
            pass
        return True
示例#12
0
 def disable(self, silent=False, force=False):
     if not self.can_disable(silent, force):
         return False
     if force:  # Force config cleanup as broke during setup attempt.
         series = util.get_platform_info('series')
         repo_filename = self.repo_list_file_tmpl.format(name=self.name,
                                                         series=series)
         keyring_file = os.path.join(apt.APT_KEYS_DIR, self.repo_key_file)
         entitlement = self.cfg.read_cache(
             'machine-access-%s' % self.name).get('entitlement', {})
         access_directives = entitlement.get('directives', {})
         repo_url = access_directives.get('aptURL', self.repo_url)
         if not repo_url:
             repo_url = self.repo_url
         apt.remove_auth_apt_repo(repo_filename, repo_url, keyring_file)
         if self.repo_pin_priority:
             repo_pref_file = self.repo_pref_file_tmpl.format(
                 name=self.name, series=series)
             if os.path.exists(repo_pref_file):
                 os.unlink(repo_pref_file)
         apt.remove_apt_list_files(repo_url, series)
         try:
             util.subp(['apt-get', 'remove', '--assume-yes'] +
                       self.packages)
         except util.ProcessExecutionError:
             pass
     if not silent:
         print('Warning: no option to disable {title}'.format(
             title=self.title))
     return False
    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
示例#14
0
def remove_auth_apt_repo(repo_filename,
                         repo_url,
                         keyring_file=None,
                         fingerprint=None):
    """Remove an authenticated apt repo and credentials to the system"""
    logging.info('Removing authenticated apt repo: %s', repo_url)
    util.del_file(repo_filename)
    if keyring_file:
        util.del_file(keyring_file)
    elif fingerprint:
        util.subp(['apt-key', 'del', fingerprint], capture=True)
    _protocol, repo_path = repo_url.split('://')
    if repo_path.endswith('/'):  # strip trailing slash
        repo_path = repo_path[:-1]
    apt_auth_file = get_apt_auth_file_from_apt_config()
    if os.path.exists(apt_auth_file):
        apt_auth = util.load_file(apt_auth_file)
        auth_prefix = 'machine {repo_path}/ login'.format(repo_path=repo_path)
        content = '\n'.join([
            line for line in apt_auth.splitlines() if auth_prefix not in line
        ])
        if not content:
            os.unlink(apt_auth_file)
        else:
            util.write_file(apt_auth_file, content, mode=0o600)
示例#15
0
def 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.

    @return: True if valid or unable to validate
    """
    protocol, repo_path = repo_url.split('://')
    if not os.path.exists('/usr/lib/apt/apt-helper'):
        return True  # Do not validate
    try:
        util.subp([
            '/usr/lib/apt/apt-helper', 'download-file',
            '%s://%s:%s@%s/ubuntu/pool/' %
            (protocol, username, password, repo_path), '/tmp/uaclient-apt-test'
        ],
                  capture=False)  # Hide credentials from logs
        return True
    except util.ProcessExecutionError:
        pass
    finally:
        if os.path.exists('/tmp/uaclient-apt-test'):
            os.unlink('/tmp/uaclient-apt-test')
    return False
示例#16
0
def valid_apt_credentials(repo_url, series, credentials):
    """Validate apt credentials for a PPA.

    @param repo_url: private-ppa url path
    @param credentials: PPA credentials string username:password.
    @param series: xenial, bionic ...

    @return: True if valid or unable to validate
    """
    protocol, repo_path = repo_url.split('://')
    if not os.path.exists('/usr/lib/apt/apt-helper'):
        return True   # Do not validate
    try:
        util.subp(['/usr/lib/apt/apt-helper', 'download-file',
                   '%s://%s@%s/ubuntu/dists/%s/Release' % (
                       protocol, credentials, repo_path, series),
                   '/tmp/uaclient-apt-test'],
                  capture=False)  # Hide credentials from logs
        os.unlink('/tmp/uaclient-apt-test')
        return True
    except util.ProcessExecutionError:
        pass
    if os.path.exists('/tmp/uaclient-apt-test'):
        os.unlink('/tmp/uaclient-apt-test')
    return False
示例#17
0
    def test_retry_doesnt_consume_retry_sleeps(self, m_sleep):
        """When retry_sleeps given, use defined sleeps between each retry."""
        sleeps = [1, 3, 0.4]
        expected_sleeps = sleeps.copy()
        with pytest.raises(util.ProcessExecutionError):
            util.subp(["ls", "--bogus"], retry_sleeps=sleeps)

        assert expected_sleeps == sleeps
示例#18
0
    def test_default_do_not_retry_on_failure_return_code(self, m_sleep):
        """When no retry_sleeps are specified, do not retry failures."""
        with pytest.raises(util.ProcessExecutionError) as excinfo:
            util.subp(["ls", "--bogus"])

        expected_error = ("Failed running command 'ls --bogus' [exit(2)]."
                          " Message: ls: unrecognized option")
        assert expected_error in str(excinfo.value)
        assert 0 == m_sleep.call_count  # no retries
    def _perform_disable(self, silent=False):
        """Disable specific entitlement

        @return: True on success, False otherwise.
        """
        if not util.which(LIVEPATCH_CMD):
            return True
        util.subp([LIVEPATCH_CMD, "disable"], capture=True)
        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
示例#21
0
    def test_retry_with_specified_sleeps_on_error(self, m_sleep):
        """When retry_sleeps given, use defined sleeps between each retry."""
        with pytest.raises(util.ProcessExecutionError) as excinfo:
            util.subp(["ls", "--bogus"], retry_sleeps=[1, 3, 0.4])

        expected_error = "Failed running command 'ls --bogus' [exit(2)]"
        assert expected_error in str(excinfo.value)
        expected_sleeps = [mock.call(1), mock.call(3), mock.call(0.4)]
        assert expected_sleeps == m_sleep.call_args_list
def get_apt_auth_file_from_apt_config():
    """Return to patch to the system configured APT auth file."""
    out, _err = util.subp(
        ["apt-config", "shell", "key", APT_CONFIG_AUTH_PARTS_DIR])
    if out:  # then auth.conf.d parts is present
        return out.split("'")[1] + "90ubuntu-advantage"
    else:  # then use configured /etc/apt/auth.conf
        out, _err = util.subp(
            ["apt-config", "shell", "key", APT_CONFIG_AUTH_FILE])
        return out.split("'")[1].rstrip("/")
示例#23
0
def process_contract_delta_after_apt_lock() -> None:
    logging.debug("Check whether to upgrade-lts-contract")
    cfg = UAConfig()
    if not cfg.is_attached:
        logging.debug("Skipping upgrade-lts-contract. Machine is unattached")
        return
    out, _err = subp(["lsof", "/var/lib/apt/lists/lock"], rcs=[0, 1])
    msg = "Starting upgrade-lts-contract."
    if out:
        msg += " Retrying every 10 seconds waiting on released apt lock"
    print(msg)
    logging.debug(msg)

    current_version = parse_os_release()["VERSION_ID"]
    current_release = version_to_codename.get(current_version)

    if current_release is None:
        msg = "Unable to get release codename for version: {}".format(
            current_version)
        print(msg)
        logging.warning(msg)
        sys.exit(1)

    past_release = current_codename_to_past_codename.get(current_release)
    if past_release is None:
        msg = "Could not find past release for: {}".format(current_release)
        print(msg)
        logging.warning(msg)
        sys.exit(1)

    past_entitlements = UAConfig(series=past_release).entitlements
    new_entitlements = UAConfig(series=current_release).entitlements

    retry_count = 0
    while out:
        # Loop until apt hold is released at the end of `do-release-upgrade`
        time.sleep(10)
        out, _err = subp(["lsof", "/var/lib/apt/lists/lock"], rcs=[0, 1])
        retry_count += 1

    msg = "upgrade-lts-contract processing contract deltas: {} -> {}".format(
        past_release, current_release)
    print(msg)
    logging.debug(msg)

    process_entitlements_delta(
        cfg=cfg,
        past_entitlements=past_entitlements,
        new_entitlements=new_entitlements,
        allow_enable=True,
        series_overrides=False,
    )
    msg = "upgrade-lts-contract succeeded after {} retries".format(retry_count)
    print(msg)
    logging.debug(msg)
示例#24
0
    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)
        return 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
示例#26
0
def action_disable(args, cfg):
    """Perform the disable action on a named entitlement.

    @return: 0 on success, 1 otherwise
    """
    ent_cls = entitlements.ENTITLEMENT_CLASS_BY_NAME[args.name]
    entitlement = ent_cls(cfg)
    if entitlement.disable():
        if hasattr(entitlement, 'repo_url'):
            util.subp(['apt-get', 'update'], capture=True)
        return 0
    else:
        return 1
 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
def refresh_motd():
    # If update-notifier is present, we might as well update
    # the package updates count related to MOTD
    if exists(UPDATE_NOTIFIER_MOTD_SCRIPT):
        # If this command fails, we shouldn't break the entire command,
        # since this command should already be triggered by
        # update-notifier apt hooks
        try:
            util.subp([UPDATE_NOTIFIER_MOTD_SCRIPT, "--force"])
        except Exception as exc:
            logging.exception(exc)

    util.subp(["sudo", "systemctl", "restart", "motd-news.service"])
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 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)
        return True