for fedora23 nodes - yet again
[nodeupdate.git] / NodeUpdate.py
old mode 100644 (file)
new mode 100755 (executable)
index c6be451..fccfcb3
@@ -1,8 +1,12 @@
 #!/usr/bin/python2
 
-import sys, os
-from random import Random
+from __future__ import print_function
+
+import sys
+import os
+import os.path
 import string
+from random import Random
 from types import StringTypes
 
 from time import strftime
@@ -18,6 +22,8 @@ TARGET_SHELL = '/bin/bash'
 CRON_FILE = '/etc/cron.d/NodeUpdate.cron'
 
 YUM_PATH = "/usr/bin/yum"
+DNF_PATH = "/usr/bin/dnf"
+HAS_DNF = os.path.exists(DNF_PATH)
 
 RPM_PATH = "/bin/rpm"
 
@@ -52,25 +58,102 @@ DELETE_RPM_LIST_FILE= '/etc/planetlab/delete-rpm-list'
 # so, we force the update of crucial pkgs independently, as 
 # the whole group is sometimes too much to swallow 
 # this one is builtin
-UPDATE_PACKAGES_BUILTIN=[ 'NodeManager' ]
+CRUCIAL_PACKAGES_BUILTIN=[
+    'NodeUpdate',
+    'nodemanager-lib',
+    'nodemanager-lxc',
+    'nodemanager-vs',
+]
 # and operations can also try to push a list through a conf_file
-UPDATE_PACKAGES_OPTIONAL_PATH='/etc/planetlab/NodeUpdate.packages'
-
+# should use the second one for consistency, try the first one as well for legacy
+CRUCIAL_PACKAGES_OPTIONAL_PATHS = [
+    # this is a legacy name, please avoid using this one
+    '/etc/planetlab/NodeUpdate.packages',
+    # file to use with a hand-written conf_file
+    '/etc/planetlab/crucial-rpm-list',
+    # this one could some day be maintained by a predefined config_file
+    '/etc/planetlab/sliceimage-rpm-list',
+]
 
 # print out a message only if we are displaying output
-def Message(message):
+def Message(*messages):
     if displayOutput:
-        if isinstance(message,StringTypes) and len(message) >=2 and message[0]=="\n":
-            print "\n",
-            message=message[1:]
-        print strftime(TIMEFORMAT),
-        print message
+        print(5*'*', strftime(TIMEFORMAT), *messages)
 
 # always print errors
-def Error(Str):
-    print strftime(TIMEFORMAT),
-    print Str
+def Error(*messages):
+    print(10*'!', strftime(TIMEFORMAT),*messages)
+
+class YumDnf:
+    def __init__(self):
+        command = DNF_PATH if HAS_DNF else YUM_PATH
+        self.command = command
+
+        options = ""
+        # --verbose option
+        Message("Checking if {} supports --verbose".format(command))
+        if os.system("{} --help | grep -q verbose".format(command)) == 0:
+            Message("It does, using --verbose option")
+            options += " --verbose"
+        else:
+            Message("Unsupported, not using --verbose option")
+        # --sslcertdir option
+        Message("Checking if {} supports SSL certificate checks"
+                .format(command))
+        if os.system("{} --help | grep -q sslcertdir".format(command)) == 0:
+            Message("It does, using --sslcertdir option")
+            sslcertdir = "--sslcertdir=" + SSL_CERT_DIR
+        else:
+            Message("Unsupported, not using --sslcertdir option")
+            sslcertdir = ""
+        self.options = options
 
+    ########## one individual package
+    def handle_package(self, package):
+        if not self.is_packaged_installed(package):
+            return self.do_package(package, "install")
+        else:
+            return self.do_package(package, "update")
+
+    def is_packaged_installed(self, package):
+        cmd = "rpm -q {} > /dev/null".format(package)
+        return os.system(cmd) == 0
+
+    def do_package(self, package, subcommand):
+        cmd = \
+            "{} {} -y {} {}".format(self.command, self.options,
+                                    subcommand, package)
+        Message("Invoking {}".format(cmd))
+        return os.system(cmd) == 0
+
+    ########## update one group
+    def update_group(self, group):
+        # it is important to invoke dnf group *upgrade* and not *update*
+        # because the semantics of groups has changed within dnf
+        overall = True
+        if HAS_DNF:
+            cmd = "{} {} -y group install {}".format(self.command, self.options, group)
+            Message("Invoking {}".format(cmd))
+            if os.system(cmd) != 0: overall = False
+            cmd = "{} {} -y group upgrade {}".format(self.command, self.options, group)
+            Message("Invoking {}".format(cmd))
+            if os.system(cmd) != 0: overall = False
+        else:
+            cmd = "{} {} -y groupinstall {}".format(self.command, self.options, group)
+            Message("Invoking {}".format(cmd))
+            if os.system(cmd) != 0: overall = False
+        return overall
+
+    ########## update the whole system
+    def update_system(self):
+        cmd = "{} {} -y update".format(self.command, self.options)
+        Message("Invoking {}".format(cmd))
+        return os.system(cmd) == 0
+
+    def clean_all(self):
+        cmd = "{} clean all".format(self.command)
+        Message("Invoking {}".format(cmd))
+        return os.system(cmd) == 0
 
 # create an entry in /etc/cron.d so we run periodically.
 # we will be run once a day at a 0-59 minute random offset
@@ -78,178 +161,160 @@ def Error(Str):
 def UpdateCronFile():
     try:
         
-        randomMinute= Random().randrange( 0, 59, 1 );
-        randomHour= Random().randrange( 0, 11, 1 );
+        randomMinute= Random().randrange(0, 59, 1);
+        randomHour= Random().randrange(0, 11, 1);
         
-        f = open( CRON_FILE, 'w' );
-        f.write( "# %s\n" % (TARGET_DESC) );
+        f = open(CRON_FILE, 'w');
+        f.write("# {}\n".format(TARGET_DESC));
         ### xxx is root aliased to the support mailing list ?
-        f.write( "MAILTO=%s\n" % (TARGET_USER) );
-        f.write( "SHELL=%s\n" % (TARGET_SHELL) );
-        f.write( "%s %s,%s * * * %s %s\n\n" %
-                 (randomMinute, randomHour, randomHour + 12, TARGET_USER, TARGET_SCRIPT) );
+        f.write("MAILTO={}\n".format(TARGET_USER));
+        f.write("SHELL={}\n".format(TARGET_SHELL));
+        f.write("{} {},{} * * * {} {}\n\n"
+                .format (randomMinute, randomHour,
+                         randomHour + 12, TARGET_USER, TARGET_SCRIPT));
         f.close()
     
-        print( "Created new cron.d entry." )
+        print("Created new cron.d entry.")
     except:
-        print( "Unable to create cron.d entry." )
+        print("Unable to create cron.d entry.")
 
 
 # simply remove the cron file we created
 def RemoveCronFile():
     try:
-        os.unlink( CRON_FILE )
-        print( "Deleted cron.d entry." )
+        os.unlink(CRON_FILE)
+        print("Deleted cron.d entry.")
     except:
-        print( "Unable to delete cron.d entry." )
+        print("Unable to delete cron.d entry.")
         
 
 
 class NodeUpdate:
 
-    def __init__( self, doReboot ):
+    def __init__(self, doReboot):
         if self.CheckProxy():
             os.environ['http_proxy']= self.HTTP_PROXY
             os.environ['HTTP_PROXY']= self.HTTP_PROXY
-            
         self.doReboot= doReboot
 
-    
-
-    def CheckProxy( self ):
-        Message( "Checking existence of proxy config file..." )
-        
+    def CheckProxy(self):
+        Message("Checking existence of proxy config file...")
         if os.access(PROXY_FILE, os.R_OK) and os.path.isfile(PROXY_FILE):
             self.HTTP_PROXY= string.strip(file(PROXY_FILE,'r').readline())
-            Message( "Using proxy %s." % self.HTTP_PROXY )
+            Message("Using proxy {}".format(self.HTTP_PROXY))
             return 1
         else:
-            Message( "Not using any proxy." )
+            Message("Not using any proxy.")
             return 0
 
+    def InstallKeys(self):
+        Message("Removing any existing GPG signing keys from the RPM database")
+        os.system("{} --allmatches -e gpg-pubkey".format(RPM_PATH))
+        Message("Installing all GPG signing keys in {}".format(RPM_GPG_PATH))
+        os.system("{} --import {}/*".format(RPM_PATH, RPM_GPG_PATH))
 
-    def InstallKeys( self ):
-        Message( "\nRemoving any existing GPG signing keys from the RPM database" )
-        os.system( "%s --allmatches -e gpg-pubkey" % RPM_PATH )
-
-        Message( "\nInstalling all GPG signing keys in %s" % RPM_GPG_PATH )
-        os.system( "%s --import %s/*" % (RPM_PATH, RPM_GPG_PATH) )
+    def ClearRebootFlag(self):
+        os.system("/bin/rm -rf {}".format(REBOOT_FLAG))
 
-
-    def ClearRebootFlag( self ):
-        os.system( "/bin/rm -rf %s" % REBOOT_FLAG )
-
-
-    def CheckForUpdates( self ):
-
-        Message( "\nRemoving any existing reboot flags" )
+    def CheckForUpdates(self):
+        Message("Removing any existing reboot flags")
         self.ClearRebootFlag()
-
         if self.doReboot == 0:
-            Message( "\nIgnoring any reboot flags set by RPMs" );
-
-        Message( "\nChecking if yum supports SSL certificate checks" )
-        if os.system( "%s --help | grep -q sslcertdir" % YUM_PATH ) == 0:
-            Message( "It does, using --sslcertdir option" )
-            sslcertdir = "--sslcertdir=" + SSL_CERT_DIR
-        else:
-            Message( "Unsupported, not using --sslcertdir option" )
-            sslcertdir = ""
+            Message("Ignoring any reboot flags set by RPMs");
                     
-        yum_options=""
-        Message( "\nChecking if yum supports --verbose" )
-        if os.system( "%s --help | grep -q verbose" % YUM_PATH ) == 0:
-            Message( "It does, using --verbose option" )
-            yum_options += " --verbose"
-        else:
-            Message( "Unsupported, not using --verbose option" )
-        
+        yum_dnf = YumDnf()
+
+        # this of course is quite suboptimal, but proved to be safer
+        yum_dnf.clean_all()
+
         # a configurable list of packages to try and update independently
         # cautious..
         try:
             crucial_packages = []
-            for package in UPDATE_PACKAGES_BUILTIN: crucial_packages.append(package)
-            try: crucial_packages += file(UPDATE_PACKAGES_OPTIONAL_PATH).read().split()
-            except: pass
+            for package in CRUCIAL_PACKAGES_BUILTIN:
+                crucial_packages.append(package)
+            for path in CRUCIAL_PACKAGES_OPTIONAL_PATHS:
+                try:
+                    crucial_packages += file(path).read().split()
+                except:
+                    pass
+            Message("List of crucial packages: {}".format(crucial_packages))
             for package in crucial_packages:
-                Message( "\nUpdating crucial package %s" % package)
-                os.system( "%s %s -y update %s" %(YUM_PATH, yum_options, package))
+                yum_dnf.handle_package(package)
         except:
+            
             pass
 
-        Message( "\nUpdating PlanetLab group" )
-        os.system( "%s %s %s -y groupinstall \"PlanetLab\"" %
-                   (YUM_PATH, yum_options, sslcertdir) )
+        Message("Updating PlanetLab group")
+        yum_dnf.update_group("PlanetLab")
 
-        Message( "\nUpdating rest of system" )
-        os.system( "%s %s %s -y update" %
-                   (YUM_PATH, yum_options, sslcertdir) )
+        Message("Updating rest of system")
+        yum_dnf.update_system()
 
-        Message( "\nChecking for extra groups (extensions) to update" )
+        Message("Checking for extra groups (extensions) to update")
         if os.access(EXTENSIONS_FILE, os.R_OK) and \
            os.path.isfile(EXTENSIONS_FILE):
             extensions_contents= file(EXTENSIONS_FILE).read()
             extensions_contents= string.strip(extensions_contents)
             if extensions_contents == "":
-                Message( "No extra groups found in file." )
+                Message("No extra groups found in file.")
             else:
                 extensions_contents.strip()
                 for extension in extensions_contents.split():
-                    group = "extension%s" % extension
-                    Message( "\nUpdating %s group" % group )
-                    os.system( "%s %s %s -y groupinstall \"%s\"" %
-                               (YUM_PATH, yum_options, sslcertdir, group) )
+                    group = "extension{}".format(extension)
+                    yum_dnf.update_group(group)
         else:
-            Message( "No extensions file found" )
+            Message("No extensions file found")
             
         if os.access(REBOOT_FLAG, os.R_OK) and os.path.isfile(REBOOT_FLAG) and self.doReboot:
-            Message( "\nAt least one update requested the system be rebooted" )
+            Message("At least one update requested the system be rebooted")
             self.ClearRebootFlag()
-            os.system( "/sbin/shutdown -r now" )
-
-    def RebuildRPMdb( self ):
-        Message( "\nRebuilding RPM Database." )
-        try: os.system( "rm /var/lib/rpm/__db.*" )
-        except Exception, err: print "RebuildRPMdb: %s" % err
-        try: os.system( "%s --rebuilddb" % RPM_PATH )
-        except Exception, err: print "RebuildRPMdb: %s" % err
+            os.system("/sbin/shutdown -r now")
 
-    def YumCleanAll ( self ):
-        Message ("\nCleaning all yum cache (yum clean all)")
+    def RebuildRPMdb(self):
+        Message("Rebuilding RPM Database.")
         try:
-            os.system( "yum clean all")
-        except:
-            pass
-
-    def RemoveRPMS( self ):
+            os.system("rm /var/lib/rpm/__db.*")
+        except Exception as err:
+            print("RebuildRPMdb: exception {}".format(err))
+        try:
+            os.system("{} --rebuilddb".format(RPM_PATH))
+        except Exception as err:
+            print("RebuildRPMdb: exception {}".format(err))
 
-        Message( "\nLooking for RPMs to be deleted." )
+    def RemoveRPMS(self):
+        Message("Looking for RPMs to be deleted.")
         if os.access(DELETE_RPM_LIST_FILE, os.R_OK) and \
            os.path.isfile(DELETE_RPM_LIST_FILE):
-            rpm_list_contents= file(DELETE_RPM_LIST_FILE).read()
-            rpm_list_contents= string.strip(rpm_list_contents)
+            rpm_list_contents= file(DELETE_RPM_LIST_FILE).read().strip()
 
             if rpm_list_contents == "":
-                Message( "No RPMs listed in file to delete." )
+                Message("No RPMs listed in file to delete.")
                 return
 
-            rpm_list= string.join(string.split(rpm_list_contents))
-            
-            Message( "Deleting these RPMs:" )
-            Message( rpm_list_contents )
+            rpm_list= string.split(rpm_list_contents)
             
-            rc= os.system( "%s -ev %s" % (RPM_PATH, rpm_list) )
-
-            if rc != 0:
-                Error( "Unable to delete RPMs, continuing. rc=%d" % rc )
-            else:
-                Message( "RPMs deleted successfully." )
+            Message("Deleting RPMs from {}: {}".format(DELETE_RPM_LIST_FILE," ".join(rpm_list)))
+
+            # invoke them separately as otherwise one faulty (e.g. already uninstalled)
+            # would prevent the other ones from uninstalling
+            for rpm in rpm_list:
+                # is it installed
+                is_installed = os.system ("{} -q {}".format(RPM_PATH, rpm)) == 0
+                if not is_installed:
+                    Message ("Ignoring rpm {} marked to delete, already uninstalled".format(rpm))
+                    continue
+                uninstalled = os.system("{} -ev {}".format(RPM_PATH, rpm)) == 0
+                if uninstalled:
+                    Message ("Successfully removed RPM {}".format(rpm))
+                    continue
+                else:
+                    Error("Unable to delete RPM {}, continuing. rc={}".format(rpm, uninstalled))
             
         else:
-            Message( "No RPMs list file found." )
-
-
+            Message("No RPMs list file found.")
 
+##############################
 if __name__ == "__main__":
 
     # if we are invoked with 'start', display the output. 
@@ -258,20 +323,20 @@ if __name__ == "__main__":
     # so the cron only outputs errors and doesn't
     # generate mail when it works correctly
 
-    displayOutput= 0
+    displayOutput = 0
 
     # if we hit an rpm that requests a reboot, do it if this is
     # set to 1. can be turned off by adding noreboot to command line
     # option
     
-    doReboot= 1
+    doReboot = 1
 
     if "start" in sys.argv or "display" in sys.argv:
-        displayOutput= 1
+        displayOutput = 1
         Message ("\nTurning on messages")
 
     if "noreboot" in sys.argv:
-        doReboot= 0
+        doReboot = 0
 
     if "updatecron" in sys.argv:
         # simply update the /etc/cron.d file for us, and exit
@@ -291,27 +356,25 @@ if __name__ == "__main__":
     if os.access(NODEUPDATE_PID_FILE, os.R_OK):
         pid= string.strip(file(NODEUPDATE_PID_FILE).readline())
         if pid <> "":
-            if os.system("/bin/kill -0 %s > /dev/null 2>&1" % pid) == 0:
-                Message( "It appears we are already running, exiting." )
+            if os.system("/bin/kill -0 {} > /dev/null 2>&1".format(pid)) == 0:
+                Message("It appears we are already running, exiting.")
                 sys.exit(1)
                     
     # write out our process id
-    pidfile= file( NODEUPDATE_PID_FILE, 'w' )
-    pidfile.write( "%d\n" % os.getpid() )
+    pidfile = file(NODEUPDATE_PID_FILE, 'w')
+    pidfile.write("{}\n".format(os.getpid()))
     pidfile.close()
 
     
-    nodeupdate= NodeUpdate(doReboot)
+    nodeupdate = NodeUpdate(doReboot)
     if not nodeupdate:
-        Error( "Unable to initialize." )
+        Error("Unable to initialize.")
     else:
         nodeupdate.RebuildRPMdb()
         nodeupdate.RemoveRPMS()
         nodeupdate.InstallKeys()
-        nodeupdate.YumCleanAll()
         nodeupdate.CheckForUpdates()
-        Message( "\nUpdate complete." )
+        Message("Update complete.")
 
     # remove the PID file
-    os.unlink( NODEUPDATE_PID_FILE )
-
+    os.unlink(NODEUPDATE_PID_FILE)