smeserver-update/root/usr/lib/python3.6/site-packages/dnf-plugins/smeserver.py
Jean-Philippe Pialasse b3580a5bd4 * Wed Apr 17 2024 Jean-Philippe Pialasse <jpp@koozali.org> 11.0.0-9.sme
- fix RuntimeError: dictionary changed size during iteration [SME: 12633]
2024-04-17 11:42:12 -04:00

506 lines
23 KiB
Python

# 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-server','nut-driver','nut-monitor',
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
for key in list(actionlist):
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("==============================================================")