2009-01-19 17:55:00 +00:00
|
|
|
#
|
2009-10-25 13:57:39 +01:00
|
|
|
# misc.py - misc functions for the Ubuntu Developer Tools scripts.
|
2009-01-19 17:55:00 +00:00
|
|
|
#
|
2010-12-24 12:00:03 +02:00
|
|
|
# Copyright (C) 2008, Jonathan Davies <jpds@ubuntu.com>,
|
|
|
|
# 2008-2009, Siegfried-Angel Gevatter Pujals <rainct@ubuntu.com>,
|
|
|
|
# 2010, Stefano Rivera <stefanor@ubuntu.com>
|
2011-06-11 05:05:35 -07:00
|
|
|
# 2011, Evan Broder <evan@ebroder.net>
|
2009-01-19 17:55:00 +00:00
|
|
|
#
|
2009-10-25 13:57:39 +01:00
|
|
|
# ##################################################################
|
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or
|
|
|
|
# modify it under the terms of the GNU General Public License
|
|
|
|
# as published by the Free Software Foundation; either version 3
|
|
|
|
# of the License, or (at your option) any later version.
|
2010-12-02 09:33:54 +02:00
|
|
|
#
|
2009-10-25 13:57:39 +01:00
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
2009-01-19 17:55:00 +00:00
|
|
|
#
|
2009-10-25 13:57:39 +01:00
|
|
|
# See file /usr/share/common-licenses/GPL for more details.
|
2009-01-19 17:55:00 +00:00
|
|
|
#
|
2009-10-25 13:57:39 +01:00
|
|
|
# ##################################################################
|
2009-01-19 17:55:00 +00:00
|
|
|
|
2020-03-04 08:25:22 +01:00
|
|
|
import distro_info
|
2020-01-23 08:48:21 -05:00
|
|
|
import hashlib
|
2011-02-13 16:15:24 +02:00
|
|
|
import locale
|
2009-01-19 17:55:00 +00:00
|
|
|
import os
|
2020-03-04 08:25:22 +01:00
|
|
|
import shutil
|
2011-02-13 16:15:24 +02:00
|
|
|
import sys
|
2020-07-20 17:43:17 -04:00
|
|
|
import tempfile
|
2009-01-19 17:55:00 +00:00
|
|
|
|
2020-03-25 15:47:33 -04:00
|
|
|
from contextlib import suppress
|
2020-03-04 08:25:22 +01:00
|
|
|
from subprocess import check_output, CalledProcessError
|
|
|
|
from urllib.parse import urlparse
|
|
|
|
from urllib.request import urlopen
|
2011-06-25 17:53:44 +02:00
|
|
|
|
2010-03-20 19:45:04 +01:00
|
|
|
from ubuntutools.lp.udtexceptions import PocketDoesNotExistError
|
|
|
|
|
2020-01-23 08:48:21 -05:00
|
|
|
import logging
|
|
|
|
Logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2019-09-11 16:24:49 -04:00
|
|
|
DEFAULT_POCKETS = ('Release', 'Security', 'Updates', 'Proposed')
|
|
|
|
POCKETS = DEFAULT_POCKETS + ('Backports',)
|
|
|
|
|
2019-11-06 17:07:42 -05:00
|
|
|
DEFAULT_STATUSES = ('Pending', 'Published')
|
|
|
|
STATUSES = DEFAULT_STATUSES + ('Superseded', 'Deleted', 'Obsolete')
|
|
|
|
|
2020-03-04 08:29:02 +01:00
|
|
|
UPLOAD_QUEUE_STATUSES = ('New', 'Unapproved', 'Accepted', 'Done', 'Rejected')
|
|
|
|
|
2011-06-11 05:05:35 -07:00
|
|
|
_system_distribution_chain = []
|
2017-05-01 00:20:03 +02:00
|
|
|
|
|
|
|
|
2011-06-11 05:05:35 -07:00
|
|
|
def system_distribution_chain():
|
|
|
|
""" system_distribution_chain() -> [string]
|
|
|
|
|
|
|
|
Detect the system's distribution as well as all of its parent
|
|
|
|
distributions and return them as a list of strings, with the
|
|
|
|
system distribution first (and the greatest grandparent last). If
|
|
|
|
the distribution chain can't be determined, print an error message
|
|
|
|
and return an empty list.
|
|
|
|
"""
|
|
|
|
global _system_distribution_chain
|
|
|
|
if len(_system_distribution_chain) == 0:
|
|
|
|
try:
|
2019-09-11 13:38:04 -03:00
|
|
|
vendor = check_output(('dpkg-vendor', '--query', 'Vendor'),
|
2019-09-26 11:05:48 +02:00
|
|
|
encoding='utf-8').strip()
|
2019-09-11 13:38:04 -03:00
|
|
|
_system_distribution_chain.append(vendor)
|
|
|
|
except CalledProcessError:
|
2020-03-04 08:27:11 +01:00
|
|
|
Logger.error('Could not determine what distribution you are running.')
|
2011-06-11 05:05:35 -07:00
|
|
|
return []
|
|
|
|
|
|
|
|
while True:
|
|
|
|
try:
|
2019-09-11 13:38:04 -03:00
|
|
|
parent = check_output((
|
|
|
|
'dpkg-vendor', '--vendor', _system_distribution_chain[-1],
|
|
|
|
'--query', 'Parent'), encoding='utf-8').strip()
|
|
|
|
except CalledProcessError:
|
|
|
|
# Vendor has no parent
|
|
|
|
break
|
|
|
|
_system_distribution_chain.append(parent)
|
2011-06-11 05:05:35 -07:00
|
|
|
|
|
|
|
return _system_distribution_chain
|
|
|
|
|
2017-05-01 00:20:03 +02:00
|
|
|
|
2009-10-25 13:57:39 +01:00
|
|
|
def system_distribution():
|
|
|
|
""" system_distro() -> string
|
2010-12-02 09:33:54 +02:00
|
|
|
|
2009-10-25 13:57:39 +01:00
|
|
|
Detect the system's distribution and return it as a string. If the
|
|
|
|
name of the distribution can't be determined, print an error message
|
|
|
|
and return None.
|
|
|
|
"""
|
2011-06-11 05:05:35 -07:00
|
|
|
return system_distribution_chain()[0]
|
2009-10-25 13:57:39 +01:00
|
|
|
|
2017-05-01 00:20:03 +02:00
|
|
|
|
2009-10-25 13:57:39 +01:00
|
|
|
def host_architecture():
|
|
|
|
""" host_architecture -> string
|
2010-12-02 09:33:54 +02:00
|
|
|
|
2010-09-20 18:12:39 +02:00
|
|
|
Detect the host's architecture and return it as a string. If the
|
|
|
|
architecture can't be determined, print an error message and return None.
|
2009-10-25 13:57:39 +01:00
|
|
|
"""
|
2010-12-02 09:33:54 +02:00
|
|
|
|
2019-09-11 13:38:04 -03:00
|
|
|
try:
|
|
|
|
arch = check_output(('dpkg', '--print-architecture'),
|
|
|
|
encoding='utf-8').strip()
|
|
|
|
except CalledProcessError:
|
|
|
|
arch = None
|
2010-12-02 09:33:54 +02:00
|
|
|
|
2019-09-11 13:38:04 -03:00
|
|
|
if not arch or 'not found' in arch:
|
2020-03-04 08:27:11 +01:00
|
|
|
Logger.error('Not running on a Debian based system; '
|
|
|
|
'could not detect its architecture.')
|
2009-10-25 13:57:39 +01:00
|
|
|
return None
|
2010-12-02 09:33:54 +02:00
|
|
|
|
2019-09-11 13:38:04 -03:00
|
|
|
return arch
|
2009-10-25 13:57:39 +01:00
|
|
|
|
2017-05-01 00:20:03 +02:00
|
|
|
|
2009-01-19 17:55:00 +00:00
|
|
|
def readlist(filename, uniq=True):
|
2009-10-25 13:57:39 +01:00
|
|
|
""" readlist(filename, uniq) -> list
|
2010-12-02 09:33:54 +02:00
|
|
|
|
2009-10-25 13:57:39 +01:00
|
|
|
Read a list of words from the indicated file. If 'uniq' is True, filter
|
|
|
|
out duplicated words.
|
|
|
|
"""
|
2010-12-02 09:33:54 +02:00
|
|
|
|
2009-01-19 17:55:00 +00:00
|
|
|
if not os.path.isfile(filename):
|
2020-03-04 08:27:11 +01:00
|
|
|
Logger.error('File "%s" does not exist.' % filename)
|
2009-01-19 17:55:00 +00:00
|
|
|
return False
|
2010-12-02 09:33:54 +02:00
|
|
|
|
2019-09-11 13:38:21 -03:00
|
|
|
with open(filename) as f:
|
|
|
|
content = f.read().replace('\n', ' ').replace(',', ' ')
|
2010-12-02 09:33:54 +02:00
|
|
|
|
2009-01-19 17:55:00 +00:00
|
|
|
if not content.strip():
|
2020-03-04 08:27:11 +01:00
|
|
|
Logger.error('File "%s" is empty.' % filename)
|
2009-01-19 17:55:00 +00:00
|
|
|
return False
|
2010-12-02 09:33:54 +02:00
|
|
|
|
2009-01-19 17:55:00 +00:00
|
|
|
items = [item for item in content.split() if item]
|
2010-12-02 09:33:54 +02:00
|
|
|
|
2009-01-19 17:55:00 +00:00
|
|
|
if uniq:
|
|
|
|
items = list(set(items))
|
2010-12-02 09:33:54 +02:00
|
|
|
|
2009-01-19 17:55:00 +00:00
|
|
|
return items
|
2010-03-20 19:45:04 +01:00
|
|
|
|
2017-05-01 00:20:03 +02:00
|
|
|
|
2011-11-23 01:45:49 +02:00
|
|
|
def split_release_pocket(release, default='Release'):
|
2010-03-20 19:45:04 +01:00
|
|
|
'''Splits the release and pocket name.
|
|
|
|
|
|
|
|
If the argument doesn't contain a pocket name then the 'Release' pocket
|
|
|
|
is assumed.
|
|
|
|
|
|
|
|
Returns the release and pocket name.
|
|
|
|
'''
|
2011-11-23 01:45:49 +02:00
|
|
|
pocket = default
|
2010-03-20 19:45:04 +01:00
|
|
|
|
|
|
|
if release is None:
|
|
|
|
raise ValueError('No release name specified')
|
|
|
|
|
|
|
|
if '-' in release:
|
2011-09-08 16:40:29 +02:00
|
|
|
(release, pocket) = release.rsplit('-', 1)
|
2010-03-20 19:45:04 +01:00
|
|
|
pocket = pocket.capitalize()
|
|
|
|
|
2019-09-11 16:24:49 -04:00
|
|
|
if pocket not in POCKETS:
|
2017-05-01 00:20:03 +02:00
|
|
|
raise PocketDoesNotExistError("Pocket '%s' does not exist." % pocket)
|
2010-03-20 19:45:04 +01:00
|
|
|
|
|
|
|
return (release, pocket)
|
2011-02-13 16:15:24 +02:00
|
|
|
|
2017-05-01 00:20:03 +02:00
|
|
|
|
2011-02-13 16:15:24 +02:00
|
|
|
def require_utf8():
|
|
|
|
'''Can be called by programs that only function in UTF-8 locales'''
|
|
|
|
if locale.getpreferredencoding() != 'UTF-8':
|
2020-03-04 08:27:11 +01:00
|
|
|
Logger.error("This program only functions in a UTF-8 locale. Aborting.")
|
2011-02-13 16:15:24 +02:00
|
|
|
sys.exit(1)
|
2011-06-11 06:04:09 -07:00
|
|
|
|
|
|
|
|
|
|
|
_vendor_to_distroinfo = {"Debian": distro_info.DebianDistroInfo,
|
|
|
|
"Ubuntu": distro_info.UbuntuDistroInfo}
|
2017-05-01 00:20:03 +02:00
|
|
|
|
|
|
|
|
2011-06-11 06:04:09 -07:00
|
|
|
def vendor_to_distroinfo(vendor):
|
|
|
|
""" vendor_to_distroinfo(string) -> DistroInfo class
|
|
|
|
|
|
|
|
Convert a string name of a distribution into a DistroInfo subclass
|
|
|
|
representing that distribution, or None if the distribution is
|
|
|
|
unknown.
|
|
|
|
"""
|
|
|
|
return _vendor_to_distroinfo.get(vendor)
|
|
|
|
|
2017-05-01 00:20:03 +02:00
|
|
|
|
2011-06-11 06:04:09 -07:00
|
|
|
def codename_to_distribution(codename):
|
|
|
|
""" codename_to_distribution(string) -> string
|
|
|
|
|
|
|
|
Finds a given release codename in your distribution's genaology
|
|
|
|
(i.e. looking at the current distribution and its parents), or
|
|
|
|
print an error message and return None if it can't be found
|
|
|
|
"""
|
2011-09-08 18:21:57 +02:00
|
|
|
for distro in system_distribution_chain() + ["Ubuntu", "Debian"]:
|
2011-06-11 06:04:09 -07:00
|
|
|
info = vendor_to_distroinfo(distro)
|
|
|
|
if not info:
|
|
|
|
continue
|
|
|
|
|
2011-06-15 00:01:49 +02:00
|
|
|
if info().valid(codename):
|
2011-06-11 06:04:09 -07:00
|
|
|
return distro
|
2020-01-23 08:48:21 -05:00
|
|
|
|
|
|
|
|
|
|
|
def verify_file_checksum(pathname, alg, checksum, size=0):
|
|
|
|
if not os.path.isfile(pathname):
|
|
|
|
Logger.error('File not found: %s', pathname)
|
|
|
|
return False
|
|
|
|
filename = os.path.basename(pathname)
|
|
|
|
if size and size != os.path.getsize(pathname):
|
|
|
|
Logger.error('File %s incorrect size, got %s expected %s',
|
|
|
|
filename, os.path.getsize(pathname), size)
|
|
|
|
return False
|
|
|
|
h = hashlib.new(alg)
|
|
|
|
with open(pathname, 'rb') as f:
|
|
|
|
while True:
|
|
|
|
block = f.read(h.block_size)
|
|
|
|
if len(block) == 0:
|
|
|
|
break
|
|
|
|
h.update(block)
|
|
|
|
match = h.hexdigest() == checksum
|
|
|
|
if match:
|
|
|
|
Logger.debug('File %s checksum (%s) verified: %s',
|
|
|
|
filename, alg, checksum)
|
|
|
|
else:
|
|
|
|
Logger.error('File %s checksum (%s) mismatch: got %s expected %s',
|
|
|
|
filename, alg, h.hexdigest(), checksum)
|
|
|
|
return match
|
2020-03-04 08:25:22 +01:00
|
|
|
|
|
|
|
|
2020-03-25 15:47:33 -04:00
|
|
|
def download(src, dst, size=0):
|
2020-07-20 17:43:17 -04:00
|
|
|
""" download/copy a file/url to local file
|
|
|
|
|
|
|
|
src: str
|
|
|
|
Source to copy from (file path or url)
|
|
|
|
dst: str
|
|
|
|
Destination dir or filename
|
|
|
|
size: int
|
|
|
|
Size of source, if known
|
|
|
|
|
|
|
|
This calls urllib.request.urlopen() so it may raise the same
|
|
|
|
exceptions as that method (URLError or HTTPError)
|
|
|
|
"""
|
2020-03-25 16:34:45 -04:00
|
|
|
if not urlparse(src).scheme:
|
|
|
|
src = 'file://%s' % os.path.abspath(os.path.expanduser(src))
|
|
|
|
dst = os.path.abspath(os.path.expanduser(dst))
|
|
|
|
|
2020-03-04 08:25:22 +01:00
|
|
|
filename = os.path.basename(urlparse(src).path)
|
|
|
|
|
2020-03-25 15:47:33 -04:00
|
|
|
if os.path.isdir(dst):
|
|
|
|
dst = os.path.join(dst, filename)
|
2020-03-04 08:25:22 +01:00
|
|
|
|
2020-03-25 16:34:45 -04:00
|
|
|
if urlparse(src).scheme == 'file':
|
|
|
|
srcfile = urlparse(src).path
|
|
|
|
if os.path.exists(srcfile) and os.path.exists(dst):
|
|
|
|
if os.path.samefile(srcfile, dst):
|
|
|
|
Logger.info(f"Using existing file {dst}")
|
|
|
|
return
|
|
|
|
|
2020-03-04 08:25:22 +01:00
|
|
|
with urlopen(src) as fsrc, open(dst, 'wb') as fdst:
|
|
|
|
url = fsrc.geturl()
|
|
|
|
Logger.debug(f"Using URL: {url}")
|
|
|
|
|
|
|
|
if not size:
|
2020-03-25 15:47:33 -04:00
|
|
|
with suppress(AttributeError, TypeError, ValueError):
|
2020-03-04 08:25:22 +01:00
|
|
|
size = int(fsrc.info().get('Content-Length'))
|
|
|
|
|
|
|
|
hostname = urlparse(url).hostname
|
|
|
|
sizemb = ' (%0.3f MiB)' % (size / 1024.0 / 1024) if size else ''
|
|
|
|
Logger.info(f'Downloading {filename} from {hostname}{sizemb}')
|
|
|
|
|
|
|
|
if not all((Logger.isEnabledFor(logging.INFO),
|
|
|
|
sys.stderr.isatty(), size)):
|
|
|
|
shutil.copyfileobj(fsrc, fdst)
|
|
|
|
return
|
|
|
|
|
|
|
|
blocksize = 4096
|
|
|
|
XTRALEN = len('[] 99%')
|
|
|
|
downloaded = 0
|
|
|
|
bar_width = 60
|
|
|
|
term_width = os.get_terminal_size(sys.stderr.fileno())[0]
|
|
|
|
if term_width < bar_width + XTRALEN + 1:
|
|
|
|
bar_width = term_width - XTRALEN - 1
|
|
|
|
|
|
|
|
try:
|
|
|
|
while True:
|
|
|
|
block = fsrc.read(blocksize)
|
|
|
|
if not block:
|
|
|
|
break
|
|
|
|
fdst.write(block)
|
|
|
|
downloaded += len(block)
|
|
|
|
pct = float(downloaded) / size
|
|
|
|
bar = ('=' * int(pct * bar_width))[:-1] + '>'
|
|
|
|
fmt = '\r[{bar:<%d}]{pct:>3}%%\r' % bar_width
|
|
|
|
sys.stderr.write(fmt.format(bar=bar, pct=int(pct * 100)))
|
|
|
|
sys.stderr.flush()
|
|
|
|
finally:
|
|
|
|
sys.stderr.write('\r' + ' ' * (term_width - 1) + '\r')
|
|
|
|
if downloaded < size:
|
|
|
|
Logger.error('Partial download: %0.3f MiB of %0.3f MiB' %
|
|
|
|
(downloaded / 1024.0 / 1024,
|
|
|
|
size / 1024.0 / 1024))
|
2020-07-20 17:43:17 -04:00
|
|
|
|
|
|
|
|
|
|
|
def download_text(src):
|
|
|
|
""" return the text content of a downloaded file
|
|
|
|
|
|
|
|
src: str
|
|
|
|
Source to copy from (file path or url)
|
|
|
|
|
|
|
|
Raises the same exceptions as download()
|
|
|
|
|
|
|
|
Returns text content of downloaded file
|
|
|
|
"""
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
dst = os.path.join(d, 'dst')
|
|
|
|
download(src, dst)
|
|
|
|
with open(dst) as f:
|
|
|
|
return f.read()
|