[PATCH 0/4] Issue 557 - Package Update Improvements - Part II

This patchset implements a simple monitor for package update. When the host tab is selected, it verifies whether a package manager is already running in the system. If so, the software update progress window, in the host tab, will display the contents of the package manager logfile. Thus, the user will know the progress of a package update even if it was not started up by Kimchi (or if wokd was restarted for any reason). Jose Ricardo Ziviani (4): Add functions for package manager monitoring Implement the package manager monitor backend Implement the package manager monitor frontend Update the documentation plugins/kimchi/control/host.py | 1 + plugins/kimchi/docs/API.md | 2 + plugins/kimchi/i18n.py | 1 + plugins/kimchi/model/host.py | 10 +++ plugins/kimchi/swupdate.py | 138 ++++++++++++++++++++++++++++++++ plugins/kimchi/ui/js/src/kimchi.api.js | 39 +++++++++ plugins/kimchi/ui/js/src/kimchi.host.js | 29 +++++++ plugins/kimchi/ui/pages/i18n.json.tmpl | 2 + 8 files changed, 222 insertions(+) -- 1.9.1

- Kimchi package manager classes are now able to know if there is already a package manager instance running in the system and where to find the default logfiles for those package managers. Signed-off-by: Jose Ricardo Ziviani <joserz@linux.vnet.ibm.com> --- plugins/kimchi/i18n.py | 1 + plugins/kimchi/swupdate.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/plugins/kimchi/i18n.py b/plugins/kimchi/i18n.py index 2274d6f..f89679f 100644 --- a/plugins/kimchi/i18n.py +++ b/plugins/kimchi/i18n.py @@ -272,6 +272,7 @@ messages = { "KCHPKGUPD0002E": _("Package %(name)s is not marked to be updated."), "KCHPKGUPD0003E": _("Error while getting packages marked to be updated. Details: %(err)s"), "KCHPKGUPD0004E": _("There is no compatible package manager for this system."), + "KCHPKGUPD0005E": _("There is a package manager instance running in the system."), "KCHUTILS0003E": _("Unable to choose a virtual machine name"), diff --git a/plugins/kimchi/swupdate.py b/plugins/kimchi/swupdate.py index 84b927f..4d92731 100644 --- a/plugins/kimchi/swupdate.py +++ b/plugins/kimchi/swupdate.py @@ -17,6 +17,7 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import fcntl import subprocess import time @@ -25,6 +26,7 @@ from wok.exception import NotFoundError, OperationFailed from wok.utils import run_command, wok_log from config import kimchiLock +from psutil import pid_exists from yumparser import get_yum_packages_list_update @@ -146,6 +148,24 @@ class YumUpdate(object): def __init__(self): self._pkgs = {} self.update_cmd = ["yum", "-y", "update"] + self.logfile = self._get_output_log() + + def _get_output_log(self): + """ + Return the logfile path + """ + try: + with open('/etc/yum.conf', 'r') as config: + for line in config: + if 'logfile' in line: + logfile = line.split('=')[1].strip() + if not logfile: + return None + + return logfile + + except (IndexError, IOError): + return None def _refreshUpdateList(self): """ @@ -166,6 +186,9 @@ class YumUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + if self.isRunning(): + raise OperationFailed('KCHPKGUPD0005E') + self._refreshUpdateList() pkg_list = [] for pkg in self._pkgs: @@ -174,6 +197,25 @@ class YumUpdate(object): pkg_list.append(package) return pkg_list + def isRunning(self): + """ + Return True whether the YUM package manager is already running or + False otherwise. + """ + try: + with open('/var/run/yum.pid', 'r') as pidfile: + pid = int(pidfile.read().rstrip('\n')) + + # cannot find pidfile, assumes yum is not running + except (IOError, ValueError): + return False + + # the pidfile exists and it lives in process table + if pid_exists(pid): + return True + + return False + class AptUpdate(object): """ @@ -185,6 +227,7 @@ class AptUpdate(object): self._pkgs = {} self.pkg_lock = getattr(__import__('apt_pkg'), 'SystemLock') self.update_cmd = ['apt-get', 'upgrade', '-y'] + self.logfile = '/var/log/apt/term.log' def _refreshUpdateList(self): """ @@ -207,6 +250,9 @@ class AptUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + if self.isRunning(): + raise OperationFailed('KCHPKGUPD0005E') + kimchiLock.acquire() self._refreshUpdateList() kimchiLock.release() @@ -220,6 +266,21 @@ class AptUpdate(object): return pkg_list + def isRunning(self): + """ + Return True whether the APT package manager is already running or + False otherwise. + """ + try: + with open('/var/lib/dpkg/lock', 'w') as lockfile: + fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) + + # cannot open dpkg lock file to write in exclusive mode means the + # apt is currently running + except IOError: + return True + + return False class ZypperUpdate(object): """ @@ -231,6 +292,7 @@ class ZypperUpdate(object): self._pkgs = {} self.update_cmd = ["zypper", "--non-interactive", "update", "--auto-agree-with-licenses"] + self.logfile = '/var/log/zypp/history' def _refreshUpdateList(self): """ @@ -257,7 +319,29 @@ class ZypperUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + if self.isRunning(): + raise OperationFailed('KCHPKGUPD0005E') + kimchiLock.acquire() self._refreshUpdateList() kimchiLock.release() return self._pkgs + + def isRunning(self): + """ + Return True whether the Zypper package manager is already running or + False otherwise. + """ + try: + with open('/var/run/zypp.pid', 'r') as pidfile: + pid = int(pidfile.read().rstrip('\n')) + + # cannot find pidfile, assumes yum is not running + except (IOError, ValueError): + return False + + # the pidfile exists and it lives in process table + if pid_exists(pid): + return True + + return False -- 1.9.1

- The client will be able to know if there is a package manager instance running and to follow the package manager logfiles to know what is happening within. Signed-off-by: Jose Ricardo Ziviani <joserz@linux.vnet.ibm.com> --- plugins/kimchi/control/host.py | 1 + plugins/kimchi/model/host.py | 10 ++++++++ plugins/kimchi/swupdate.py | 54 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/plugins/kimchi/control/host.py b/plugins/kimchi/control/host.py index 0a40f1b..6c2ed43 100644 --- a/plugins/kimchi/control/host.py +++ b/plugins/kimchi/control/host.py @@ -39,6 +39,7 @@ class Host(Resource): self.packagesupdate = PackagesUpdate(self.model) self.repositories = Repositories(self.model) self.swupdate = self.generate_action_handler_task('swupdate') + self.taillogs = self.generate_action_handler_task('taillogs') self.cpuinfo = CPUInfo(self.model) @property diff --git a/plugins/kimchi/model/host.py b/plugins/kimchi/model/host.py index 23fda3b..a159f0e 100644 --- a/plugins/kimchi/model/host.py +++ b/plugins/kimchi/model/host.py @@ -128,6 +128,16 @@ class HostModel(object): self.host_info['memory'] = psutil.virtual_memory().total return self.host_info + def taillogs(self, *name): + try: + swupdate = SoftwareUpdate() + except: + raise OperationFailed('KCHPKGUPD0004E') + + taskid = add_task('/plugins/kimchi/host/taillogs', + swupdate.tailUpdateLogs, self.objstore, None) + return self.task.lookup(taskid) + def swupdate(self, *name): try: swupdate = SoftwareUpdate() diff --git a/plugins/kimchi/swupdate.py b/plugins/kimchi/swupdate.py index 4d92731..6045467 100644 --- a/plugins/kimchi/swupdate.py +++ b/plugins/kimchi/swupdate.py @@ -18,6 +18,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import fcntl +import os import subprocess import time @@ -112,6 +113,59 @@ class SoftwareUpdate(object): self._scanUpdates() return self._num2update + def tailUpdateLogs(self, cb, params): + """ + When the package manager is already running (started outside kimchi or + if wokd is restarted) we can only know what's happening by reading the + logfiles. This method acts like a 'tail -f' on the default package + manager logfile. If the logfile is not found, a simple '*' is + displayed to track progress. This will be until the process finishes. + """ + if not self._pkg_mnger.isRunning(): + return + + fd = None + try: + fd = os.open(self._pkg_mnger.logfile, os.O_RDONLY) + + # cannot open logfile, print something to let users know that the + # system is being upgrading until the package manager finishes its + # job + except (TypeError, OSError): + msgs = [] + while self._pkg_mnger.isRunning(): + msgs.append('*') + cb(''.join(msgs)) + time.sleep(1) + msgs.append('\n') + cb(''.join(msgs), True) + return + + # go to the end of logfile and starts reading, if nothing is read or + # a pattern is not found in the message just wait and retry until + # the package manager finishes + os.lseek(fd, 0, os.SEEK_END) + msgs = [] + progress = [] + while True: + read = os.read(fd, 1024) + if not read: + if not self._pkg_mnger.isRunning(): + break + + if not msgs: + progress.append('*') + cb(''.join(progress)) + + time.sleep(1) + continue + + msgs.append(read) + cb(''.join(msgs)) + + os.close(fd) + return cb(''.join(msgs), True) + def doUpdate(self, cb, params): """ Execute the update -- 1.9.1

- Whenever Kimchi host tab is displayed this client will check if the package manager is running, if so the logfile will be displayed in the Package Update progress window. Signed-off-by: Jose Ricardo Ziviani <joserz@linux.vnet.ibm.com> --- plugins/kimchi/ui/js/src/kimchi.api.js | 39 +++++++++++++++++++++++++++++++++ plugins/kimchi/ui/js/src/kimchi.host.js | 29 ++++++++++++++++++++++++ plugins/kimchi/ui/pages/i18n.json.tmpl | 2 ++ 3 files changed, 70 insertions(+) diff --git a/plugins/kimchi/ui/js/src/kimchi.api.js b/plugins/kimchi/ui/js/src/kimchi.api.js index c82d040..f5839f1 100644 --- a/plugins/kimchi/ui/js/src/kimchi.api.js +++ b/plugins/kimchi/ui/js/src/kimchi.api.js @@ -884,6 +884,45 @@ var kimchi = { }); }, + tailSoftwareUpdateLogs : function(suc, err, progress) { + var taskID = -1; + var onResponse = function(data) { + taskID = data['id']; + trackTask(); + }; + + var trackTask = function() { + kimchi.getTask(taskID, onTaskResponse, err); + }; + + var onTaskResponse = function(result) { + var taskStatus = result['status']; + switch(taskStatus) { + case 'running': + progress && progress(result); + setTimeout(function() { + trackTask(); + }, 1000); + break; + case 'finished': + case 'failed': + suc(result); + break; + default: + break; + } + }; + + wok.requestJSON({ + url : 'plugins/kimchi/host/taillogs', + type : "POST", + contentType : "application/json", + dataType : "json", + success : onResponse, + error : err + }); + }, + updateSoftware : function(suc, err, progress) { var taskID = -1; var onResponse = function(data) { diff --git a/plugins/kimchi/ui/js/src/kimchi.host.js b/plugins/kimchi/ui/js/src/kimchi.host.js index c3765dc..c925fa1 100644 --- a/plugins/kimchi/ui/js/src/kimchi.host.js +++ b/plugins/kimchi/ui/js/src/kimchi.host.js @@ -258,6 +258,24 @@ kimchi.host_main = function() { }); }; + var startSoftwareUpdateProgress = function() { + var progressArea = $('#' + progressAreaID)[0]; + $('#software-updates-progress-container').removeClass('hidden'); + $(progressArea).text(''); + !wok.isElementInViewport(progressArea) && + progressArea.scrollIntoView(); + + kimchi.tailSoftwareUpdateLogs(function(result) { + reloadProgressArea(result); + wok.topic('kimchi/softwareUpdated').publish({ + result: result + }); + wok.message.warn(i18n['KCHUPD6010M']); + }, function(error) { + wok.message.error(i18n['KCHUPD6011M']); + }, reloadProgressArea); + }; + var listSoftwareUpdates = function(gridCallback) { kimchi.listSoftwareUpdates(function(softwareUpdates) { if($.isFunction(gridCallback)) { @@ -276,6 +294,17 @@ kimchi.host_main = function() { $(updateButton).prop('disabled', softwareUpdates.length === 0); }, function(error) { var message = error && error['responseJSON'] && error['responseJSON']['reason']; + + // cannot get the list of packages because there is another + // package manager instance running, so follow that instance updates + if (message.indexOf("KCHPKGUPD0005E") !== -1) { + startSoftwareUpdateProgress(); + if($.isFunction(gridCallback)) { + gridCallback([]); + } + return; + } + if($.isFunction(gridCallback)) { gridCallback([]); } diff --git a/plugins/kimchi/ui/pages/i18n.json.tmpl b/plugins/kimchi/ui/pages/i18n.json.tmpl index cd320e0..64045a9 100644 --- a/plugins/kimchi/ui/pages/i18n.json.tmpl +++ b/plugins/kimchi/ui/pages/i18n.json.tmpl @@ -103,6 +103,8 @@ "KCHUPD6007M": "$_("Updating...")", "KCHUPD6008M": "$_("Failed to retrieve packages update information.")", "KCHUPD6009M": "$_("Failed to update package(s).")", + "KCHUPD6010M": "$_("The package manager job is done.")", + "KCHUPD6011M": "$_("Failed to read the package manager logfile.")", "KCHDR6001M": "$_("Debug report will be removed permanently and can't be recovered. Do you want to continue?")", -- 1.9.1

Signed-off-by: Jose Ricardo Ziviani <joserz@linux.vnet.ibm.com> --- plugins/kimchi/docs/API.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/kimchi/docs/API.md b/plugins/kimchi/docs/API.md index e314a0f..81a4eb1 100644 --- a/plugins/kimchi/docs/API.md +++ b/plugins/kimchi/docs/API.md @@ -827,6 +827,8 @@ Contains information of host. Only allowed if there is not vm running. * swupdate: Start the update of packages in background and return a Task resource * task resource. * See Resource: Task * +* taillogs: Read the package manager logfile in background and return a Task resource + * task resource. * See Resource: Task * ### Resource: Users -- 1.9.1
participants (1)
-
Jose Ricardo Ziviani