Source code for apt_smart.cli

# Automated, robust apt-get mirror selection for Debian and Ubuntu.
#
# Author: martin68 and Peter Odding
# Last Change: September 15, 2019
# URL: https://apt-smart.readthedocs.io

"""
Usage: apt-smart [OPTIONS]

The apt-smart program automates robust apt-get mirror selection for
Debian and Ubuntu by enabling discovery of available mirrors, ranking of
available mirrors, automatic switching between mirrors and robust package list
updating.

Supported options:

  -r, --remote-host=SSH_ALIAS

    Operate on a remote system instead of the local system. The SSH_ALIAS
    argument gives the SSH alias of the remote host. It is assumed that the
    remote account has root privileges or password-less sudo access.

  -f, --find-current-mirror

    Determine the main mirror that is currently configured in
    /etc/apt/sources.list and report its URL on standard output.

  -F, --file-to-read=local_file_absolute_path

    Read a local absolute path (path and filename must NOT contain whitespace) file
    containing custom mirror URLs (one URL per line) to add custom mirrors to rank.

  -b, --find-best-mirror

    Discover available mirrors, rank them, select the best one and report its
    URL on standard output.

  -l, --list-mirrors

    List available (ranked) mirrors on the terminal in a human readable format.

  -L, --url-char-len=int

    An integer to specify the length of chars in mirrors' URL to display when
    using --list-mirrors, default is 34

  -c, --change-mirror=MIRROR_URL

    Update /etc/apt/sources.list to use the given MIRROR_URL.

  -a, --auto-change-mirror

    Discover available mirrors, rank the mirrors by connection speed and update
    status and update /etc/apt/sources.list to use the best available mirror.

  -u, --update, --update-package-lists

    Update the package lists using `apt-get update', retrying on failure and
    automatically switch to a different mirror when it looks like the current
    mirror is being updated.

  -U, --ubuntu

    Ubuntu mode for Linux Mint to deal with upstream Ubuntu mirror instead of Linux Mint mirror.
    e.g. --auto-change-mirror --ubuntu will auto-change Linux Mint's upstream Ubuntu mirror

  -x, --exclude=PATTERN

    Add a pattern to the mirror selection blacklist. PATTERN is expected to be
    a shell pattern (containing wild cards like `?' and `*') that is matched
    against the full URL of each mirror.

  -v, --verbose

    Increase logging verbosity (can be repeated).

  -V, --version

    Show version number and Python version.

  -R, --create-chroot=local_dir_absolute_path

    Create chroot with the best mirror in a local directory with absolute_path

  -C, --codename=codename

    Must use with -R , create chroot with a codename of Ubuntu or Debian, e.g. bionic, buster

  -q, --quiet

    Decrease logging verbosity (can be repeated).

  -h, --help

    Show this message and exit.

  Note: since apt-smart uses `urlopen` method in The Python Standard Library,
        you can set Environment Variables to make apt-smart connect via HTTP proxy, e.g. in terminal type:
        export {http,https,ftp}_proxy='http://user:password@myproxy.com:1080'
        These will not persist however (no longer active after you close the terminal),
        so you may wish to add the line to your ~/.bashrc
"""

# Standard library modules.
import functools
import getopt
import logging
import sys
import os

# External dependencies.
import coloredlogs
from executor.contexts import LocalContext, RemoteContext
from humanfriendly import format_size, format_table, format_timespan
from humanfriendly.terminal import connected_to_terminal, output, usage, warning

# Modules included in our package.
from apt_smart import MAX_MIRRORS, URL_CHAR_LEN, AptMirrorUpdater
from apt_smart import __version__ as updater_version

# Initialize a logger for this module.
logger = logging.getLogger(__name__)


[docs]def main(): """Command line interface for the ``apt-smart`` program.""" # Initialize logging to the terminal and system log. coloredlogs.install(syslog=True) # Command line option defaults. context = LocalContext() updater = AptMirrorUpdater(context=context) limit = MAX_MIRRORS url_char_len = URL_CHAR_LEN ubuntu_mode = False chroot_path = '' codename = '' actions = [] # Parse the command line arguments. try: options, arguments = getopt.getopt(sys.argv[1:], 'r:fF:blL:c:auUx:m:vVR:C:qh', [ 'remote-host=', 'find-current-mirror', 'find-best-mirror', 'file-to-read=', 'list-mirrors', 'url-char-len=', 'change-mirror=', 'auto-change-mirror', 'update', 'update-package-lists', 'ubuntu', 'exclude=', 'max=', 'verbose', 'version', 'create-chroot=', 'codename=', 'quiet', 'help', ]) for option, value in options: if option in ('-r', '--remote-host'): if actions: msg = "The %s option should be the first option given on the command line!" raise Exception(msg % option) context = RemoteContext(value) updater = AptMirrorUpdater(context=context) elif option in ('-f', '--find-current-mirror'): actions.append(functools.partial(report_current_mirror, updater)) elif option in ('-F', '--file-to-read'): updater.custom_mirror_file_path = value elif option in ('-b', '--find-best-mirror'): actions.append(functools.partial(report_best_mirror, updater)) elif option in ('-l', '--list-mirrors'): actions.append(functools.partial(report_available_mirrors, updater)) elif option in ('-L', '--url-char-len'): url_char_len = int(value) elif option in ('-c', '--change-mirror'): if value.strip().startswith(('http://', 'https://', 'ftp://', 'mirror://')): actions.append(functools.partial(updater.change_mirror, value)) else: raise Exception("\'%s\' is not a valid mirror URL" % value) elif option in ('-a', '--auto-change-mirror'): actions.append(updater.change_mirror) elif option in ('-u', '--update', '--update-package-lists'): actions.append(updater.smart_update) elif option in ('-U', '--ubuntu'): ubuntu_mode = True elif option in ('-x', '--exclude'): actions.insert(0, functools.partial(updater.ignore_mirror, value)) elif option in ('-m', '--max'): limit = int(value) elif option in ('-v', '--verbose'): coloredlogs.increase_verbosity() elif option in ('-V', '--version'): output("Version: %s on Python %i.%i", updater_version, sys.version_info[0], sys.version_info[1]) return elif option in ('-C', '--codename'): codename = value elif option in ('-R', '--create-chroot'): chroot_path = value elif option in ('-q', '--quiet'): coloredlogs.decrease_verbosity() elif option in ('-h', '--help'): usage(__doc__) return else: assert False, "Unhandled option!" if codename and not chroot_path: assert False, "--codename must be used with valid -R to specify chroot path" if chroot_path: actions.append(functools.partial(updater.create_chroot, chroot_path, codename=codename)) if not actions: usage(__doc__) return # Propagate options to the Python API. updater.max_mirrors = limit updater.url_char_len = url_char_len updater.ubuntu_mode = ubuntu_mode except Exception as e: warning("Error: Failed to parse command line arguments! (%s)" % e) sys.exit(1) # Perform the requested action(s). try: for callback in actions: callback() except Exception: logger.exception("Encountered unexpected exception! Aborting ..") sys.exit(1)
[docs]def report_current_mirror(updater): """Print the URL of the currently configured ``apt-get`` mirror.""" output(updater.current_mirror)
[docs]def report_best_mirror(updater): """Print the URL of the "best" mirror.""" output(updater.best_mirror)
[docs]def report_available_mirrors(updater): """Print the available mirrors to the terminal (in a human friendly format).""" if connected_to_terminal() or os.getenv('TRAVIS') == 'true': # make Travis CI test this code # https://docs.travis-ci.com/user/environment-variables/#default-environment-variables have_bandwidth = any(c.bandwidth for c in updater.ranked_mirrors) have_last_updated = any(c.last_updated is not None for c in updater.ranked_mirrors) column_names = ["Rank", "Mirror URL", "Available?", "Updating?"] if have_last_updated: column_names.append("Last updated") if have_bandwidth: column_names.append("Bandwidth") data = [] long_mirror_urls = {} if os.getenv('TRAVIS') == 'true' and updater.url_char_len < 50: updater.url_char_len = 50 for i, candidate in enumerate(updater.ranked_mirrors, start=1): if len(candidate.mirror_url) <= updater.url_char_len: stripped_mirror_url = candidate.mirror_url else: # the mirror_url is too long, strip it stripped_mirror_url = candidate.mirror_url[:updater.url_char_len - 3] stripped_mirror_url = stripped_mirror_url + "..." long_mirror_urls[i] = candidate.mirror_url # store it, output as full afterwards row = [i, stripped_mirror_url, "Yes" if candidate.is_available else "No", "Yes" if candidate.is_updating else "No"] if have_last_updated: row.append("Up to date" if candidate.last_updated == 0 else ( "%s behind" % format_timespan(candidate.last_updated, max_units=1) if candidate.last_updated else "Unknown" )) if have_bandwidth: row.append("%s/s" % format_size(round(candidate.bandwidth, 0)) if candidate.bandwidth else "Unknown") data.append(row) output(format_table(data, column_names=column_names)) if long_mirror_urls: output(u"Full URLs which are too long to be shown in above table:") for key in long_mirror_urls: output(u"%i: %s", key, long_mirror_urls[key]) else: output(u"\n".join( candidate.mirror_url for candidate in updater.ranked_mirrors if candidate.is_available and not candidate.is_updating ))