diff --git a/src/rockstor/storageadmin/views/command.py b/src/rockstor/storageadmin/views/command.py
index caab8dfa2..01764a4ff 100644
--- a/src/rockstor/storageadmin/views/command.py
+++ b/src/rockstor/storageadmin/views/command.py
@@ -39,7 +39,7 @@
from django.db import transaction
from share_helpers import (sftp_snap_toggle, import_shares, import_snapshots)
from rest_framework_custom.oauth_wrapper import RockstorOAuth2Authentication
-from system.pkg_mgmt import (auto_update, current_version, update_check,
+from system.pkg_mgmt import (auto_update, current_version, rockstor_pkg_update_check,
update_run, auto_update_status)
from nfs_exports import NFSExportMixin
import logging
@@ -222,7 +222,7 @@ def post(self, request, command, rtcepoch=None):
status='active')
except UpdateSubscription.DoesNotExist:
pass
- return Response(update_check(subscription=subo))
+ return Response(rockstor_pkg_update_check(subscription=subo))
except Exception as e:
e_msg = ('Unable to check update due to a system error: '
'({}).').format(e.__str__())
@@ -238,7 +238,7 @@ def post(self, request, command, rtcepoch=None):
if request.auth is None:
update_run()
else:
- update_run(yum_update=True)
+ update_run(update_all_other=True)
return Response('Done')
except Exception as e:
e_msg = ('Update failed due to this exception: '
diff --git a/src/rockstor/system/pkg_mgmt.py b/src/rockstor/system/pkg_mgmt.py
index 310ccfcae..7496477fa 100644
--- a/src/rockstor/system/pkg_mgmt.py
+++ b/src/rockstor/system/pkg_mgmt.py
@@ -17,6 +17,7 @@
"""
import os
+import platform
import re
import stat
from tempfile import mkstemp
@@ -24,7 +25,7 @@
from services import systemctl
import shutil
import time
-from datetime import (datetime, timedelta)
+from datetime import datetime, timedelta
import requests
from django.conf import settings
from system.exceptions import CommandException
@@ -33,78 +34,81 @@
logger = logging.getLogger(__name__)
-YUM = '/usr/bin/yum'
-RPM = '/usr/bin/rpm'
-SYSTEMCTL = '/usr/bin/systemctl'
-AT = '/usr/bin/at'
-YCFILE = '/etc/yum/yum-cron.conf' # Doesn't exist in openSUSE
+YUM = "/usr/bin/yum"
+RPM = "/usr/bin/rpm"
+ZYPPER = "/usr/bin/zypper"
+SYSTEMCTL = "/usr/bin/systemctl"
+AT = "/usr/bin/at"
+YCFILE = "/etc/yum/yum-cron.conf" # Doesn't exist in openSUSE
def install_pkg(name):
- return run_command([YUM, '--setopt=timeout=600', '-y', 'install', name])
+ return run_command([YUM, "--setopt=timeout=600", "-y", "install", name])
def downgrade_pkgs(*packages):
- cmd = [YUM, '--setopt=timeout=600', '-y', 'downgrade', ]
+ cmd = [YUM, "--setopt=timeout=600", "-y", "downgrade"]
for p in packages:
cmd.append(p)
return run_command(cmd)
def auto_update(enable=True):
- service = 'yum-cron'
+ # TODO: Add openSUSE zypper equivalent
+ service = "yum-cron"
fo, npath = mkstemp()
updated = False
- with open(YCFILE) as ifo, open(npath, 'w') as tfo:
+ with open(YCFILE) as ifo, open(npath, "w") as tfo:
for line in ifo.readlines():
- if (re.match('apply_updates = ', line) is not None):
- if (enable):
- tfo.write('apply_updates = yes\n')
+ if re.match("apply_updates = ", line) is not None:
+ if enable:
+ tfo.write("apply_updates = yes\n")
else:
- tfo.write('apply_updates = no\n')
+ tfo.write("apply_updates = no\n")
updated = True
else:
tfo.write(line)
- if (not updated):
- raise Exception('apply_updates directive missing in %s, assuming its '
- 'is corrupt. No change made.' % YCFILE)
+ if not updated:
+ raise Exception(
+ "apply_updates directive missing in {}, assuming it "
+ "is corrupt. No change made.".format(YCFILE)
+ )
shutil.move(npath, YCFILE)
- if (enable):
- systemctl(service, 'enable')
- systemctl(service, 'start')
+ if enable:
+ systemctl(service, "enable")
+ systemctl(service, "start")
else:
- systemctl(service, 'stop')
- systemctl(service, 'disable')
+ systemctl(service, "stop")
+ systemctl(service, "disable")
def auto_update_status():
enabled = False
with open(YCFILE) as ifo:
for line in ifo.readlines():
- if (re.match('apply_updates = yes', line) is not None):
+ if re.match("apply_updates = yes", line) is not None:
enabled = True
break
- if (enabled):
- systemctl('yum-cron', 'status')
+ if enabled:
+ systemctl("yum-cron", "status")
return enabled
def current_version():
- out, err, rc = run_command([RPM, '-qi', 'rockstor'], throw=False)
- if (rc != 0):
- return '0.0-0'
- return ('%s-%s' % (out[1].split(':')[-1].strip(),
- out[2].split(':')[-1].strip()))
+ out, err, rc = run_command([RPM, "-qi", "rockstor"], throw=False)
+ if rc != 0:
+ return "0.0-0"
+ return "{}-{}".format(out[1].split(":")[-1].strip(), out[2].split(":")[-1].strip())
def rpm_build_info(pkg):
- version = 'Unknown Version'
+ version = "Unknown Version"
date = None
try:
- o, e, rc = run_command([YUM, 'info', 'installed', '-v', pkg])
+ o, e, rc = run_command([YUM, "info", "installed", "-v", pkg])
except CommandException as e:
# Catch "No matching Packages to list" so we can return None, None.
- emsg = 'Error: No matching Packages to list'
+ emsg = "Error: No matching Packages to list"
# By checking both the first error element and the second to last we
# catch one yum waiting for another to release yum lock.
if e.err[0] == emsg or e.err[-2] == emsg:
@@ -113,202 +117,302 @@ def rpm_build_info(pkg):
# otherwise we raise an exception as normal.
raise e
for l in o:
- if (re.match('Buildtime', l) is not None):
+ if re.match("Buildtime", l) is not None:
# eg: "Buildtime : Tue Dec 5 13:34:06 2017"
# we return 2017-Dec-06
# Note the one day on from retrieved Buildtime with zero padding.
dfields = l.strip().split()
- dstr = dfields[6] + ' ' + dfields[3] + ' ' + dfields[4]
- bdate = datetime.strptime(dstr, '%Y %b %d')
+ dstr = dfields[6] + " " + dfields[3] + " " + dfields[4]
+ bdate = datetime.strptime(dstr, "%Y %b %d")
bdate += timedelta(days=1)
- date = bdate.strftime('%Y-%b-%d')
- if (re.match('Version ', l) is not None):
+ date = bdate.strftime("%Y-%b-%d")
+ if re.match("Version ", l) is not None:
version = l.strip().split()[2]
- if (re.match('Release ', l) is not None):
- version = '%s-%s' % (version, l.strip().split()[2])
+ if re.match("Release ", l) is not None:
+ version = "{}-{}".format(version, l.strip().split()[2])
return version, date
+def zypper_repos_list():
+ """
+ Low level wrapper around "zypper repos"
+ :return: List of repo Alias's
+ """
+ repo_list = []
+ cmd = [ZYPPER, "-q", "repos"]
+ out, err, rc = run_command(cmd, log=True)
+ if len(out) > 0 and rc == 0:
+ for line in out:
+ # Skip empty or header lines
+ if len(line) == 0 or line[0] == "#" or line[0] == "-":
+ continue
+ line_fields = line.split()
+ if len(line_fields) >= 3:
+ repo_list.append(line_fields[2])
+ return repo_list
+
+
def switch_repo(subscription, on=True):
- repos_dir = '/etc/yum.repos.d'
- yum_file = '{}/Rockstor-{}.repo'.format(repos_dir, subscription.name)
+ repos_dir = "/etc/yum.repos.d"
+ yum_file = "{}/Rockstor-{}.repo".format(repos_dir, subscription.name)
+ rock_pub_key_file = "{}conf/ROCKSTOR-GPG-KEY".format(settings.ROOT_DIR)
# Historically our base subscription url denotes our CentOS rpm repo.
subscription_distro_url = subscription.url
distro_id = distro.id()
- if distro_id == 'opensuse-leap':
- subscription_distro_url += '/leap/{}'.format(distro.version())
- elif distro_id == 'opensuse-tumbleweed':
- subscription_distro_url += '/tumbleweed'
+ use_zypper = True
+ repo_alias = "Rockstor-{}".format(subscription.name)
+ logger.debug("########### SWITCH REPO repo-alias = {}".format(repo_alias))
+ if distro_id == "opensuse-leap":
+ subscription_distro_url += "/leap/{}".format(distro.version())
+ elif distro_id == "opensuse-tumbleweed":
+ subscription_distro_url += "/tumbleweed"
+ else:
+ use_zypper = False
# Check if dir /etc/yum.repos.d exists and if not create.
if not os.path.isdir(repos_dir):
# Can use os.makedirs(path) if intermediate levels also don't exist.
- os.mkdir(repos_dir, )
- if (on):
- with open(yum_file, 'w') as rfo:
- rfo.write('[Rockstor-%s]\n' % subscription.name)
- rfo.write('name=%s\n' % subscription.description)
- if (subscription.password is not None):
- rfo.write('baseurl=http://%s:%s@%s\n' %
- (subscription.appliance.uuid, subscription.password,
- subscription_distro_url))
- else:
- rfo.write('baseurl=http://%s\n' % subscription_distro_url)
- rfo.write('enabled=1\n')
- rfo.write('gpgcheck=1\n')
- rfo.write('gpgkey=file://%sconf/ROCKSTOR-GPG-KEY\n'
- % settings.ROOT_DIR)
- rfo.write('metadata_expire=1h\n')
+ os.mkdir(repos_dir)
+ if subscription.password is not None:
+ repo_url = "http://{}:{}@{}".format(
+ subscription.appliance.uuid, subscription.password, subscription_distro_url
+ )
+ else:
+ repo_url = "http://{}".format(subscription_distro_url)
+ if on:
+ if use_zypper:
+ run_command([RPM, "--import", rock_pub_key_file], log=True)
+ current_repo_list = zypper_repos_list()
+ if subscription.name == "Stable" and repo_alias not in current_repo_list:
+ if "Rockstor-Testing" in current_repo_list:
+ run_command([ZYPPER, "removerepo", "Rockstor-Testing"])
+ # If already added rc=4
+ run_command(
+ [ZYPPER, "addrepo", "--refresh", repo_url, repo_alias],
+ log=True,
+ throw=False,
+ )
+ if subscription.name == "Testing" and repo_alias not in zypper_repos_list():
+ if "Rockstor-Stable" in current_repo_list:
+ run_command([ZYPPER, "removerepo", "Rockstor-Stable"])
+ # If already added rc=4
+ run_command(
+ [ZYPPER, "addrepo", "--refresh", repo_url, repo_alias],
+ log=True,
+ throw=False,
+ )
+ # N.B. for now we also use YUM (read only) on openSUSE
+ with open(yum_file, "w") as rfo:
+ rfo.write("[Rockstor-{}]\n".format(subscription.name))
+ rfo.write("name={}\n".format(subscription.description))
+ rfo.write("baseurl={}\n".format(repo_url))
+ rfo.write("enabled=1\n")
+ rfo.write("gpgcheck=1\n")
+ rfo.write("gpgkey=file://{}\n".format(rock_pub_key_file))
+ rfo.write("metadata_expire=1h\n")
# Set file to rw- --- --- (600) via stat constants.
os.chmod(yum_file, stat.S_IRUSR | stat.S_IWUSR)
else:
- if (os.path.exists(yum_file)):
+ if os.path.exists(yum_file):
os.remove(yum_file)
+ if use_zypper:
+ run_command(
+ [ZYPPER, "removerepo", "Rockstor-Testing", "Rockstor-Stable"], log=True
+ )
def repo_status(subscription):
- if (subscription.password is None):
- return ('active', 'public repo')
-
+ if subscription.password is None:
+ return "active", "public repo"
try:
- res = requests.get('http://%s' % subscription.url,
- auth=(subscription.appliance.uuid,
- subscription.password))
- if (res.status_code == 401):
- return ('inactive', res.text)
- elif (res.status_code == 200):
- return ('active', res.text)
- return (res.status_code, res.text)
+ res = requests.get(
+ "http://{}".format(subscription.url),
+ auth=(subscription.appliance.uuid, subscription.password),
+ )
+ if res.status_code == 401:
+ return "inactive", res.text
+ elif res.status_code == 200:
+ return "active", res.text
+ return res.status_code, res.text
except requests.ConnectionError as e:
- e_msg = ('Failed to connect to %s. Is the Rockstor system connected '
- 'to the internet?. Lower level exception: %s'
- % (subscription.url, e.__str__()))
+ e_msg = (
+ "Failed to connect to {}. Is the Rockstor system connected "
+ "to the internet?. Lower level exception: {}".format(
+ subscription.url, e.__str__()
+ )
+ )
raise Exception(e_msg)
-def update_check(subscription=None):
- if (subscription is not None):
+def rockstor_pkg_update_check(subscription=None):
+ if subscription is not None:
switch_repo(subscription)
-
- pkg = 'rockstor'
+ pkg = "rockstor"
version, date = rpm_build_info(pkg)
if date is None:
# None date signifies no rpm installed so list all changelog entries.
- date = 'all'
+ date = "all"
log = False
available = False
new_version = None
updates = []
try:
- o, e, rc = run_command([YUM, 'changelog', date, pkg])
+ o, e, rc = run_command([YUM, "changelog", date, pkg])
except CommandException as e:
# Catch as yet unconfigured repos ie Leap 15.1: error log accordingly.
# Avoids breaking current version display and update channel selection.
- emsg = 'Error\\: Cannot retrieve repository metadata \\(repomd.xml\\)'
+ emsg = "Error\\: Cannot retrieve repository metadata \\(repomd.xml\\)"
if re.match(emsg, e.err[-2]) is not None:
- logger.error('Rockstor repo for distro.id ({}) version ({}) may '
- 'not exist: pending or deprecated.\nReceived: ({}).'
- .format(distro.id(), distro.version(), e.err))
+ logger.error(
+ "Rockstor repo for distro.id ({}) version ({}) may "
+ "not exist: pending or deprecated.\nReceived: ({}).".format(
+ distro.id(), distro.version(), e.err
+ )
+ )
new_version = version # Explicitly set (flag) for code clarity.
return version, new_version, updates
# otherwise we raise an exception as normal.
raise e
for l in o:
- if (re.search('Available Packages', l) is not None):
+ if re.search("Available Packages", l) is not None:
available = True
- if (not available):
+ if not available:
continue
- if (new_version is None and (re.match('rockstor-', l) is not None)):
- new_version = l.split()[0].split(
- 'rockstor-')[1].split('.x86_64')[0]
- if (log is True):
+ if new_version is None and (re.match("rockstor-", l) is not None):
+ machine_arch = platform.machine()
+ new_version = (
+ l.split()[0].split("rockstor-")[1].split(".{}".format(machine_arch))[0]
+ )
+ if log is True:
updates.append(l)
- if (len(l.strip()) == 0):
+ if len(l.strip()) == 0:
log = False
- if (re.match('\* ', l) is not None):
+ if re.match("\* ", l) is not None:
updates.append(l)
log = True
- if (new_version is None):
+ if new_version is None:
new_version = version
# do a second check which is valid for updates without changelog
# updates. eg: same day updates, testing updates.
- o, e, rc = run_command([YUM, 'update', pkg, '--assumeno'], throw=False)
- if (rc == 1):
+ o, e, rc = run_command([YUM, "update", pkg, "--assumeno"], throw=False)
+ if rc == 1:
for l in o:
- if (re.search('will be an update', l) is not None):
- if (re.search('rockstor.x86_64', l) is not None):
- new_version = l.strip().split()[3].split(':')[1]
-
+ if re.search("will be an update", l) is not None:
+ if re.search("rockstor.x86_64", l) is not None:
+ new_version = l.strip().split()[3].split(":")[1]
return version, new_version, updates
-def update_run(subscription=None, yum_update=False):
+def update_run(subscription=None, update_all_other=False):
# update_run modified to handle yum updates too
# and avoid an ad hoc yum update function
# If we have a yum update we don't stop/start Rockstor and
# don't delete *.pyc files
- if (subscription is not None):
+ if subscription is not None:
switch_repo(subscription)
-
- run_command([SYSTEMCTL, 'start', 'atd'])
+ run_command([SYSTEMCTL, "start", "atd"])
fh, npath = mkstemp()
- with open(npath, 'w') as atfo:
- if not yum_update:
- atfo.write('%s stop rockstor\n' % SYSTEMCTL)
+ # Set system wide package manager refresh command according to distro.
+ distro_id = distro.id()
+ pkg_refresh_cmd = "{} --non-interactive refresh\n".format(ZYPPER)
+ if distro_id == "rockstor": # CentOS based Rockstor conditional
+ pkg_refresh_cmd = "{} --setopt=timeout=600 -y update\n".format(YUM)
+ # Set package manager rockstor install/update command according to distro.
+ pkg_in_up_rockstor = "{} --non-interactive install rockstor\n".format(ZYPPER)
+ if distro_id == "rockstor": # CentOS based Rockstor conditional
+ pkg_in_up_rockstor = "{} --setopt=timeout=600 -y install rockstor\n".format(YUM)
+ pkg_update_all = ""
+ if distro_id == "opensuse-leap":
+ pkg_update_all = "{} --non-interactive update --no-recommends\n".format(ZYPPER)
+ if distro_id == "opensuse-tumbleweed":
+ pkg_update_all = "{} --non-interactive dist-upgrade --no-recommends\n".format(
+ ZYPPER
+ )
+ with open(npath, "w") as atfo:
+ if not update_all_other:
+ atfo.write("sleep 10\n")
+ atfo.write("{} stop rockstor\n".format(SYSTEMCTL))
# rockstor-pre stop ensures initrock re-run on next rockstor start
- atfo.write('%s stop rockstor-pre\n' % SYSTEMCTL)
- atfo.write('/usr/bin/find %s -name "*.pyc" -type f -delete\n'
- % settings.ROOT_DIR)
- atfo.write('%s --setopt=timeout=600 -y update\n' % YUM)
+ atfo.write("{} stop rockstor-pre\n".format(SYSTEMCTL))
+ # Exclude eggs subdir, as these are in rpm so will be deleted
+ # as otherwise floods YUM log with "No such file or directory"
+ atfo.write(
+ '/usr/bin/find {} -name "*.pyc" -not -path "*/eggs/*" -type f -delete\n'.format(
+ settings.ROOT_DIR
+ )
+ )
+ atfo.write(pkg_refresh_cmd)
# account for moving from dev/source to package install:
- atfo.write('%s --setopt=timeout=600 -y install rockstor\n' % YUM)
+ atfo.write(pkg_in_up_rockstor)
# the following rockstor start invokes rockstor-pre (initrock) also
- atfo.write('%s start rockstor\n' % SYSTEMCTL)
- else:
- atfo.write('%s --setopt=timeout=600 -y -x rock* update\n' % YUM)
- atfo.write('/bin/rm -f %s\n' % npath)
- out, err, rc = run_command([AT, '-f', npath, 'now + 1 minutes'])
+ atfo.write("{} start rockstor\n".format(SYSTEMCTL))
+ else: # update_all_other True so update all bar the rockstor package.
+ logger.info(
+ "Updating all but rockstor package for distro {}".format(distro_id)
+ )
+ if distro_id == "rockstor":
+ atfo.write("{} --setopt=timeout=600 -y -x rock* update\n".format(YUM))
+ else:
+ atfo.write("{} addlock rockstor\n".format(ZYPPER))
+ atfo.write(pkg_update_all)
+ atfo.write("{} removelock rockstor\n".format(ZYPPER))
+ atfo.write("/bin/rm -f {}\n".format(npath))
+ # out, err, rc = run_command([AT, '-f', npath, 'now + 1 minutes'])
+ out, err, rc = run_command([AT, "-f", npath, "now"])
time.sleep(120)
-
return out, err, rc
-def pkg_changelog(package):
- # Retrieve yum packages changelog, no update_check func
- # update_check is "Rockstor specific" and with standard CentOS packages
+def pkg_changelog(package, distro_id):
+ """
+ Takes a package name and builds a dictionary based on current and update
+ info for that package by parsing the output from:
+ yum changelog 1 package
+ The result is formatted appropriate for display.
+ :param package: A package name
+ :param distro_id: System expected output from distro.id()
+ :return: Dict indexed by 'name', 'installed', 'available' and 'description'
+ """
# we can't work with rpm -qi Build Date field: some packages have
# Build Date > new package version changelog
# pkg_changelog behaviour is output beautify too, returning pkg name,
# changelog for installed package and available new package update
- out, err, rc = run_command([YUM, 'changelog', '1', package], throw=False)
- package_info = {'name': package.split('.')[0]}
- package_info['installed'] = []
- package_info['available'] = []
- package_info['description'] = ''
+ # TODO: Find way to show pending update packages changelogs in openSUSE.
+ # could be pending addition to zypper via
+ # zypper info --changelog packagename
+ # https://github.com/openSUSE/zypper/issues/138
+ out, err, rc = run_command([YUM, "changelog", "1", package], throw=False)
+ package_info = {
+ "name": package.split(".")[0],
+ "installed": [],
+ "available": [],
+ "description": "",
+ }
+ if distro_id != "rockstor":
+ package_info["available"] = [
+ "Version and changelog of update not available in openSUSE"
+ ]
installed = False
available = False
- for l in out:
- l = l.strip()
- if (re.search('Available Packages', l) is not None):
+ for line in out:
+ stripped_line = line.strip()
+ if re.search("Available Packages", stripped_line) is not None:
installed = False
available = True
continue
- if (re.search('Installed Packages', l) is not None):
+ if re.search("Installed Packages", stripped_line) is not None:
installed = True
continue
- if (re.search('changelog stats', l) is not None):
+ if re.search("changelog stats", stripped_line) is not None:
installed = False
available = False
break
- if (installed and len(l) != 0):
- package_info['installed'].append(l)
- if (available and len(l) != 0):
- package_info['available'].append(l)
-
- package_info['installed'] = '[line]'.join(package_info['installed'])
- package_info['available'] = '[line]'.join(package_info['available'])
- package_info['description'] = pkg_infos(package_info['name'],
- 'DESCRIPTION')
-
+ if installed and len(stripped_line) != 0:
+ package_info["installed"].append(stripped_line)
+ if available and len(stripped_line) != 0:
+ package_info["available"].append(stripped_line)
+ package_info["installed"] = "[line]".join(package_info["installed"])
+ package_info["available"] = "[line]".join(package_info["available"])
+ package_info["description"] = pkg_infos(package_info["name"], "DESCRIPTION")
return package_info
@@ -319,27 +423,57 @@ def pkg_infos(package, tag="DESCRIPTION"):
# to grab packages infos like on Rockstor updates check
# Full ref for avail tags here: http://rpm.org/user_doc/query_format.html
# and avail tags list with rpm --querytags
- tag = '%%{%s}' % tag
- out, err, rc = run_command([RPM, '-q', '--queryformat', tag, package],
- throw=False)
- if (rc != 0):
- return ''
-
- return ' '.join(out)
-
-
-def yum_check():
+ tag = "%%{%s}" % tag
+ out, err, rc = run_command([RPM, "-q", "--queryformat", tag, package], throw=False)
+ if rc != 0:
+ return ""
+ return " ".join(out)
+
+
+def pkg_update_check():
+ """
+ Retrieves list of available package updates and passes each in turn to
+ pkg_changelog for package specific info and presentation formatting.
+ :return: list of dictionaries returned by pkg_changelog()
+ """
# Query yum for updates and grab return code
- # yum check-update retun code is 0 with no updates
- # and 100 if at least 1 update available
+ # yum check-update return code is 0 with no updates
+ # and 100 if at least 1 update available.
+ # But this is not the case with zypper equivalent.
# Using -x rockstor* to avoid having Rockstor updated here
# instead of Rockstor "ad hoc" updater
- out, err, rc = run_command([YUM, 'check-update', '-q', '-x', 'rock*'],
- throw=False)
+ distro_id = distro.id()
+ if distro_id == "rockstor":
+ # N.B. equivalent cmd - yum check-update -q -x 'rock*'
+ # i.e. quotes important around wildcard
+ pkg_list_all_other = [YUM, "check-update", "-q", "-x", "rock*"]
+ else:
+ # N.B. although we fail to exclude the rockstor package here, so it
+ # will be listed, it is skipped in update_run(). Different behaviour
+ # form before as will, re-trigger notification but workable for now.
+ pkg_list_all_other = [ZYPPER, "--non-interactive", "-q", "list-updates"]
+ out, err, rc = run_command(pkg_list_all_other, throw=False, log=True)
+ if rc == 106: # zypper specific
+ logger.error("### REPOSITORY ERROR ###\n {}".format(err))
packages = []
- # Read check-update output skipping first and last empty line
- # on every round we apply some beautify with pkg_changelog
- for line in out[1:-1]:
- packages.append(pkg_changelog(line.split()[0].strip()))
-
- return rc, packages
+ # Read output skipping first one or two lines and last empty line
+ # on every line we add changelog info and some beautify with pkg_changelog.
+ if distro_id == "rockstor":
+ for line in out[1:-1]:
+ packages.append(pkg_changelog(line.split()[0].strip(), distro_id))
+ else:
+ # N.B. repo issues give warnings/info above the package updates table.
+ for line in out:
+ if line == "":
+ continue
+ if line[0] == "-" or line[0] == "S":
+ continue
+ # Skip "File 'repomd.xml' ..." and "Warning: ..." lines:
+ line_fields = line.split() # Assumed faster than re.match()
+ if line_fields[0] == "File" or line_fields[0] == "Warning:":
+ continue
+ line_table_fields = line.split("|")
+ if len(line_table_fields) < 3:
+ continue # Avoid index out of range on unknown line content.
+ packages.append(pkg_changelog(line_table_fields[2].strip(), distro_id))
+ return packages
diff --git a/src/rockstor/system/tests/test_pkg_mgmt.py b/src/rockstor/system/tests/test_pkg_mgmt.py
new file mode 100644
index 000000000..246aaaaff
--- /dev/null
+++ b/src/rockstor/system/tests/test_pkg_mgmt.py
@@ -0,0 +1,333 @@
+"""
+Copyright (c) 2012-2019 RockStor, Inc.
+This file is part of RockStor.
+RockStor 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 2 of the License,
+or (at your option) any later version.
+RockStor 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.
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+import unittest
+from mock import patch
+
+from system.pkg_mgmt import pkg_update_check, pkg_changelog, zypper_repos_list
+
+
+class SystemPackageTests(unittest.TestCase):
+ """
+ The tests in this suite can be run via the following command:
+ cd
+ ./bin/test --settings=test-settings -v 3 -p test_pkg_mgmt*
+ """
+
+ def setUp(self):
+ self.patch_run_command = patch("system.pkg_mgmt.run_command")
+ self.mock_run_command = self.patch_run_command.start()
+
+ # We need to test the conditions of distro.id() returning one of:
+ # rockstor, opensuse-leap, opensuse-tumbleweed
+ self.patch_distro = patch("system.pkg_mgmt.distro")
+ self.mock_distro = self.patch_distro.start()
+
+ # Mock pkg_infos to return "" to simplify higher level testing.
+ self.patch_pkg_infos = patch("system.pkg_mgmt.pkg_infos")
+ self.mock_pkg_infos = self.patch_pkg_infos.start()
+ self.mock_pkg_infos.return_value = ""
+
+ def tearDown(self):
+ patch.stopall()
+
+ def test_pkg_changelog(self):
+ """
+ Test pkg_changelog, a package changelog (including update changes)
+ parser and presenter
+ :return:
+ """
+ # Example output form "yum changelog 1 sos.noarch" such as is executed
+ # in pkg_changelog()
+ out = [
+ [
+ "==================== Installed Packages ====================", # noqa E501
+ "sos-3.6-17.el7.centos.noarch installed", # noqa E501
+ "* Tue Apr 23 05:00:00 2019 CentOS Sources - 3.6-17.el7.centos", # noqa E501
+ "- Roll in CentOS Branding", # noqa E501
+ "" # noqa E501
+ "==================== Available Packages ====================", # noqa E501
+ "sos-3.7-6.el7.centos.noarch updates", # noqa E501
+ "* Tue Sep 3 05:00:00 2019 CentOS Sources - 3.7-6.el7.centos", # noqa E501
+ "- Roll in CentOS Branding", # noqa E501
+ "", # noqa E501
+ "changelog stats. 2 pkgs, 2 source pkgs, 2 changelogs", # noqa E501
+ "", # noqa E501
+ ]
+ ]
+ err = [[""]]
+ rc = [0]
+ expected_results = [
+ {
+ "available": "sos-3.7-6.el7.centos.noarch updates[line]* Tue Sep 3 05:00:00 2019 CentOS Sources - 3.7-6.el7.centos[line]- Roll in CentOS Branding", # noqa E501
+ "description": "",
+ "name": "fake",
+ "installed": "sos-3.6-17.el7.centos.noarch installed[line]* Tue Apr 23 05:00:00 2019 CentOS Sources - 3.6-17.el7.centos[line]- Roll in CentOS Branding", # noqa E501
+ }
+ ]
+ distro_id = ["rockstor"]
+ #
+ # TODO: add openSUSE Leap example output. But currently exactly the same
+ # command but no available is listed as yum knows only rockstor repos.
+ #
+ for o, e, r, expected, distro in zip(out, err, rc, expected_results, distro_id):
+ self.mock_run_command.return_value = (o, e, r)
+ returned = pkg_changelog("fake.123", distro)
+ self.assertEqual(
+ returned,
+ expected,
+ msg="Un-expected pkg_changelog() result:\n "
+ "returned = ({}).\n "
+ "expected = ({}).".format(returned, expected),
+ )
+
+ def test_pkg_update_check(self):
+ """
+ Test pkg_update_check() across distro.id values and consequently different
+ output format of:
+ distro.id = "rockstor" (CentOS) base
+ yum check-update -q -x rock*
+ and:
+ output format of:
+ distro.id = "opensuse-leap"
+ zypper -q list-updates
+ and:
+ distro.id = "opensuse-tumbleweed"
+ same command as for opensuse-leap
+ """
+ # Mock pkg_changelog to allow for isolated testing of yum_check
+ self.patch_pkg_changelog = patch("system.pkg_mgmt.pkg_changelog")
+ self.mock_pkg_changelog = self.patch_pkg_changelog.start()
+
+ def fake_pkg_changelog(*args, **kwargs):
+ """
+ Stubbed out fake pkg_changelog to allow for isolation of caller
+ N.B. currenlty only uses single package test data to simply dict
+ comparisons, ie recersive dict sort othewise required.
+ :param args:
+ :param kwargs:
+ :return: Dict indexed by name=arg[0], installed, available, and description.
+ last 3 are = [] unless arg[1] is not "rockstor" then available has different
+ content.
+ """
+ pkg_info = {"name": args[0].split(".")[0]}
+ pkg_info["installed"] = ""
+ pkg_info["available"] = ""
+ if args[1] != "rockstor":
+ pkg_info[
+ "available"
+ ] = "Version and changelog of update not available in openSUSE"
+ pkg_info["description"] = ""
+ return pkg_info
+
+ self.mock_pkg_changelog.side_effect = fake_pkg_changelog
+
+ # TODO: We need more example here of un-happy paths
+ # zypper spaces in Repository name Example,
+ out = [
+ [
+ "S | Repository | Name | Current Version | Available Version | Arch ", # noqa E501
+ "--+------------------------+-------------------------+---------------------------------------+---------------------------------------+-------", # noqa E501
+ "v | Main Update Repository | aaa_base | 84.87+git20180409.04c9dae-lp151.5.3.1 | 84.87+git20180409.04c9dae-lp151.5.6.1 | x86_64", # noqa E501
+ "",
+ ]
+ ]
+ expected_result = [
+ [
+ {
+ "available": "Version and changelog of update not available in openSUSE",
+ "description": "",
+ "name": "aaa_base",
+ "installed": "",
+ }
+ ]
+ ]
+ err = [[""]]
+ rc = [0]
+ dist_id = ["opensuse-tumbleweed"]
+ #
+ # zypper no spaces in Repository name Example,
+ # actual Leap 15.0 but no-space repos also seen in other openSUSE variants.
+ out.append(
+ [
+ "S | Repository | Name | Current Version | Available Version | Arch", # noqa E501
+ "--+---------------------------+---------------------------------+---------------------------------------------+---------------------------------------------+-------", # noqa E501
+ "v | openSUSE-Leap-15.0-Update | NetworkManager | 1.10.6-lp150.4.6.1 | 1.10.6-lp150.4.9.1 | x86_64", # noqa E501
+ "",
+ ]
+ )
+ expected_result.append(
+ [
+ {
+ "available": "Version and changelog of update not available in openSUSE",
+ "description": "",
+ "name": "NetworkManager",
+ "installed": "",
+ }
+ ]
+ )
+ err.append([""])
+ rc.append(0)
+ dist_id.append("opensuse-tumbleweed")
+ #
+ # CentOS yum output example, ie one clear line above.
+ out.append(
+ [
+ "",
+ "epel-release.noarch 7-12 epel", # noqa E501
+ "",
+ ]
+ )
+ expected_result.append(
+ [
+ {
+ "available": "",
+ "description": "",
+ "name": "epel-release",
+ "installed": "",
+ }
+ ]
+ )
+ err.append([""])
+ rc.append(100)
+ dist_id.append("rockstor")
+ #
+ # When we have a poorly repo.
+ # '/usr/bin/zypper', '--non-interactive', '-q', 'list-updates']
+ out.append(
+ [
+ "",
+ "",
+ "",
+ "",
+ "File 'repomd.xml' from repository 'Rockstor-Testing' is unsigned, continue? [yes/no] (no): no", # noqa E501
+ "Warning: Skipping repository 'Rockstor-Testing' because of the above error.", # noqa E501
+ "S | Repository | Name | Current Version | Available Version | Arch ", # noqa E501
+ "--+------------------------+-------------------------+---------------------------------------+---------------------------------------+-------", # noqa E501
+ "v | Main Update Repository | aaa_base | 84.87+git20180409.04c9dae-lp151.5.3.1 | 84.87+git20180409.04c9dae-lp151.5.9.1 | x86_64", # noqa E501
+ "v | Main Update Repository | aaa_base-extras | 84.87+git20180409.04c9dae-lp151.5.3.1 | 84.87+git20180409.04c9dae-lp151.5.9.1 | x86_64", # noqa E501
+ "v | Main Update Repository | apparmor-parser | 2.12.2-lp151.3.2 | 2.12.3-lp151.4.3.1 | x86_64", # noqa E501
+ "v | Main Update Repository | bash | 4.4-lp151.9.53 | 4.4-lp151.10.3.1 | x86_64", # noqa E501
+ "",
+ ]
+ )
+ expected_result.append(
+ [
+ {
+ "available": "Version and changelog of update not available in openSUSE",
+ "description": "",
+ "name": "aaa_base",
+ "installed": "",
+ },
+ {
+ "available": "Version and changelog of update not available in openSUSE",
+ "description": "",
+ "name": "aaa_base-extras",
+ "installed": "",
+ },
+ {
+ "available": "Version and changelog of update not available in openSUSE",
+ "description": "",
+ "name": "apparmor-parser",
+ "installed": "",
+ },
+ {
+ "available": "Version and changelog of update not available in openSUSE",
+ "description": "",
+ "name": "bash",
+ "installed": "",
+ },
+ ]
+ )
+ err.append(
+ [
+ "Repository 'Rockstor-Testing' is invalid.",
+ "[Rockstor-Testing|http://updates.rockstor.com:8999/rockstor-testing/leap/15.1] Valid metadata not found at specified URL", # noqa E501
+ "Some of the repositories have not been refreshed because of an error.", # noqa E501
+ "",
+ ]
+ )
+ rc.append(106)
+ dist_id.append("opensuse-leap")
+ #
+ for o, e, r, expected, distro in zip(out, err, rc, expected_result, dist_id):
+ self.mock_run_command.return_value = (o, e, r)
+ self.mock_distro.id.return_value = distro
+ returned = pkg_update_check()
+ self.assertEqual(
+ returned,
+ expected,
+ msg="Un-expected yum_check() result:\n "
+ "returned = ({}).\n "
+ "expected = ({}).".format(returned, expected),
+ )
+
+ def test_zypper_repos_list(self):
+ # Test empty return values
+ out = [[""]]
+ err = [[""]]
+ rc = [0]
+ expected_results = [[]]
+ # test typical output
+ out.append(
+ [
+ "",
+ "# | Alias | Name | Enabled | GPG Check | Refresh", # noqa E501
+ "---+---------------------------+------------------------------------+---------+-----------+--------", # noqa E501
+ " 1 | Local-Repository | Local-Repository | Yes | ( p) Yes | Yes ", # noqa E501
+ " 2 | Rockstor-Testing | Rockstor-Testing | Yes | ( p) Yes | Yes ", # noqa E501
+ " 3 | illuusio | illuusio | Yes | (r ) Yes | Yes ", # noqa E501
+ " 4 | repo-debug | Debug Repository | No | ---- | ---- ", # noqa E501
+ " 5 | repo-debug-non-oss | Debug Repository (Non-OSS) | No | ---- | ---- ", # noqa E501
+ " 6 | repo-debug-update | Update Repository (Debug) | No | ---- | ---- ", # noqa E501
+ " 7 | repo-debug-update-non-oss | Update Repository (Debug, Non-OSS) | No | ---- | ---- ", # noqa E501
+ " 8 | repo-non-oss | Non-OSS Repository | Yes | (r ) Yes | No ", # noqa E501
+ " 9 | repo-oss | Main Repository | Yes | (r ) Yes | No ", # noqa E501
+ "10 | repo-source | Source Repository | No | ---- | ---- ", # noqa E501
+ "11 | repo-source-non-oss | Source Repository (Non-OSS) | No | ---- | ---- ", # noqa E501
+ "12 | repo-update | Main Update Repository | Yes | (r ) Yes | No ", # noqa E501
+ "13 | repo-update-non-oss | Update Repository (Non-Oss) | Yes | (r ) Yes | No ", # noqa E501
+ "",
+ ]
+ )
+ err.append([""])
+ rc.append(0)
+ expected_results.append(
+ [
+ "Local-Repository",
+ "Rockstor-Testing",
+ "illuusio",
+ "repo-debug",
+ "repo-debug-non-oss",
+ "repo-debug-update",
+ "repo-debug-update-non-oss",
+ "repo-non-oss",
+ "repo-oss",
+ "repo-source",
+ "repo-source-non-oss",
+ "repo-update",
+ "repo-update-non-oss",
+ ]
+ )
+
+ for o, e, r, expected in zip(out, err, rc, expected_results):
+ self.mock_run_command.return_value = (o, e, r)
+ returned = zypper_repos_list()
+ self.assertEqual(
+ returned,
+ expected,
+ msg="Un-expected zypper_repos_list() result:\n "
+ "returned = ({}).\n "
+ "expected = ({}).".format(returned, expected),
+ )