# vim: noexpandtab tabstop=4 import os import shutil import subprocess import syslog from dnfpluginscore import _, logger import libdnf.conf import dnf import dnf.transaction events_path = '/etc/e-smith/events' initialize_database = events_path + '/actions/initialize-default-databases' navigation_conf = events_path + '/actions/navigation-conf' systemctl = "/usr/bin/systemctl" signal_event = '/sbin/e-smith/signal-event' config_set = '/sbin/e-smith/config' status_file = '/var/cache/dnf/dnf.status' eventlist = dict() actionlist = dict() templateslist = dict() serviceslist = dict() # list of packages that need a reboot # if a package name starts with one these names, a reboot is needed rebootpkgs = ['daemontools', 'dbus', 'glibc', 'gnutls', 'kernel', 'linux-firmware', 'lvm2', 'mdadm', 'openssl-libs', 'systemd'] # exclusions : rebootpkgsexclude = dict() rebootpkgsexclude['dbus']='-python','-tests','-devel','-doc', rebootpkgsexclude['glibc']='-devel','-headers','-statics','-utils','-glib', rebootpkgsexclude['gnutls']='-devel', rebootpkgsexclude['kernel']='-doc','-debug','-devel','-abi-whitelists','tools', rebootpkgsexclude['lvm2']='-devel','-python', rebootpkgsexclude['systemd']='-devel','-python', # list of packages that need a service restart # if a package name starts with one of these values, a service restart is needed # since, for example, httpd means httpd-admin and httpd-e-smith, we won't use these names directly but via serviceslist (see below) restartpkgs = ['dovecot','bglibs','cvm','freeradius','httpd','iptables', 'mariadb' , 'nut', 'openldap', 'openssh', 'php', 'pptpd', 'proftp', 'samba', 'spamassassin', 'squid', 'qmail', 'qpsmtpd'] servicenames = dict() servicenames['bglibs']='cvm-unix', servicenames['cvm']='cvm-unix', servicenames['dovecot']='dovecot', servicenames['freeradius']='radiusd', servicenames['httpd']='httpd-admin','httpd-e-smith', servicenames['iptables']='masq', servicenames['mariadb']='mariadb', servicenames['nut']='nut', servicenames['openldap']='ldap', servicenames['openssh']='sshd', servicenames['php']='httpd-e-smith', 'php72-php-fpm', 'php73-php-fpm', 'php74-php-fpm', 'php80-php-fpm', 'php81-php-fpm', 'php82-php-fpm', 'php83-php-fpm', servicenames['proftp']='ftp', servicenames['samba']='smb', servicenames['spamassassin']='spamassassin', servicenames['squid']='squid', servicenames['qmail']='qmail', servicenames['qpsmtpd']='qpsmtpd', 'sqpsmtpd', DEBUG = False smechange = False smechangelist = dict() ourfile = False # not used erasing = False # not used smeEventPretrans = False removenorebootok = dict() class SMEServer(dnf.Plugin): name = "smeserver" def __init__(self, base, cli): super(SMEServer, self).__init__(base, cli) self.base = base self.logger = logger self.report_yum_status('init') def log(self,s): if DEBUG : print(s) def createevent(self): if os.path.isdir('/etc/e-smith/events/temp'): shutil.rmtree('/etc/e-smith/events/temp') os.makedirs('/etc/e-smith/events/temp/services2adjust', 0o777) os.makedirs('/etc/e-smith/events/temp/templates2expand', 0o777) def addtemplate2expand(self,template): #check filename head_tail = os.path.split(template) head = head_tail[0]; filename = head_tail[1]; # create dir if not os.path.isdir('/etc/e-smith/events/temp/templates2expand' + head): os.makedirs('/etc/e-smith/events/temp/templates2expand' + head, 0o777) #touch file fname= '/etc/e-smith/events/temp/templates2expand'+ template fhandle = open(fname, 'a') try: os.utime(fname, None) finally: fhandle.close() def report_yum_status(self,status): fileHandle = open(status_file, 'w') fileHandle.write(status) fileHandle.close() def pre_config(self): self.report_yum_status('pre_config') def config(self): self.report_yum_status('config') def sack(self): global ourfile ourfile = True self.report_yum_status('sack') def resolved(self): self.report_yum_status('resolved') #2 PKG_DOWNGRADE = dnf.transaction.PKG_DOWNGRADE # :api #1 PKG_INSTALL = dnf.transaction.PKG_INSTALL # :api #4 PKG_OBSOLETE = dnf.transaction.PKG_OBSOLETE # :api #9 PKG_REINSTALL = dnf.transaction.PKG_REINSTALL # :api #8 PKG_REMOVE = dnf.transaction.PKG_ERASE # :api #6 PKG_UPGRADE = dnf.transaction.PKG_UPGRADE # :api #7 PKG_UPGRADED = libdnf.transaction.TransactionItemAction_UPGRADED #101 PKG_CLEANUP = dnf.transaction.PKG_CLEANUP # :api #102 PKG_VERIFY = dnf.transaction.PKG_VERIFY # :api #103 PKG_SCRIPTLET = dnf.transaction.PKG_SCRIPTLET # :api def pre_transaction(self): self.report_yum_status('pretrans') # Prefetch filelist for packages to be removed, in_ts_items = [] out_ts_items = [] all_ts_items = [] for ts_item in self.base.transaction: if ts_item.action in dnf.transaction.FORWARD_ACTIONS: in_ts_items.append(ts_item) elif ts_item.action in dnf.transaction.BACKWARD_ACTIONS: out_ts_items.append(ts_item) else: # The action is not rpm change. It can be a reason change, therefore we can skip that item continue all_ts_items.append(ts_item) allpkgs = all_ts_items # [tsi.pkg for tsi in all_ts_items] # also to see dnf.db.group.RPMTransaction.install_set installs = in_ts_items #[tsi.pkg for tsi in in_ts_items] installs_dict = {} for item in installs: nam = item.name installs_dict[nam] = item # one liner new_dict = {item['name']:item for item in data} # also to see dnf.db.group.RPMTransaction.remove_set removes = out_ts_items #[tsi.pkg for tsi in out_ts_items] self.log('## allpkgs: ' + str([ str(tsi.pkg) for tsi in allpkgs])) self.log('## installs: ' +str([ str(tsi.pkg) for tsi in installs])) self.log('## Removes: ' + str([ str(tsi.pkg) for tsi in removes])) ##allpkgs: ['smeserver-tinydns-11.0.0-4.el8.sme.noarch', 'smeserver-tinydns-11.0.0-3.el8.sme.noarch'] ## install: ['smeserver-tinydns-11.0.0-4.el8.sme.noarch'] ## Remove: ['smeserver-tinydns-11.0.0-3.el8.sme.noarch'] # list of all instaled rpm base = dnf.Base() base.fill_sack() q = base.sack.query() rpmdb = q.installed() global smechange global eventlist global actionlist global templateslist global serviceslist global erasing global smeEventPretrans global removenorebootok # reinstall are not listed there so already skipped # downgrade are going there # update are going there, we filter them out # real removal are the real deal there ; downgrade are played like if they were removal for tsmem in removes: erasing = True n= str(tsmem.name) # if the pkg.name is present on both IN and OUT, then we assume update/downgrade and continue to transaction if n in installs_dict : #self.log('updating ==> state ' + str(tsmem.pkg) + " to " + str(installs_dict[n].pkg) ) continue self.log('**Package: ' + str(tsmem.pkg) + ' to be removed') smeevent = n + '-update' #check if /etc/e-smith/events/$smeevent exist, if yes add it to todo list if os.path.isdir(events_path + os.sep + smeevent): tmppath = events_path + os.sep + smeevent + os.sep eventlist[n] = smeevent # if templates to expand, add them if os.path.isdir(tmppath + "templates2expand" + os.sep): tmppathtmpl = tmppath + "templates2expand" + os.sep for subdir, dirs, files in os.walk(tmppathtmpl): for file in files: mytmptemplate = os.path.join(subdir, file).replace(tmppathtmpl,os.sep) if mytmptemplate not in templateslist : templateslist[mytmptemplate]=mytmptemplate self.log(" template " + mytmptemplate) # we add the actions scripts tmppathsrv = tmppath + os.sep files = [f for f in os.listdir(tmppathsrv)] for f in files: if os.path.islink(tmppathsrv + f): mytmpaction = os.path.realpath(os.readlink(tmppath + f).replace("..",events_path)) self.log(" link " + f + ": " + mytmpaction) actionlist[f]=mytmpaction # if there are services to handle we had them if os.path.isdir(tmppath + "services2adjust" + os.sep): tmppathsrv = tmppath + "services2adjust" + os.sep files = [f for f in os.listdir(tmppathsrv)] for f in files: if os.path.islink(tmppathsrv + f): self.log(" service link " + f + ": " + (os.readlink(tmppathsrv + f))) mytmpserv = os.readlink(tmppathsrv + f) # we had the service with its action, except if we need to force a restart # TODO as we have only pkgs to remove here, shouldn't we just stop the service NOW? if f not in serviceslist : serviceslist[f]=mytmpserv else: if mytmpserv == "restart": self.log("overriding all signals, forcing restart " + f ) serviceslist[f]="restart" removenorebootok[n]=1; else: # if we are here this is a smeserver pkg without an update event, needs reboot... if (n.startswith('smeserver') or n.startswith('e-smith')) and not n.startswith('smeserver-locale'): smechange = True smechangelist[n]=str(tsmem.pkg) self.log("smechange set to True because of " + n + " without an update event " + str(tsmem.pkg) ) #end of if smeserver-*-update event exist # do we need this for removal ? if "/etc/e-smith/web/panels/manager/cgi-bin" in tsmem.files : self.log(" ==> pannel detected : adding navigation-conf") actionlist['S80navigation-conf']="/etc/e-smith/events/actions/navigation-conf" #end of for list of packages #only for debug self.log('## smechange: '+str(smechange)) self.log('## eventlist: '+ str(eventlist)) self.log('## templateslist: '+str(templateslist.keys())) self.log('## serviceslist: '+str(serviceslist.items())) self.log('## actions ALL: ' + str(actionlist.keys())) self.log('## smechangelist: ' + str(smechangelist.keys())) if len(serviceslist)>0: # would it be a good idea to do some action/event there to stop service being removed ? # for the moment we do not, logic : if template still there at posttrans, we expand it after removal # probably the actions in some situations could be done before... # create an empty temp event print("Creating temporary event 'temp' and populating it...") self.createevent() print(" Adding services to adjust") for kservices in serviceslist: self.log(" " + kservices + ": " + serviceslist[kservices]) os.symlink(serviceslist[kservices], '/etc/e-smith/events/temp/services2adjust/' + kservices) # execute the event ; should we really wait ?? print("Executing signal-event temp before uninstalling ...........") os.spawnl(os.P_WAIT,signal_event,signal_event, 'temp') smeEventPretrans = True; def transaction(self): self.report_yum_status('transaction') #ts = self.getTsInfo() #rpmdb = self.getRpmDB() # Prefetch filelist for packages to be removed, in_ts_items = [] out_ts_items = [] all_ts_items = [] for ts_item in self.base.transaction: if ts_item.action in dnf.transaction.FORWARD_ACTIONS: in_ts_items.append(ts_item) elif ts_item.action in dnf.transaction.BACKWARD_ACTIONS: out_ts_items.append(ts_item) else: # The action is not rpm change. It can be a reason change, therefore we can skip that item continue all_ts_items.append(ts_item) allpkgs = all_ts_items # [tsi.pkg for tsi in all_ts_items] # also to see dnf.db.group.RPMTransaction.install_set installs = in_ts_items #[tsi.pkg for tsi in in_ts_items] installs_dict = {} for item in installs: nam = item.name installs_dict[nam] = item # one liner new_dict = {item['name']:item for item in data} # also to see dnf.db.group.RPMTransaction.remove_set removes = out_ts_items #[tsi.pkg for tsi in out_ts_items] global smechange global eventlist global actionlist global templateslist global serviceslist global erasing global smeEventPretrans global removenorebootok global ourfile for tsmem in installs: # n: name ; a : arch; e: epoch ; v: version, r: release n = tsmem.name a = tsmem.arch e = tsmem.epoch v = tsmem.version r = tsmem.release # if we're upgrading/installing/removing a rebootpkgs package.. a reboot is required for pkg in rebootpkgs: if n.startswith(pkg): self.log('**Package: ' + n + ' triggers reboot') # we do some exclusions -devel -doc ... if pkg in rebootpkgsexclude: cont=False for name in rebootpkgsexclude[pkg]: if n.startswith(pkg + name): cont=True if cont: # this is an exception we do not need reboot : -devel, -doc ... continue # either no exception or does not fit exceptions: we need reboot smechange = True smechangelist[n]=str(n) + "-" + str(v) + "-" + str(r) self.log("smechange set to True because of " + n + " with " + str(v) + "-" + str(r) ) #self.log("smechange set to True because of " + pkg) # check if we're upgrading a restartpkgs rpm for pkg in restartpkgs: if n.startswith(pkg): if pkg in servicenames: for name in servicenames[pkg]: if name not in serviceslist : serviceslist[name] = 'restart' else: if pkg not in serviceslist : serviceslist[pkg] = 'restart' #if smechange is true we can ignore the following part # wondering if we reallly do want to ignore that : let's say we want to delay kernel reboot, # could we at least configure httpd cleanly until next week ? # also when updating a package the erasing flag was on and prevented to do this .... # if not smechange and not erasing: #if True: smeevent = n + '-update' if os.path.isdir(events_path + os.sep + smeevent): tmppath = events_path + os.sep + smeevent + os.sep eventlist[n] = smeevent files = [f for f in os.listdir(tmppath)] for f in files: if os.path.islink(tmppath + f): mytmpaction = os.path.realpath(os.readlink(tmppath + f).replace("..",events_path)) #self.log(" link " + f + ": " + mytmpaction) actionlist[f]=mytmpaction if os.path.isdir(tmppath + "templates2expand" + os.sep): tmppathtmpl = tmppath + "templates2expand" + os.sep for subdir, dirs, files in os.walk(tmppathtmpl): for file in files: mytmptemplate = os.path.join(subdir, file).replace(tmppathtmpl,os.sep) if mytmptemplate not in templateslist : templateslist[mytmptemplate]=mytmptemplate self.log(" template " + mytmptemplate) if os.path.isdir(tmppath + "services2adjust" + os.sep): tmppathsrv = tmppath + "services2adjust" + os.sep files = [f for f in os.listdir(tmppathsrv)] for f in files: if os.path.islink(tmppathsrv + f): self.log(" service link " + f + ": " + (os.readlink(tmppathsrv + f))) mytmpserv = os.readlink(tmppathsrv + f) if f not in serviceslist : serviceslist[f]=mytmpserv else: if mytmpserv == "restart": #only for debug self.log("overriding all signals, forcing restart " + f) serviceslist[f]="restart" else: if (n.startswith('smeserver') or n.startswith('e-smith')) and not n.startswith('smeserver-locale') and not (n in removenorebootok): smechange = True smechangelist[n]=str(tsmem.name) + "-" + str(tsmem.version) + "-" + str(tsmem.release) self.log("smechange set to True because of " + n ) # as long as it is a sme pkg we need to rebuild panel if "/etc/e-smith/web/panels/manager/cgi-bin" in tsmem.files : self.log(" ==> pannel detected : adding navigation-conf") actionlist['S80navigation-conf']="/etc/e-smith/events/actions/navigation-conf" # end of for installed list #only for debug self.log('## smechange: '+str(smechange)) self.log('## eventlist: '+ str(eventlist)) self.log('## templateslist: '+str(templateslist.keys())) self.log('## serviceslist: '+str(serviceslist.items())) self.log('## actions ALL: ' + str(actionlist.keys())) self.log('## smechangelist: ' + str(smechangelist.keys())) # check if smechange is true or if eventlist has items.. in both cases we must initialize databases if smechange == True or len(eventlist)>0: print('Initializing databases') os.spawnl(os.P_WAIT, initialize_database, initialize_database) # just in case we add systemd-default and systemd-reload if 'S88systemd-default' not in actionlist : actionlist['S88systemd-default'] = '/etc/e-smith/events/actions/systemd-default' if 'S89systemd-reload' not in actionlist : actionlist['S89systemd-reload'] = '/etc/e-smith/events/actions/systemd-reload' # here we would like to remove duplicates # those are potential : S??navigation-conf S??systemd-reload S??systemd-default for act in ('navigation-conf','systemd-reload','systemd-default'): global iter iter = 0 tmpactionlist = actionlist for key in list(tmpactionlist): if key.endswith(act): iter += 1 if iter>1: del actionlist[key] self.log("removing duplicate action " + key + " iter " + str(iter)) self.report_yum_status('transaction') self.log("smeserver.py: transaction") # now, if smechange is false (no reboot needed), we can execute actions, expand templates and adjust services # create an empty temp event print("Creating temporary event 'temp' and populating it...") self.createevent() # fill it with our list of actions if len(actionlist)>0: print(" Adding actions to execute") for kactions in actionlist: if os.path.isfile(actionlist[kactions]): self.log(" " + kactions) os.symlink(actionlist[kactions],'/etc/e-smith/events/temp/' + kactions) # fill it with our list of templates to expand if len(templateslist)>0: print(" Adding templates to expand") for ktemplates in templateslist: mytemplatedir = '/etc/e-smith/templates/' + ktemplates mytemplatemeta = '/etc/e-smith/templates.metadata/' + ktemplates if os.path.exists(mytemplatedir) or os.path.exists(mytemplatemeta): self.log(" " + ktemplates) self.addtemplate2expand(ktemplates) if len(serviceslist)>0: print(" Adding services to adjust") for kservices in serviceslist: self.log(" " + kservices + ": " + serviceslist[kservices]) os.symlink(serviceslist[kservices], '/etc/e-smith/events/temp/services2adjust/' + kservices) # execute the event ; should we really wait ?? print("Executing signal-event temp ...........") os.spawnl(os.P_WAIT,signal_event,signal_event, 'temp') if smechange: os.spawnl(os.P_WAIT, config_set, config_set, 'set', 'UnsavedChanges', 'yes') os.spawnl(os.P_WAIT, navigation_conf, navigation_conf) syslog.syslog('Needs signal-event post-upgrade; signal-event reboot because of: ' + str(smechangelist.keys()) ) print("Reload dnf db for server-manager") os.spawnl(os.P_WAIT, systemctl, systemctl, 'restart', 'dnf') #no more close hook #def close_hook(conduit): if ourfile and os.path.isfile(status_file): os.unlink(status_file) if smechange: print("\nThe following updates require a server reboot:\n" + str(smechangelist.keys())) print("\n==============================================================\n") print("WARNING: You now need to run BOTH of the following commands") print("to ensure consistent system state:") print("signal-event post-upgrade; signal-event reboot") print("You should run these commands unless you are certain that") print("dnf made no changes to your system.\n") print("==============================================================")