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

v3: - rename API to swupdateprogress - use configobj instead of parsing manually v2: - rebased to master 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 src/wok/control/base.py | 18 ++++ src/wok/plugins/kimchi/control/host.py | 15 ++- src/wok/plugins/kimchi/docs/API.md | 3 + src/wok/plugins/kimchi/i18n.py | 1 + src/wok/plugins/kimchi/model/host.py | 16 +++ src/wok/plugins/kimchi/swupdate.py | 137 ++++++++++++++++++++++++ src/wok/plugins/kimchi/ui/js/src/kimchi.api.js | 39 +++++++ src/wok/plugins/kimchi/ui/js/src/kimchi.host.js | 29 +++++ src/wok/plugins/kimchi/ui/pages/i18n.json.tmpl | 2 + 9 files changed, 259 insertions(+), 1 deletion(-) -- 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> --- src/wok/plugins/kimchi/i18n.py | 1 + src/wok/plugins/kimchi/swupdate.py | 84 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/src/wok/plugins/kimchi/i18n.py b/src/wok/plugins/kimchi/i18n.py index ea325b8..416d951 100644 --- a/src/wok/plugins/kimchi/i18n.py +++ b/src/wok/plugins/kimchi/i18n.py @@ -270,6 +270,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/src/wok/plugins/kimchi/swupdate.py b/src/wok/plugins/kimchi/swupdate.py index b966424..73692d5 100644 --- a/src/wok/plugins/kimchi/swupdate.py +++ b/src/wok/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 os import signal import subprocess @@ -27,6 +28,8 @@ from wok.exception import NotFoundError, OperationFailed from wok.utils import run_command, wok_log from config import kimchiLock +from configobj import ConfigObj, ConfigObjError +from psutil import pid_exists from yumparser import get_yum_packages_list_update @@ -157,6 +160,23 @@ 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 + """ + yumcfg = None + try: + yumcfg = ConfigObj('/etc/yum.conf') + + except ConfigObjError: + return None + + if 'main' in yumcfg and 'logfile' in yumcfg['main']: + return yumcfg['main']['logfile'] + + return None def _refreshUpdateList(self): """ @@ -177,6 +197,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: @@ -185,6 +208,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): """ @@ -196,6 +238,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): """ @@ -218,6 +261,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() @@ -231,6 +277,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): """ @@ -242,6 +303,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): """ @@ -268,7 +330,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> --- src/wok/control/base.py | 18 ++++++++++++ src/wok/plugins/kimchi/control/host.py | 15 +++++++++- src/wok/plugins/kimchi/model/host.py | 16 ++++++++++ src/wok/plugins/kimchi/swupdate.py | 53 ++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 1 deletion(-) diff --git a/src/wok/control/base.py b/src/wok/control/base.py index 5c5c95f..e9ed3c8 100644 --- a/src/wok/control/base.py +++ b/src/wok/control/base.py @@ -226,6 +226,24 @@ class Resource(object): return {} +class AsyncResource(Resource): + """ + AsyncResource is a specialized Resource to handle async task. + """ + def __init__(self, model, ident=None): + super(AsyncResource, self).__init__(model, ident) + + def lookup(self): + try: + lookup = getattr(self.model, model_fn(self, 'lookup')) + self.info = lookup(*self.model_args) + except AttributeError: + self.info = {} + + cherrypy.response.status = 202 + return wok.template.render('Task', self.info) + + class Collection(object): """ A Collection is a container for Resource objects. To create a new diff --git a/src/wok/plugins/kimchi/control/host.py b/src/wok/plugins/kimchi/control/host.py index 0a40f1b..9fe4c0a 100644 --- a/src/wok/plugins/kimchi/control/host.py +++ b/src/wok/plugins/kimchi/control/host.py @@ -17,7 +17,8 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -from wok.control.base import Collection, Resource, SimpleCollection +from wok.control.base import AsyncResource, Collection +from wok.control.base import Resource, SimpleCollection from wok.control.utils import UrlSubNode from wok.exception import NotFoundError @@ -39,6 +40,7 @@ class Host(Resource): self.packagesupdate = PackagesUpdate(self.model) self.repositories = Repositories(self.model) self.swupdate = self.generate_action_handler_task('swupdate') + self.swupdateprogress = SoftwareUpdateProgress(self.model) self.cpuinfo = CPUInfo(self.model) @property @@ -46,6 +48,17 @@ class Host(Resource): return self.info +class SoftwareUpdateProgress(AsyncResource): + def __init__(self, model, id=None): + super(SoftwareUpdateProgress, self).__init__(model, id) + self.role_key = 'host' + self.admin_methods = ['GET'] + + @property + def data(self): + return self.info + + class HostStats(Resource): def __init__(self, model, id=None): super(HostStats, self).__init__(model, id) diff --git a/src/wok/plugins/kimchi/model/host.py b/src/wok/plugins/kimchi/model/host.py index f32cf62..9b1fc32 100644 --- a/src/wok/plugins/kimchi/model/host.py +++ b/src/wok/plugins/kimchi/model/host.py @@ -168,6 +168,22 @@ class HostModel(object): if (DOM_STATE_MAP[dom.info()[0]]) == state] +class SoftwareUpdateProgressModel(object): + def __init__(self, **kargs): + self.task = TaskModel(**kargs) + self.objstore = kargs['objstore'] + + def lookup(self, *name): + try: + swupdate = SoftwareUpdate() + except: + raise OperationFailed('KCHPKGUPD0004E') + + taskid = add_task('/plugins/kimchi/host/swupdateprogress', + swupdate.tailUpdateLogs, self.objstore, None) + return self.task.lookup(taskid) + + class HostStatsModel(object): __metaclass__ = Singleton diff --git a/src/wok/plugins/kimchi/swupdate.py b/src/wok/plugins/kimchi/swupdate.py index 73692d5..08f3b26 100644 --- a/src/wok/plugins/kimchi/swupdate.py +++ b/src/wok/plugins/kimchi/swupdate.py @@ -123,6 +123,59 @@ class SoftwareUpdate(object): os.setsid() signal.signal(signal.SIGTERM, signal.SIG_IGN) + 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> --- src/wok/plugins/kimchi/ui/js/src/kimchi.api.js | 39 +++++++++++++++++++++++++ src/wok/plugins/kimchi/ui/js/src/kimchi.host.js | 29 ++++++++++++++++++ src/wok/plugins/kimchi/ui/pages/i18n.json.tmpl | 2 ++ 3 files changed, 70 insertions(+) diff --git a/src/wok/plugins/kimchi/ui/js/src/kimchi.api.js b/src/wok/plugins/kimchi/ui/js/src/kimchi.api.js index c82d040..a16c95e 100644 --- a/src/wok/plugins/kimchi/ui/js/src/kimchi.api.js +++ b/src/wok/plugins/kimchi/ui/js/src/kimchi.api.js @@ -884,6 +884,45 @@ var kimchi = { }); }, + softwareUpdateProgress : 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/swupdateprogress', + type : "GET", + contentType : "application/json", + dataType : "json", + success : onResponse, + error : err + }); + }, + updateSoftware : function(suc, err, progress) { var taskID = -1; var onResponse = function(data) { diff --git a/src/wok/plugins/kimchi/ui/js/src/kimchi.host.js b/src/wok/plugins/kimchi/ui/js/src/kimchi.host.js index c3765dc..8d2517a 100644 --- a/src/wok/plugins/kimchi/ui/js/src/kimchi.host.js +++ b/src/wok/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.softwareUpdateProgress(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/src/wok/plugins/kimchi/ui/pages/i18n.json.tmpl b/src/wok/plugins/kimchi/ui/pages/i18n.json.tmpl index cd320e0..64045a9 100644 --- a/src/wok/plugins/kimchi/ui/pages/i18n.json.tmpl +++ b/src/wok/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> --- src/wok/plugins/kimchi/docs/API.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wok/plugins/kimchi/docs/API.md b/src/wok/plugins/kimchi/docs/API.md index e314a0f..ccc843f 100644 --- a/src/wok/plugins/kimchi/docs/API.md +++ b/src/wok/plugins/kimchi/docs/API.md @@ -827,6 +827,9 @@ 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 * +* swupdateprogress: Read the package manager logfile in background and return a + Task resource. + * task resource. * See Resource: Task * ### Resource: Users -- 1.9.1

Please, ignore this patch. I'm going to send the correct one. Thank you On 07-10-2015 13:15, Jose Ricardo Ziviani wrote:
v3: - rename API to swupdateprogress - use configobj instead of parsing manually
v2: - rebased to master
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
src/wok/control/base.py | 18 ++++ src/wok/plugins/kimchi/control/host.py | 15 ++- src/wok/plugins/kimchi/docs/API.md | 3 + src/wok/plugins/kimchi/i18n.py | 1 + src/wok/plugins/kimchi/model/host.py | 16 +++ src/wok/plugins/kimchi/swupdate.py | 137 ++++++++++++++++++++++++ src/wok/plugins/kimchi/ui/js/src/kimchi.api.js | 39 +++++++ src/wok/plugins/kimchi/ui/js/src/kimchi.host.js | 29 +++++ src/wok/plugins/kimchi/ui/pages/i18n.json.tmpl | 2 + 9 files changed, 259 insertions(+), 1 deletion(-)
-- Jose Ricardo Ziviani ----------------------------- Software Engineer Linux Technology Center - IBM
participants (1)
-
Jose Ricardo Ziviani