Initial commit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
.*~
|
||||||
|
.*.sw[op]
|
||||||
28
README.md
Normal file
28
README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# pkgsync
|
||||||
|
|
||||||
|
pkgsync synchronizes packages between two servers.
|
||||||
|
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
On the reference server:
|
||||||
|
```shell
|
||||||
|
pkgsync export packages.json
|
||||||
|
```
|
||||||
|
|
||||||
|
On the outdated server:
|
||||||
|
```shell
|
||||||
|
# yum
|
||||||
|
pkgsync import packages.json | xargs yum --assumeyes install
|
||||||
|
|
||||||
|
# apt
|
||||||
|
pkgsync import packages.json | xargs apt -y install
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Caveats
|
||||||
|
|
||||||
|
- Servers must be the same Linux distribution
|
||||||
|
- Servers must be the same major release
|
||||||
|
- Repositories must be the same between servers
|
||||||
|
|
||||||
558
pkgsync
Normal file
558
pkgsync
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# pkgsync: Help synchronize packages between systems
|
||||||
|
|
||||||
|
from __future__ import print_function, with_statement
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
|
RH_FAMILY = ('centos', 'ol', 'rhel', 'rockylinux')
|
||||||
|
DEB_FAMILY = ('debian', 'ubuntu')
|
||||||
|
|
||||||
|
DIST_FAMILIES = RH_FAMILY + DEB_FAMILY
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryFileNotFoundError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def detect_distro():
|
||||||
|
"""Get the Linux distribution"""
|
||||||
|
distro = None
|
||||||
|
|
||||||
|
if os.path.exists('/etc/oracle-release'):
|
||||||
|
distro = 'ol'
|
||||||
|
elif os.path.exists('/etc/centos-release'):
|
||||||
|
distro = 'centos'
|
||||||
|
elif os.path.exists('/etc/redhat-release'):
|
||||||
|
distro = 'rhel'
|
||||||
|
|
||||||
|
elif os.path.exists('/etc/os-release'):
|
||||||
|
with open('/etc/os-release') as fi:
|
||||||
|
lines = fi.readlines()
|
||||||
|
for line in lines:
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
if key == 'ID':
|
||||||
|
distro = value.strip()
|
||||||
|
break
|
||||||
|
return distro
|
||||||
|
|
||||||
|
|
||||||
|
def compare_version(a, b):
|
||||||
|
"""Compare package versions"""
|
||||||
|
re_digits_non_digits = re.compile(r'\d+|\D+')
|
||||||
|
re_digits = re.compile(r'\d+')
|
||||||
|
re_digit = re.compile(r'\d')
|
||||||
|
|
||||||
|
def order(c):
|
||||||
|
if c == '~':
|
||||||
|
return -1
|
||||||
|
if re_digit.match(c):
|
||||||
|
return int(c) + 1
|
||||||
|
if re_alpha.match(c):
|
||||||
|
return ord(c)
|
||||||
|
return ord(c) + 256
|
||||||
|
|
||||||
|
def compare_parts(p1, p2):
|
||||||
|
if p1 is None:
|
||||||
|
p1 = ''
|
||||||
|
if p2 is None:
|
||||||
|
p2 = ''
|
||||||
|
|
||||||
|
lhs = re_digits_non_digits.findall(p1)
|
||||||
|
rhs = re_digits_non_digits.findall(p2)
|
||||||
|
|
||||||
|
while lhs or rhs:
|
||||||
|
left = '0'
|
||||||
|
right = '0'
|
||||||
|
|
||||||
|
if lhs:
|
||||||
|
left = lhs.pop(0)
|
||||||
|
if rhs:
|
||||||
|
right = rhs.pop(0)
|
||||||
|
|
||||||
|
if re_digits.match(left) and re_digits.match(right):
|
||||||
|
val_left = int(left)
|
||||||
|
val_right = int(right)
|
||||||
|
|
||||||
|
if val_left < val_right:
|
||||||
|
return 1
|
||||||
|
if val_left > val_right:
|
||||||
|
return -1
|
||||||
|
else:
|
||||||
|
rv = compare_str(left, right)
|
||||||
|
if rv != 0:
|
||||||
|
return rv
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def compare_str(s1, s2):
|
||||||
|
lhs = [order(c) for c in s1]
|
||||||
|
rhs = [order(c) for c in s2]
|
||||||
|
|
||||||
|
while lhs or rhs:
|
||||||
|
left = '0'
|
||||||
|
right = '0'
|
||||||
|
|
||||||
|
if lhs:
|
||||||
|
left = lhs.pop(0)
|
||||||
|
if rhs:
|
||||||
|
right = rhs.pop(0)
|
||||||
|
|
||||||
|
if left < right:
|
||||||
|
return 1
|
||||||
|
if left > right:
|
||||||
|
return -1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
e1 = int(a.get('epoch', 0))
|
||||||
|
e2 = int(b.get('epoch', 0))
|
||||||
|
|
||||||
|
if e1 < e2:
|
||||||
|
return 1
|
||||||
|
if e1 > e2:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
re_alpha = re.compile(r'[A-Za-z]')
|
||||||
|
|
||||||
|
v1 = a.get('version')
|
||||||
|
v2 = b.get('version')
|
||||||
|
|
||||||
|
rc = compare_parts(v1, v2)
|
||||||
|
|
||||||
|
if rc != 0:
|
||||||
|
return rc
|
||||||
|
|
||||||
|
r1 = a.get('release')
|
||||||
|
r2 = b.get('release')
|
||||||
|
|
||||||
|
rc = compare_parts(r1, r2)
|
||||||
|
|
||||||
|
if rc != 0:
|
||||||
|
return rc
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def dict_factory(cursor, row):
|
||||||
|
d = dict()
|
||||||
|
for idx, col in enumerate(cursor.description):
|
||||||
|
d[col[0]] = row[idx]
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def get_yum_history_db():
|
||||||
|
"""Get the latest yum history database"""
|
||||||
|
databases = glob.glob('/var/lib/yum/history/history-*-*-*.sqlite')
|
||||||
|
reversed(sorted(databases))
|
||||||
|
logger.debug('SQLite databases found: {0}'.format(', '.join(databases)))
|
||||||
|
|
||||||
|
for db in databases:
|
||||||
|
filename = os.path.basename(db)
|
||||||
|
date = filename[filename.find('-') + 1:filename.rfind('.')]
|
||||||
|
# validate filename
|
||||||
|
parts = date.split('-', 4)
|
||||||
|
|
||||||
|
if len(parts) != 3:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
[int(p) for p in parts]
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
logger.info('Using database: {0}'.format(db))
|
||||||
|
return db
|
||||||
|
logger.info('No valid database found')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def list_packages_yum():
|
||||||
|
"""List packages installed by yum"""
|
||||||
|
history_db = get_yum_history_db()
|
||||||
|
|
||||||
|
if history_db is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if sys.version_info.major == 2:
|
||||||
|
db_url = history_db
|
||||||
|
else:
|
||||||
|
db_url = 'file:///{0}?mode=ro'.format(history_db)
|
||||||
|
|
||||||
|
logger.debug('Connecting to {0}'.format(db_url))
|
||||||
|
|
||||||
|
with closing(sqlite3.connect(db_url)) as conn:
|
||||||
|
conn.row_factory = dict_factory
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
cur.execute("""SELECT name, arch, epoch, version, release, state
|
||||||
|
FROM trans_data_pkgs
|
||||||
|
JOIN pkgtups ON
|
||||||
|
trans_data_pkgs.pkgtupid = pkgtups.pkgtupid
|
||||||
|
JOIN trans_beg ON
|
||||||
|
trans_beg.tid = trans_data_pkgs.tid
|
||||||
|
ORDER BY timestamp ASC""")
|
||||||
|
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
logger.info('Found {0} rows'.format(len(rows)))
|
||||||
|
|
||||||
|
results = dict()
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
name = row['name']
|
||||||
|
arch = row['arch']
|
||||||
|
package = '{0}:{1}'.format(name, arch)
|
||||||
|
|
||||||
|
state = row['state']
|
||||||
|
|
||||||
|
if state in ('Install', 'True-Install', 'Dep-Install', 'Upgrade', 'Update', 'Obsoleting'):
|
||||||
|
logger.debug('+++ {0}: {1}'.format(state, row))
|
||||||
|
results[package] = dict(
|
||||||
|
name=name,
|
||||||
|
arch=arch,
|
||||||
|
epoch=row['epoch'],
|
||||||
|
release=row['release'],
|
||||||
|
version=row['version'],
|
||||||
|
)
|
||||||
|
elif state in ('Erase', 'Obsoleted'):
|
||||||
|
logger.debug('--- {0}: {1}'.format(state, row))
|
||||||
|
if package in results:
|
||||||
|
del results[package]
|
||||||
|
else:
|
||||||
|
logger.debug('... {0}: {1}'.format(state, row))
|
||||||
|
|
||||||
|
logger.info('Total packages: {0}'.format(len(results)))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def list_packages_dnf():
|
||||||
|
"""List packages installed by dnf"""
|
||||||
|
|
||||||
|
# https://github.com/rpm-software-management/libdnf/blob/9a0e17562b19586b3ffa70fa93eb961b558794c7/libdnf/transaction/Types.hpp
|
||||||
|
# INSTALL = 1, // a new package that was installed on the system
|
||||||
|
# DOWNGRADE = 2, // an older package version that replaced previously installed version
|
||||||
|
# DOWNGRADED = 3, // an original package version that was replaced
|
||||||
|
# OBSOLETE = 4, //
|
||||||
|
# OBSOLETED = 5, //
|
||||||
|
# UPGRADE = 6, //
|
||||||
|
# UPGRADED = 7, //
|
||||||
|
# REMOVE = 8, // a package that was removed from the system
|
||||||
|
# REINSTALL = 9, // a package that was reinstalled with the identical version
|
||||||
|
# REINSTALLED = 10, // a package that was reinstalled with the identical version (old repo, for example)
|
||||||
|
# REASON_CHANGE = 11 // a package was kept on the system but it's reason has changed
|
||||||
|
|
||||||
|
history_db = '/var/lib/dnf/history.sqlite'
|
||||||
|
|
||||||
|
if not os.path.isfile(history_db):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if sys.version_info.major == 2:
|
||||||
|
db_url = history_db
|
||||||
|
else:
|
||||||
|
db_url = 'file:///{0}?mode=ro'.format(history_db)
|
||||||
|
|
||||||
|
logger.info('Connecting to {0}'.format(db_url))
|
||||||
|
with closing(sqlite3.connect(db_url)) as conn:
|
||||||
|
conn.row_factory = dict_factory
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
cur.execute("""SELECT name, arch, epoch, version, release, action
|
||||||
|
FROM trans_item
|
||||||
|
JOIN trans ON trans_item.trans_id = trans.id
|
||||||
|
JOIN rpm ON rpm.item_id = trans_item.item_id
|
||||||
|
ORDER BY dt_end ASC""")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
logger.info('Found {0} rows'.format(len(rows)))
|
||||||
|
|
||||||
|
results = dict()
|
||||||
|
for row in rows:
|
||||||
|
name = row['name']
|
||||||
|
arch = row['arch']
|
||||||
|
action = row['action']
|
||||||
|
package = '{0}:{1}'.format(name, arch)
|
||||||
|
|
||||||
|
if action in (1, 6): # INSTALL, UPGRADE
|
||||||
|
results[package] = dict(
|
||||||
|
name=name,
|
||||||
|
arch=arch,
|
||||||
|
epoch=row['epoch'],
|
||||||
|
release=row['release'],
|
||||||
|
version=row['version']
|
||||||
|
)
|
||||||
|
elif row['action'] in (5, 8): # OBSOLETED, REMOVE
|
||||||
|
if package in results:
|
||||||
|
del results[package]
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dpkg_status():
|
||||||
|
def split_field(l):
|
||||||
|
if ':' in l:
|
||||||
|
field, value = l.split(':', 1)
|
||||||
|
return field, value.strip()
|
||||||
|
return l, None
|
||||||
|
|
||||||
|
with open('/var/lib/dpkg/status', 'r') as fi:
|
||||||
|
lines = fi.readlines()
|
||||||
|
|
||||||
|
packages = list()
|
||||||
|
package = dict()
|
||||||
|
last_field = None
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.rstrip('\n')
|
||||||
|
|
||||||
|
# starting a new package entry
|
||||||
|
if line == '' and package:
|
||||||
|
packages.append(package)
|
||||||
|
package = dict()
|
||||||
|
continue
|
||||||
|
|
||||||
|
# continuation of the previous field
|
||||||
|
if line.startswith(' '):
|
||||||
|
package[last_field] += line
|
||||||
|
else:
|
||||||
|
field, value = split_field(line)
|
||||||
|
package[field] = value
|
||||||
|
last_field = field
|
||||||
|
|
||||||
|
if package: # just in case the file doesn't end with a new line
|
||||||
|
packages.append(package)
|
||||||
|
return packages
|
||||||
|
|
||||||
|
|
||||||
|
def list_packages_dpkg():
|
||||||
|
"""Get list of packages installed on a Debian-based system"""
|
||||||
|
def parse_version(ver):
|
||||||
|
e = '0'
|
||||||
|
v = ver
|
||||||
|
if ':' in ver:
|
||||||
|
e = ver[:ver.find(':')]
|
||||||
|
v = ver[ver.find(':') + 1:]
|
||||||
|
|
||||||
|
if '-' in ver:
|
||||||
|
v = ver[:ver.find('-')]
|
||||||
|
r = ver[ver.find('-') + 1:]
|
||||||
|
else:
|
||||||
|
r = None
|
||||||
|
return e, v, r
|
||||||
|
|
||||||
|
packages = parse_dpkg_status()
|
||||||
|
|
||||||
|
results = dict()
|
||||||
|
for package in packages:
|
||||||
|
name = package.get('Package')
|
||||||
|
status = package.get('Status')
|
||||||
|
|
||||||
|
if status != 'install ok installed':
|
||||||
|
logger.debug('Package not installed, skipping: {0}'.format(name))
|
||||||
|
continue
|
||||||
|
|
||||||
|
arch = package.get('Architecture')
|
||||||
|
version_str = package.get('Version')
|
||||||
|
epoch, version, release = parse_version(version_str)
|
||||||
|
|
||||||
|
pkgarch = '{0}:{1}'.format(name, arch)
|
||||||
|
results[pkgarch] = dict(
|
||||||
|
name=name,
|
||||||
|
arch=arch,
|
||||||
|
epoch=epoch,
|
||||||
|
release=release,
|
||||||
|
version=version
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def list_packages(distro):
|
||||||
|
"""Get list of packages installed on the system"""
|
||||||
|
packages = list()
|
||||||
|
|
||||||
|
if distro in RH_FAMILY:
|
||||||
|
packages = list_packages_yum()
|
||||||
|
if packages is None:
|
||||||
|
packages = list_packages_dnf()
|
||||||
|
if packages is None:
|
||||||
|
raise HistoryFileNotFoundError()
|
||||||
|
elif distro in DEB_FAMILY:
|
||||||
|
packages = list_packages_dpkg()
|
||||||
|
|
||||||
|
return packages
|
||||||
|
|
||||||
|
|
||||||
|
def package_full(d, distro):
|
||||||
|
"""Get a string of a package name, arch, version to pass to the package manager"""
|
||||||
|
if distro in RH_FAMILY:
|
||||||
|
return '{name}-{epoch}:{version}-{release}.{arch}'.format(**d)
|
||||||
|
elif distro in DEB_FAMILY:
|
||||||
|
package = ['{name}:{arch}'.format(**d), '=']
|
||||||
|
|
||||||
|
# using explicit 0 epochs don't seem to work, so ignore it
|
||||||
|
epoch = d.get('epoch')
|
||||||
|
if epoch and epoch != '0':
|
||||||
|
package.extend([epoch, ':'])
|
||||||
|
|
||||||
|
version = d.get('version')
|
||||||
|
package.append(version)
|
||||||
|
|
||||||
|
# release is optional
|
||||||
|
release = d.get('release')
|
||||||
|
if release:
|
||||||
|
package.extend(['-', release])
|
||||||
|
return ''.join(package)
|
||||||
|
return '{name}'.format(**d) # need a better default
|
||||||
|
|
||||||
|
|
||||||
|
def package_name(d, distro):
|
||||||
|
"""Get the string of the package name and arch to pass to a package manager"""
|
||||||
|
if distro in RH_FAMILY:
|
||||||
|
return '{name}.{arch}'.format(**d)
|
||||||
|
elif distro in DEB_FAMILY:
|
||||||
|
return '{name}:{arch}'.format(**d)
|
||||||
|
return '{name}'.format(**d)
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_input(d):
|
||||||
|
if not isinstance(d, dict):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for k, v in d.items():
|
||||||
|
if not isinstance(k, str) or not isinstance(v, dict):
|
||||||
|
return False
|
||||||
|
if 'name' not in v and 'version' not in v:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main(options):
|
||||||
|
logger.debug(options)
|
||||||
|
if options.distro:
|
||||||
|
distro = options.distro
|
||||||
|
else:
|
||||||
|
distro = detect_distro()
|
||||||
|
|
||||||
|
logger.info('Detected distribution: {0}'.format(distro))
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_packages = list_packages(distro)
|
||||||
|
except HistoryFileNotFoundError:
|
||||||
|
logger.error('Could not find package history')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if options.command == 'export':
|
||||||
|
if options.file == '-':
|
||||||
|
json.dump(current_packages, sys.stdout, indent=2)
|
||||||
|
else:
|
||||||
|
logger.info('Writing to {0}'.format(options.file))
|
||||||
|
with open(options.file, 'w') as fo:
|
||||||
|
json.dump(current_packages, fo)
|
||||||
|
|
||||||
|
elif options.command == 'import':
|
||||||
|
if options.name_only:
|
||||||
|
pkg_name_fn = package_name
|
||||||
|
else:
|
||||||
|
pkg_name_fn = package_full
|
||||||
|
|
||||||
|
if options.file == '-':
|
||||||
|
try:
|
||||||
|
imported_packages = json.load(sys.stdin)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
with open(options.file, 'r') as fi:
|
||||||
|
imported_packages = json.load(fi)
|
||||||
|
|
||||||
|
if not is_valid_input(imported_packages):
|
||||||
|
logger.error('Input file is in an invalid format')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
packages = list()
|
||||||
|
|
||||||
|
if options.upgraded or options.outdated:
|
||||||
|
upgraded_packages = sorted(set(imported_packages).intersection(current_packages))
|
||||||
|
for pkg in upgraded_packages:
|
||||||
|
current = current_packages.get(pkg)
|
||||||
|
imported = imported_packages.get(pkg)
|
||||||
|
|
||||||
|
rc = compare_version(current, imported)
|
||||||
|
|
||||||
|
if rc == 1 and options.upgraded:
|
||||||
|
logger.debug('Upgrade: {0} (current) - {1} (imported)'.format(current, imported))
|
||||||
|
packages.append(imported)
|
||||||
|
elif rc == -1 and options.outdated:
|
||||||
|
logger.debug('Outdated: {0} (current) - {1} (imported)'.format(current, imported))
|
||||||
|
packages.append(current)
|
||||||
|
elif options.new:
|
||||||
|
new_packages = sorted(set(imported_packages).difference(current_packages))
|
||||||
|
packages = [imported_packages.get(p) for p in new_packages]
|
||||||
|
elif options.removed:
|
||||||
|
removed_packages = sorted(set(current_packages).difference(imported_packages))
|
||||||
|
packages = [current_packages.get(p) for p in removed_packages]
|
||||||
|
|
||||||
|
logger.debug('Packages: {0}', packages)
|
||||||
|
try:
|
||||||
|
for pkg in packages:
|
||||||
|
name = pkg_name_fn(pkg, distro)
|
||||||
|
print(name)
|
||||||
|
except IOError:
|
||||||
|
logger.debug('Broken pipe')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
parser.add_argument('--debug', action='store_true')
|
||||||
|
parser.add_argument('--verbose', action='store_true')
|
||||||
|
parser.add_argument('--distro', choices=DIST_FAMILIES,
|
||||||
|
help='Force distribution')
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(dest='command')
|
||||||
|
subparsers.required = True # https://bugs.python.org/issue9253
|
||||||
|
|
||||||
|
parser_export = subparsers.add_parser('export')
|
||||||
|
parser_export.add_argument('file', help='export packages to this file')
|
||||||
|
|
||||||
|
parser_import = subparsers.add_parser('import')
|
||||||
|
parser_import.add_argument('file', help='JSON file containing packages')
|
||||||
|
parser_import.add_argument('--name-only', action='store_true',
|
||||||
|
help='Output package names and architecture (no version)')
|
||||||
|
|
||||||
|
pkg_group = parser_import.add_mutually_exclusive_group()
|
||||||
|
pkg_group.add_argument('--upgraded', action='store_true',
|
||||||
|
help='Only show upgraded packages (default)')
|
||||||
|
pkg_group.add_argument('--new', dest='new', action='store_true',
|
||||||
|
help='Only show new packages')
|
||||||
|
pkg_group.add_argument('--removed', action='store_true',
|
||||||
|
help='Only show removed packages')
|
||||||
|
pkg_group.add_argument('--outdated', action='store_true',
|
||||||
|
help='Only show outdated packages')
|
||||||
|
pkg_group.add_argument('--same', action='store_true',
|
||||||
|
help='Only show matching packages')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
stderr_handler = logging.StreamHandler(sys.stderr)
|
||||||
|
logger.addHandler(stderr_handler)
|
||||||
|
|
||||||
|
if args.debug:
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
elif args.verbose:
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
else:
|
||||||
|
logger.setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
if args.command == 'import':
|
||||||
|
if not any((args.upgraded, args.new,
|
||||||
|
args.removed, args.outdated, args.same)):
|
||||||
|
args.upgraded = True
|
||||||
|
|
||||||
|
main(args)
|
||||||
Reference in New Issue
Block a user