# 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
))