7 from random import Random
8 from types import StringTypes
10 from time import strftime
11 TIMEFORMAT = "%Y-%m-%d %H:%M:%S"
13 NODEUPDATE_PID_FILE = "/var/run/NodeUpdate.pid"
15 # variables for cron file creation
16 TARGET_SCRIPT = '(echo && date && echo && /usr/bin/NodeUpdate.py start) >> /var/log/NodeUpdate.log 2>&1'
17 TARGET_DESC = 'Update node RPMs periodically'
19 TARGET_SHELL = '/bin/bash'
20 CRON_FILE = '/etc/cron.d/NodeUpdate.cron'
22 YUM_PATH = "/usr/bin/yum"
23 DNF_PATH = "/usr/bin/dnf"
24 HAS_DNF = os.path.exists(DNF_PATH)
28 RPM_GPG_PATH = "/etc/pki/rpm-gpg"
30 # location of file containing http/https proxy info, if needed
31 PROXY_FILE = '/etc/planetlab/http_proxy'
33 # this is the flag that indicates an update needs to restart
34 # the system to take effect. it is created by the rpm that requested
36 REBOOT_FLAG = '/etc/planetlab/update-reboot'
38 # location of directory containing boot server ssl certs
39 SSL_CERT_DIR = '/mnt/cdrom/bootme/cacert/'
41 # file containing the list of extensions this node has, each
42 # correspond to a package group in yum repository.
43 # this is expected to be updated from the 'extensions tag'
44 # through the 'extensions.php' nodeconfig script
45 EXTENSIONS_FILE = '/etc/planetlab/extensions'
47 # file containing a list of rpms that we should attempt to delete
48 # before updating everything else. This list is not removed with
49 # 'yum remove', because that could accidently remove dependency rpms
50 # that were not intended to be deleted.
51 DELETE_RPM_LIST_FILE = '/etc/planetlab/delete-rpm-list'
53 # ok, so the logic should be simple, just yum update the world
54 # however there are many cases in the real life where this
55 # just does not work, because of a glitch somewhere
56 # so, we force the update of crucial pkgs independently, as
57 # the whole group is sometimes too much to swallow
59 CRUCIAL_PACKAGES_BUILTIN = [
65 # and operations can also try to push a list through a conf_file
66 # should use the second one for consistency, try the first one as well for legacy
67 CRUCIAL_PACKAGES_OPTIONAL_PATHS = [
68 # this is a legacy name, please avoid using this one
69 '/etc/planetlab/NodeUpdate.packages',
70 # file to use with a hand-written conf_file
71 '/etc/planetlab/crucial-rpm-list',
72 # this one could some day be maintained by a predefined config_file
73 '/etc/planetlab/sliceimage-rpm-list',
76 # print out a message only if we are displaying output
79 def Message(*messages):
81 print(5*'*', strftime(TIMEFORMAT), *messages)
87 print(10*'!', strftime(TIMEFORMAT), *messages)
92 command = DNF_PATH if HAS_DNF else YUM_PATH
93 self.command = command
97 Message("Checking if {} supports --verbose".format(command))
98 if os.system("{} --help | grep -q verbose".format(command)) == 0:
99 Message("It does, using --verbose option")
100 options += " --verbose"
102 Message("Unsupported, not using --verbose option")
103 # --sslcertdir option
104 Message("Checking if {} supports SSL certificate checks"
106 if os.system("{} --help | grep -q sslcertdir".format(command)) == 0:
107 Message("It does, using --sslcertdir option")
108 sslcertdir = "--sslcertdir=" + SSL_CERT_DIR
110 Message("Unsupported, not using --sslcertdir option")
112 self.options = options
114 # one individual package
115 def handle_package(self, package):
116 if not self.is_packaged_installed(package):
117 return self.do_package(package, "install")
119 return self.do_package(package, "update")
121 def is_packaged_installed(self, package):
122 cmd = "rpm -q {} > /dev/null".format(package)
123 return os.system(cmd) == 0
125 def do_package(self, package, subcommand):
127 "{} {} -y {} {}".format(self.command, self.options,
129 Message("Invoking {}".format(cmd))
130 return os.system(cmd) == 0
133 def update_group(self, group):
134 # it is important to invoke dnf group *upgrade* and not *update*
135 # because the semantics of groups has changed within dnf
138 cmd = "{} {} -y group install {}".format(
139 self.command, self.options, group)
140 Message("Invoking {}".format(cmd))
141 if os.system(cmd) != 0:
143 cmd = "{} {} -y group upgrade {}".format(
144 self.command, self.options, group)
145 Message("Invoking {}".format(cmd))
146 if os.system(cmd) != 0:
149 cmd = "{} {} -y groupinstall {}".format(
150 self.command, self.options, group)
151 Message("Invoking {}".format(cmd))
152 if os.system(cmd) != 0:
156 # update the whole system
157 def update_system(self):
158 cmd = "{} {} -y update".format(self.command, self.options)
159 Message("Invoking {}".format(cmd))
160 return os.system(cmd) == 0
163 cmd = "{} clean all".format(self.command)
164 Message("Invoking {}".format(cmd))
165 return os.system(cmd) == 0
167 # create an entry in /etc/cron.d so we run periodically.
168 # we will be run once a day at a 0-59 minute random offset
169 # into a 0-23 random hour
172 def UpdateCronFile():
175 randomMinute = Random().randrange(0, 59, 1)
176 randomHour = Random().randrange(0, 11, 1)
178 f = open(CRON_FILE, 'w')
179 f.write("# {}\n".format(TARGET_DESC))
180 # xxx is root aliased to the support mailing list ?
181 f.write("MAILTO={}\n".format(TARGET_USER))
182 f.write("SHELL={}\n".format(TARGET_SHELL))
183 f.write("{} {},{} * * * {} {}\n\n"
184 .format(randomMinute, randomHour,
185 randomHour + 12, TARGET_USER, TARGET_SCRIPT))
188 print("Created new cron.d entry.")
190 print("Unable to create cron.d entry.")
193 # simply remove the cron file we created
194 def RemoveCronFile():
197 print("Deleted cron.d entry.")
199 print("Unable to delete cron.d entry.")
204 def __init__(self, doReboot):
205 if self.CheckProxy():
206 os.environ['http_proxy'] = self.HTTP_PROXY
207 os.environ['HTTP_PROXY'] = self.HTTP_PROXY
208 self.doReboot = doReboot
210 def CheckProxy(self):
211 Message("Checking existence of proxy config file...")
212 if os.access(PROXY_FILE, os.R_OK) and os.path.isfile(PROXY_FILE):
213 self.HTTP_PROXY = string.strip(file(PROXY_FILE, 'r').readline())
214 Message("Using proxy {}".format(self.HTTP_PROXY))
217 Message("Not using any proxy.")
220 def InstallKeys(self):
221 Message("Removing any existing GPG signing keys from the RPM database")
222 os.system("{} --allmatches -e gpg-pubkey".format(RPM_PATH))
223 Message("Installing all GPG signing keys in {}".format(RPM_GPG_PATH))
224 os.system("{} --import {}/*".format(RPM_PATH, RPM_GPG_PATH))
226 def ClearRebootFlag(self):
227 os.system("/bin/rm -rf {}".format(REBOOT_FLAG))
229 def CheckForUpdates(self):
230 Message("Removing any existing reboot flags")
231 self.ClearRebootFlag()
232 if self.doReboot == 0:
233 Message("Ignoring any reboot flags set by RPMs")
237 # this of course is quite suboptimal, but proved to be safer
240 # a configurable list of packages to try and update independently
243 crucial_packages = []
244 for package in CRUCIAL_PACKAGES_BUILTIN:
245 crucial_packages.append(package)
246 for path in CRUCIAL_PACKAGES_OPTIONAL_PATHS:
248 crucial_packages += file(path).read().split()
251 Message("List of crucial packages: {}".format(crucial_packages))
252 for package in crucial_packages:
253 yum_dnf.handle_package(package)
258 Message("Updating PlanetLab group")
259 yum_dnf.update_group("PlanetLab")
261 Message("Updating rest of system")
262 yum_dnf.update_system()
264 Message("Checking for extra groups (extensions) to update")
265 if os.access(EXTENSIONS_FILE, os.R_OK) and \
266 os.path.isfile(EXTENSIONS_FILE):
267 extensions_contents = file(EXTENSIONS_FILE).read()
268 extensions_contents = string.strip(extensions_contents)
269 if extensions_contents == "":
270 Message("No extra groups found in file.")
272 extensions_contents.strip()
273 for extension in extensions_contents.split():
274 group = "extension{}".format(extension)
275 yum_dnf.update_group(group)
277 Message("No extensions file found")
279 if os.access(REBOOT_FLAG, os.R_OK) and os.path.isfile(REBOOT_FLAG) and self.doReboot:
280 Message("At least one update requested the system be rebooted")
281 self.ClearRebootFlag()
282 os.system("/sbin/shutdown -r now")
284 def RebuildRPMdb(self):
285 Message("Rebuilding RPM Database.")
287 os.system("rm /var/lib/rpm/__db.*")
288 except Exception as err:
289 print("RebuildRPMdb: exception {}".format(err))
291 os.system("{} --rebuilddb".format(RPM_PATH))
292 except Exception as err:
293 print("RebuildRPMdb: exception {}".format(err))
295 def RemoveRPMS(self):
296 Message("Looking for RPMs to be deleted.")
297 if os.access(DELETE_RPM_LIST_FILE, os.R_OK) and \
298 os.path.isfile(DELETE_RPM_LIST_FILE):
299 rpm_list_contents = file(DELETE_RPM_LIST_FILE).read().strip()
301 if rpm_list_contents == "":
302 Message("No RPMs listed in file to delete.")
305 rpm_list = string.split(rpm_list_contents)
307 Message("Deleting RPMs from {}: {}".format(
308 DELETE_RPM_LIST_FILE, " ".join(rpm_list)))
310 # invoke them separately as otherwise one faulty (e.g. already uninstalled)
311 # would prevent the other ones from uninstalling
314 is_installed = os.system("{} -q {}".format(RPM_PATH, rpm)) == 0
317 "Ignoring rpm {} marked to delete, already uninstalled".format(rpm))
319 uninstalled = os.system("{} -ev {}".format(RPM_PATH, rpm)) == 0
321 Message("Successfully removed RPM {}".format(rpm))
324 Error("Unable to delete RPM {}, continuing. rc={}".format(
328 Message("No RPMs list file found.")
331 ##############################
332 if __name__ == "__main__":
334 # if we are invoked with 'start', display the output.
335 # this is useful for running something silently
336 # under cron and as a service (at startup),
337 # so the cron only outputs errors and doesn't
338 # generate mail when it works correctly
342 # if we hit an rpm that requests a reboot, do it if this is
343 # set to 1. can be turned off by adding noreboot to command line
348 if "start" in sys.argv or "display" in sys.argv:
350 Message("\nTurning on messages")
352 if "noreboot" in sys.argv:
355 if "updatecron" in sys.argv:
356 # simply update the /etc/cron.d file for us, and exit
360 if "removecron" in sys.argv:
364 # see if we are already running by checking the existance
365 # of a PID file, and if it exists, attempting a test kill
366 # to see if the process really does exist. If both of these
369 if os.access(NODEUPDATE_PID_FILE, os.R_OK):
370 pid = string.strip(file(NODEUPDATE_PID_FILE).readline())
372 if os.system("/bin/kill -0 {} > /dev/null 2>&1".format(pid)) == 0:
373 Message("It appears we are already running, exiting.")
376 # write out our process id
377 pidfile = file(NODEUPDATE_PID_FILE, 'w')
378 pidfile.write("{}\n".format(os.getpid()))
381 nodeupdate = NodeUpdate(doReboot)
383 Error("Unable to initialize.")
385 nodeupdate.RebuildRPMdb()
386 nodeupdate.RemoveRPMS()
387 nodeupdate.InstallKeys()
388 nodeupdate.CheckForUpdates()
389 Message("Update complete.")
391 # remove the PID file
392 os.unlink(NODEUPDATE_PID_FILE)