#!/usr/bin/python # # $Id: cfmclient.py 2747 2007-11-16 00:19:58Z mike $ # # Copyright 2007 Platform Computing Inc # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # # # NOTE: The cfmclient runs on nodes that do not have the complete # kusu/lib. Do not use libraries that are unavailable on the # compute nodes. UPDATEFILE = 1 UPDATEPACKAGE = 2 FORCEFILES = 4 UPDATEREPO = 8 # Set DEBUG to 1 to see debugging info in /var/log/cfmclient.log DEBUG = 1 import os import sys import string import pwd import grp import urllib import random import glob import subprocess from kusu.ipfun import * import tempfile from path import path import atexit import time # Add primitive to the python path. This is needed for cfmd to spawn # cfmclient child processes properly since we are going to import # primitive modules in cfmclient.py. Currently cfmd only adds # /opt/kusu/lib/python to PYTHONPATH before spawning cfmclient. sys.path.append('/opt/primitive/lib/python2.4/site-packages') sys.path.append('/opt/primitive/lib64/python2.4/site-packages') from primitive.system.software.probe import OS from primitive.system.software.dispatcher import Dispatcher PLUGINS='/opt/kusu/lib/plugins/cfmclient' CFMFILE='/etc/cfm/.cfmsecret' YUMCONF='/var/cache/yum/yum.conf' LOGFILE='/var/log/cfmclient.log' CHGLIST='/opt/kusu/etc/cfmchanges.lst' # When the cfmclient is updating files on itself it will # not replace the files in the IGNORELST IGNORELST = ['/etc/passwd.merge', '/etc/shadow.merge', '/etc/group.merge', '/etc/gshadow.merge'] def log(mesg): """log - Output messages to a log file""" global DEBUG if DEBUG: try: fp = file(LOGFILE, 'a') fp.write('%s %s' % (time.strftime('%Y-%m-%d %H:%M:%S'), mesg)) fp.close() except: print "Logging unavailable!" class Merger: """ The Merger Class is responsible for dealing with merging different types of files.""" def __init__(self): pass def log(self, mesg): log('%s: %s' % (self.__class__.__name__, mesg)) def mergeFile(self, filename): """ This function is for merging the group, password, and shadow files only. """ if filename in ['/etc/group', '/etc/passwd', '/etc/shadow']: # Set up files as path objects osfile = path("/opt/kusu/etc/cfm/%s.OS" % filename) t0file = path("/opt/kusu/etc/cfm/%s.T0" % filename) currfile = path(filename) apndfile = currfile + '.merge' # If the saved OS copy does not exist: # 1. Make a copy from the current file, keeping same file permissions # 2. Remove the entry for 'root' user in the OS copy for shadow # i.e. /opt/kusu/etc/cfm/shadow.OS if not osfile.exists(): if not osfile.dirname().exists(): osfile.dirname().makedirs() currfile.copy2(osfile) if currfile == '/etc/shadow': # Remove the password entry for root from the OS file self.__removeSelectedLine(osfile, 'root', ':') # The t0 file is the state of the file since the last merge # operation was executed. The current file is then the state at # t+X. Hence, the following finds out what has been added and # removed since the last merge operation. added, removed = self.__getLineChanges(currfile, t0file) # Remove from OS copy the lines that have been removed since t0 for line in removed: key = line.split(':')[0] self.__removeSelectedLine(osfile, key, ':') # Add to OS copy the lines that were added since t0 if added: osfile.write_lines(added, append=True) # Overwrite the current file with contents of the OS file and append file self.log("Merging %s and %s into %s\n" % (osfile, apndfile, currfile)) os.system('cat "%s" "%s" > "%s"' % (osfile, apndfile, currfile)) apndfile.remove() # Update t0 file using copy2 which also preserves permissions currfile.copy2(t0file) else: print "WARNING: Unhandled file merge for file: %s. Ignoring" % currfile def __removeComments(self, lines): return [line for line in lines if not line[0] == '#'] def __getLineChanges(self, origfile, t0file): """ Compare the original and time=0 file for added and removed lines. Returns a list of the added and removed lines. """ added = [] removed = [] if not t0file.exists(): return [[], []] curr = set(self.__removeComments(origfile.lines())) t0 = set(self.__removeComments(t0file.lines())) # curr - t0 ==> added since t0 # t0 - curr ==> removed since t0 return [curr - t0, t0 - curr] def __removeSelectedLine(self, infile, key, separator): """ Search through a file removing the line that starts with the given key. Entries in that file are delimited by given separator. """ try: lines = [line for line in infile.lines() \ if not line.split(separator)[0] == key] infile.write_lines(lines) except: print "Failed to remove line with key %s from %s" % (key, infile) class CFMClient: def __init__(self, argv): self.args = argv self.CFMBaseDir = '' self.osname = OS()[0].lower() self.ngid = 0 self.repoid = 0 self.pkglstpost = 'package.lst' self.packagelst = '/opt/kusu/etc/package.lst' self.cfmfilelst = '/opt/kusu/etc/cfmfiles.lst' self.newpackages = [] # List of packages to add self.oldpackages = [] # List of packages to remove self.newfiles = [] # List of new filesto update self.md5sum = '/usr/bin/md5sum' self.type = 0 self.installers = [] self.bestinstaller = '' # The IP of the installer to get files from self.selfupdate = False # Flag to indicate that this is on an installer def log(self, mesg): log('%s: %s' % (self.__class__.__name__, mesg)) def parseargs(self): """parseargs - Parse the command line arguments and populate the class variables""" # This tool is not for use by the user args = self.args[1:] i = 0 while i < len(args): if args[i] == '-t': if len(args) > (i+1): val = args[i+1] i += 1 try: self.type = string.atoi(val) except: self.log("ERROR: Invalid type argument\n") sys.exit(1) else: self.log("ERROR: Type not specified\n") sys.exit(2) elif args[i] == '-i': if len(args) > (i+1): self.installers = string.split(args[i+1], ',') i += 1 else: self.log("ERROR: Installers not specified\n") sys.exit(3) else: self.log("ERROR: Unknown argument\n") sys.exit(4) i += 1 if self.type == 0 or self.installers == []: # User is running this by hand self.log("ERROR: Missing arguments!\n") print "ERROR: Missing arguments. See the manpage for help." sys.exit(5) def getProfileVal(self, name): """getProfileVal - Returns the value of the NII property from /etc/profile.nii with any quotes removed.""" cmd = "grep %s /etc/profile.nii 2>/dev/null" % name val = '' proc = os.popen(cmd) for line in proc.readlines(): loc = string.find(line, name) if loc < 0: continue t2 = string.split(line[string.find(line, '=')+1:])[0] val = string.strip(t2, '"') break proc.close() return val def setupNIIVars(self): """setupNIIVars - Read in the needed variables from the profile.nii""" self.CFMBaseDir = self.getProfileVal("CFMBaseDir") val = self.getProfileVal("NII_REPO") if val: self.repoid = os.path.basename(val) try: self.ngid = string.atoi(self.getProfileVal("NII_NGID")) except: self.log("ERROR: Unable to determine NGID. Got: %s\n" % self.ngid) self.ngid = 0 def __haveLocalAccess(self): """__haveLocalAccess - This function will determine if the CFMBaseDir is mounted locally. Returns True if it is available. This will look for the profile.nii, then try to get the CFMBaseDir from it, then see if it has the $CFMBaseDir/$NGID/opt/kusu/etc/package.lst If local directory access is not available it will find an installer to use from the installer list and set self.bestinstaller.""" if not self.CFMBaseDir: self.CFMBaseDir = self.getProfileVal("CFMBaseDir") # Test for local access filetest = "%s/cfmfiles.lst" % (self.CFMBaseDir) self.log("++ Testing for: %s\n" % filetest) self.log("++ NGID = %i, CFMBaseDir: %s\n" % (self.ngid, self.CFMBaseDir)) if os.path.exists(filetest): return True myIPs = getMyIPs() self.log("myIPs = %s, installers = %s\n" % (myIPs, self.installers)) bestIPlist = bestIP(myIPs, self.installers) if len(bestIPlist) == 0: # No good one found! bestIPlist = self.installers[:] self.bestinstaller = bestIPlist[random.randint(0, len(bestIPlist)-1)] self.log("Using installer: %s from list: %s\n" % (self.bestinstaller, bestIPlist)) return False def __setFilePerms (self, file, username, grpname, mode): """__setFilePerms - Set the file ownership, group, and mode of a file. The user and group are the names, not UID/GID's. This is to allow this to work across OS's. If the username, or grpname cannot be determined then "nobody" will be used.""" try: uid = pwd.getpwnam(username)[2] except: uid = pwd.getpwnam('nobody')[2] try: gid = grp.getgrnam(grpname)[2] except: gid = grp.getgrnam('nobody')[2] try: os.chmod(file, mode) except: self.log("ERROR: Failed to set the mode of %s to %s\n" % (file, mode)) sys.exit(-1) try: os.chown(file, uid, gid) except: self.log("ERROR: Failed to set the ownership of %s, UID=%s, GID=%s\n" % (file, uid, gid)) def __getFile (self, source, deststruct, cfmfile=0): """__getFile - Copy, or download a file to a given location. The source is the fully qualified path without the CFMBaseDir and NGID. The __getFile will work out the correct path/URL to get it from. The deststruct is a tupple containing: (filename, user, group, mode, md5sum(optional)) If the decrypt flag is set to 1 it will decrypt the file. If the cfmfile flag is set to 1, then the source path will not use the ngid. This is needed to get cfmfiles.lst, and package.lst These files are not encrypted.""" # This is an intermediate file we will use tmpfile = "%s.CFMtmpFile" % deststruct[0] # Make the directory if it does not exist if not os.path.exists(os.path.dirname(tmpfile)): os.system('mkdir -p %s' % os.path.dirname(tmpfile)) if self.__haveLocalAccess(): # Copy the file from a directory self.log("INFO: Have local Access\n") if cfmfile: cfmpath = "%s/%s" % (self.CFMBaseDir, source) else: cfmpath = "%s/%i/%s" % (self.CFMBaseDir, self.ngid, source) if not os.path.exists(cfmpath): self.log("ERROR: Unable to locate: %s\n" % cfmpath) return -1 os.system('cp \"%s\" \"%s\" >/dev/null 2>&1' % (cfmpath, tmpfile)) else: if self.bestinstaller.lower() == 'self' and os.path.exists(self.CFMBaseDir): self.log("INFO: First boot not completed so CFM is not ready. Exiting.\n") sys.exit(0) # Copy the file from one of the installers. Make the URL if cfmfile: url = "http://%s/cfm/%s" % (self.bestinstaller, source) else: url = "http://%s/cfm/%s/%s" % (self.bestinstaller, self.ngid, source) datafile = '' try: (datafile, header) = urllib.urlretrieve(url, tmpfile) except Exception, e: import traceback msg = str(e) tb = traceback.format_exc() msg = msg + '\n' + tb + '\n' self.log("URL download failed! Trace: %s" % msg) self.log("WARNING: Download from %s Failed!\n" % self.bestinstaller) # Download failed. Try the other IP's for ip in self.installers: if cfmfile: url = "http://%s/cfm/%s" % (ip, source) else: url = "http://%s/cfm/%s/%s" % (ip, self.ngid, source) self.log("Trying: %s\n" % url) try: (datafile, header) = urllib.urlretrieve(url, tmpfile) # This one worked. Switch to it self.bestinstaller = ip[:] break except: pass if not datafile: self.log("ERROR: Failed to download: %s\n" % url) return -1 # Apache does not return the 404 error code in the header as seen here # Date: Tue, 18 Mar 2008 16:22:11 GMT # Server: Apache/2.2.3 (Red Hat) # Content-Length: 287 # Connection: close # Content-Type: text/html; charset=iso-8859-1 # Instead the data must be parsed for the error, which looks like: # # # 404 Not Found # #

Not Found

#

The requested URL /repos/1001/booger was not found on this server.

#
#
Apache/2.2.3 (Red Hat) Server at tyan04 Port 80
# # Put code here to deal with this............................ # Decrypt and decompress the file (if needed) if not cfmfile: tmpfile2 = "%s.decrypted.CFMtmpFile" % deststruct[0] global CFMFILE if os.path.exists(CFMFILE): cmd = 'openssl bf -d -a -salt -pass file:%s -in "%s" |gunzip > "%s"' % (CFMFILE, tmpfile, tmpfile2) else: # *** Really need a better shared secret. REMOVE THIS LATER cmd = 'openssl bf -d -a -salt -pass file:/opt/kusu/etc/db.passwd -in "%s" |gunzip > "%s"' % (tmpfile, tmpfile2) proc = os.popen(cmd) for line in proc.readlines(): if line: self.log("ERROR: %s\n" % line) if os.path.exists(tmpfile2): os.unlink(tmpfile2) if os.path.exists(tmpfile): os.unlink(tmpfile) proc.close() return -1 proc.close() try: os.rename(tmpfile2, tmpfile) except OSError, e: self.log("ERROR: unable to rename %s to %s: %s" % (tmpfile2, tmpfile, e)) return -1 # Set the file permissions self.__setFilePerms(tmpfile, deststruct[1], deststruct[2], deststruct[3]) # If an md5sum is provided test the file. try: origmd5sum = deststruct[4] cmd = '%s "%s"' % (self.md5sum, tmpfile) md5sum = '-none-' proc = os.popen(cmd) for line in proc.readlines(): bits = string.split(line) md5sum = bits[0] if origmd5sum != md5sum: self.log("ERROR: The checksum of the received file, and the original do not match. Aborting transfer!\n") self.log("Filename = %s\n" % tmpfile) self.log("origmd5sum= %s md5sum= %s\n" % (origmd5sum, md5sum)) os.unlink(tmpfile) proc.close() return -1 proc.close() except: # No md5sum provided pass os.rename(tmpfile, deststruct[0]) return 0 def __setupForYum(self): """__setupForYum - Make a yum.conf pointing to the installer that is closest""" dirname = Dispatcher.get('yum_repo_subdir', 'Server') global YUMCONF yumconf = YUMCONF fp = file(yumconf, 'w') out = ( '[main]\n' 'cachedir=/var/cache/yum\n' 'debuglevel=2\n' 'logfile=/var/log/yum.log\n' 'reposdir=/dev/null\n' 'retries=20\n' 'timeout=30\n' 'assumeyes=1\n' 'gpmcheck=0\n' 'tolerant=1\n\n' '[kusu-installer]\n' 'name=%s - Booger\n' 'baseurl=http://%s/repos/%s%s\n' % (self.osname, self.bestinstaller, self.repoid, dirname) ) fp.write(out) # Handle other other RHEL directories if self.osname == 'rhel': out = ('[kusu-installer-Cluster]\n' 'name=%s - Booger - Cluster\n' 'baseurl=http://%s/repos/%s/Cluster/\n' % (self.osname, self.bestinstaller, self.repoid) ) fp.write(out) out = ('[kusu-installer-ClusterStorage]\n' 'name=%s - Booger - ClusterStorage\n' 'baseurl=http://%s/repos/%s/ClusterStorage/\n' % (self.osname, self.bestinstaller, self.repoid) ) fp.write(out) out = ('[kusu-installer-VT]\n' 'name=%s - Booger - VT\n' 'baseurl=http://%s/repos/%s/VT/\n' % (self.osname, self.bestinstaller, self.repoid) ) fp.write(out) fp.close() def __runCommand(self, cmd): self.log("Running: %s\n" % cmd) proc = os.popen(cmd) for line in proc.readlines(): self.log(line) proc.close() def __runCommand2(self, cmd): self.log("Running: %s\n" % cmd) p = subprocess.Popen(cmd, shell=True, bufsize=-1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True) (sout, serr) = p.communicate() try: p.wait() except: pass try: p.stdin.close() p.stdout.close() except: pass def __setupZypperSource(self): """ Backup existing sources and caches and create just one source that points to the closest installer. """ atexit.register(self.__restoreZypperSources) sources_dir = path(Dispatcher.get('zypper_sources_dir')) cache_dir = path(Dispatcher.get('zypper_cache_dir')) if sources_dir.exists(): self.temp_dir = path(tempfile.mkdtemp(prefix=self.__class__.__name__, dir=Dispatcher.get('zypper_base_dir', default='/tmp'))) (self.temp_dir / 'sources').mkdir() (self.temp_dir / 'cache').mkdir() # Move all source files to the temp_dir cmd = "mv %s/* %s/sources" % (sources_dir, self.temp_dir) self.__runCommand(cmd) # Move all directories in the cache_dir to the temp_dir cmd = "mv %s/* %s/cache" % (cache_dir, self.temp_dir) self.__runCommand(cmd) # Add the installer as the only source cmd = "echo y | /usr/bin/zypper service-add http://%s/repos/%s %s >> %s 2>&1" % (self.bestinstaller, self.repoid, self.osname, LOGFILE) self.__runCommand2(cmd) def __restoreZypperSources(self): """ Restore the sources that were backed up earlier in __setupZypperSource. """ sources_dir = path(Dispatcher.get('zypper_sources_dir')) cache_dir = path(Dispatcher.get('zypper_cache_dir')) if sources_dir.exists() and \ cache_dir.exists() and \ hasattr(self, 'temp_dir') and \ (self.temp_dir / 'sources').exists() and \ (self.temp_dir / 'cache').exists(): # Remove the installer source added earlier cmd = "echo y | /usr/bin/zypper service-delete http://%s/repos/%s >> %s 2>&1" % (self.bestinstaller, self.repoid, LOGFILE) self.__runCommand2(cmd) # Move the original zypper sources back cmd = "mv %s/sources/* %s" % (self.temp_dir, sources_dir) self.__runCommand(cmd) # Delete cache directories for dir in cache_dir.dirs(): dir.rmtree() # Move the original zypper caches back cmd = "mv %s/cache/* %s" % (self.temp_dir, cache_dir) self.__runCommand(cmd) self.temp_dir.rmtree() def __installPackages(self): """__installPackages - Install the packages in the list.""" if not self.newpackages: self.log("Nothing to add\n") return cmd = '' if self.osname in ['rhel', 'fedora', 'centos']: global YUMCONF self.__setupForYum() cmd = "/usr/bin/yum -y -c %s clean all >> %s 2>&1" % (YUMCONF, LOGFILE) self.__runCommand2(cmd) cmd = "/usr/bin/yum -y -c %s install " % YUMCONF elif self.osname in ['sles', 'opensuse', 'suse']: self.__setupZypperSource() cmd = "echo y | %s" % Dispatcher.get('zypper_install_cmd') if cmd: for i in self.newpackages: # Using redirection here because the p.communicate deadlocks cmd2 = "%s %s >> %s 2>&1" % (cmd, i, LOGFILE) self.__runCommand2(cmd2) def __removePackages(self): """__removePackages - Remove the packages in the list.""" if not self.oldpackages: self.log("Nothing to remove\n") return cmd = '' if self.osname in ['rhel', 'fedora', 'centos']: global YUMCONF self.__setupForYum() cmd = "/usr/bin/yum -y -c %s remove" % YUMCONF elif self.osname in ['sles', 'opensuse', 'suse']: self.__setupZypperSource() cmd = "echo y | %s" % Dispatcher.get('zypper_remove_cmd') if cmd: for i in self.oldpackages: # Using redirection here because the p.communicate deadlocks cmd2 = "%s %s >> %s 2>&1" % (cmd, i, LOGFILE) self.__runCommand2(cmd2) def __getFileEntries(self, filename): """__getFileEntries - Return the contents of a file as a list. Comments are stripped.""" retval = [] try: filep = open(filename, 'r') except: return retval while True: line = filep.readline() if len(line) == 0: break if line and line[0] != '#': retval.append(string.strip(line)) filep.close() return retval def __getPackageChanges(self): """__getPackageChanges - Populate the old and new lists of packages.""" oldfile = '%s.ORIG' % self.packagelst oldlist = self.__getFileEntries(oldfile) newlist = self.__getFileEntries(self.packagelst) # Strip out duplicates oldlist.sort() tmplst = oldlist[:] for i in xrange(1, len(tmplst)): if tmplst[i-1] == tmplst[i]: oldlist.remove(tmplst[i]) # Scan over the newlist and oldlist for entries removing those in both tmp = newlist[:] for entry in tmp: if entry in oldlist: oldlist.remove(entry) newlist.remove(entry) # Anything still in newlist is a new package self.newpackages = [] for entry in newlist: if entry: self.newpackages.append(entry) self.log("Found new package: %s\n" % entry) # Anything still in oldlist is a package to remove self.oldpackages = [] for entry in oldlist: # Skip yum and rpm if entry == 'yum' or entry == 'rpm': self.log("WARNING: Can't remove: %s\n" % entry) continue if entry: self.oldpackages.append(entry) self.log("Found old package: %s\n" % entry) def __processFileName(self, filename): """__processFileName - The filename from the cfmfile list contains additional information on how the file should be treated. This function returns a tuple with: "filename.action", "action" The action will be ignored if it is from another node group.""" # Determine is any special processing is needed action = '' if filename[-7:] == '.append': action = 'append' if filename[-6:] == '.merge': action = 'merge' fn = filename #if action: # fn = filename[:-(len(action) + 1)] #else: # fn = filename # Test to see if this is in the same node group try: ng = string.atoi(string.split(fn[(len(self.CFMBaseDir)):], '/')[1]) if ng != self.ngid: action = 'ignore' except ValueError: self.log('Path does not match expected format. This may be due to wrong or unset CFMBaseDir') action = 'ignore' # Now strip off the leading CFMBaseDir, and NGID fn = fn[(len(self.CFMBaseDir) + len("%i" % self.ngid) + 1):] if self.selfupdate and action != 'ignore': # Set this to ignore if this is the installer and it is one # of the files not to replace if fn in IGNORELST: action = 'ignore' self.log("Not replacing: %s\n" % fn) return (fn, action) def __findOlderFiles(self, force=0): """__findOlderFiles - This function will read the cfmfiles.lst, and locate files in that list that are older than the timestamp in the file, then populate the self.newfiles with the list of files to update. If the force option is provided and is non-zero then all files will be updated. This is to deal with newly installed nodes where the timestamp on the files is always newer then the CFM file.""" try: filep = open(self.cfmfilelst, 'r') except: self.log("ERROR: Could not open: %s\n" % self.cfmfilelst) return for line in filep.readlines(): if line[0] == '#': continue if line: # e.g. /opt/kusu/cfm/5/opt/kusu/etc/package.lst apache apache # 420 1181168668 8c1a2bb5be7c8a4c8279484772fccf01 chunks = string.split(line) md5sum = chunks[-1:][0] time = string.atoi(chunks[-2:-1][0]) tmode = chunks[-3:-2][0] group = chunks[-4:-3][0] user = chunks[-5:-4][0] filen = string.join(chunks[:-5], ' ') filename, action = self.__processFileName(filen) if action == 'ignore': continue # Make mode an octal number mode = string.atoi(tmode, 8) # Only look at destination file fn = filename if action != '': fn = filename[:-(len(action) + 1)] # Force the file installation if force != 0: self.newfiles.append([filename, user, group, mode, action, md5sum]) continue # Test to see if it's newer if os.path.exists(fn): mtime = os.path.getmtime(fn) if mtime < time: self.log(" ++ Going to get: %s\n" % filename) self.log("local file time=%i, remote file time=%i\n" % (mtime, time)) # File needs to be updated self.newfiles.append([filename, user, group, mode, action, md5sum]) elif action == '': # Test the md5sum of the file to see if it differs # NOTE: This is only valid if the action is '' cmd = '%s "%s"' % (self.md5sum, filename) origsum = '-none-' proc = os.popen(cmd) for line in proc.readlines(): bits = string.split(line) origsum = bits[0] if origsum != md5sum: self.log(" ++ Md5sum differs Going to get: %s\n" % filename) self.newfiles.append([filename, user, group, mode, action, md5sum]) proc.close() else: # Special case. For merging files we cannot use the md5sum. Have to merge anyway self.log(" ++ Local file is newer! Action=%s Going to get: %s\n" % (action, filename)) self.newfiles.append([filename, user, group, mode, action, md5sum]) else: self.log("NOTICE: File: %s does not exist!\n" % filename) self.newfiles.append([filename, user, group, mode, action, md5sum]) filep.close() def __installNewFiles(self): """__installNewFiles - Install the files in the self.newfiles list""" for filename, user, group, mode, action, md5sum in self.newfiles: attr = (filename, user, group, mode, md5sum) self.log("Updating file: %s\n" % filename) retval = self.__getFile(filename, attr, 0) if retval: continue # Determine what to do based on the action if action == '': continue elif action == 'append': realfile = filename[:-(len(action) + 1)] osfile = "/opt/kusu/etc/%s.OS" % realfile if not os.path.exists(osfile): self.log("Copying original file to: %s\n" % osfile) os.system('mkdir -p \"%s\"' % os.path.dirname(osfile)) if os.path.exists(realfile): os.system('cp \"%s\" \"%s\"' % (realfile, osfile)) else: fin = open(osfile, 'w') fin.close() self.log("Combining: %s and %s to %s\n" % (osfile, filename, realfile)) os.system('cat \"%s\" \"%s\" > \"%s\"' % (osfile, filename, realfile)) os.unlink(filename) elif action == 'merge': # This is for the group passwd, and shadow files. realfile = filename[:-(len(action) + 1)] merger = Merger() merger.mergeFile(realfile) else: self.log("WARNING: Unknown action type: %s for %s\n" % (filename, action) ) def __runPlugins(self): """__runPlugins - Run any plugins found in the PLUGINS directory. These can be any type of executable. The list of plugins will be sorted then run. """ global PLUGINS sys.path.append(PLUGINS) # Drop a file listing the changes that have happened try: fptr = open(CHGLIST,'w') if self.newpackages: for i in self.newpackages: fptr.write("Added %s\n" % i) if self.oldpackages: for i in self.oldpackages: fptr.write("Removed %s\n" % i) if self.newfiles: for filename, user, group, mode, action, md5sum in self.newfiles: fptr.write("New_file %s\n" % filename) fptr.close() except: self.log("ERROR Failed to open: %s\n" % CHGLIST) flist = glob.glob('%s/*' % PLUGINS) if len(flist) == 0: return flist.sort() for plugin in flist: if plugin[-7:] == '.remove': continue self.log("Running plugin: %s\n" % plugin) os.system('/bin/sh %s >/dev/null 2>&1' % plugin) def __removeDeps(self): """__removeDeps - Run any plugin found in the PLUGINS directory that end in .remove These can be any type of executable. The list of plugins will be sorted then run. """ global PLUGINS sys.path.append(PLUGINS) flist = glob.glob('%s/*.remove' % PLUGINS) if len(flist) == 0: return flist.sort() for plugin in flist: self.log("Running plugin: %s\n" % plugin) os.system('/bin/sh %s' % plugin) if os.path.exists(plugin): try: os.unlink(plugin) except: pass def updatePackages (self): """updatepackages - Update packages""" self.log("Updating Packages\n") if os.path.exists(self.packagelst): os.rename(self.packagelst, '%s.ORIG' % self.packagelst) attr = (self.packagelst, 'root', 'root', 0600) if self.__getFile('%i.%s' % (self.ngid, self.pkglstpost), attr, 1): # Download failed self.log("Failed to download package list. Aborting!\n") if os.path.exists('%s.ORIG' % self.packagelst): os.rename('%s.ORIG' % self.packagelst, self.packagelst) return -1 # Test the package file to see if it is valid testdata = self.__getFileEntries(self.packagelst) if len(testdata) != 0 and testdata[0][0] == '<': # This is not proper content self.log("ERROR: Failed to get the package.lst.\n") if os.path.exists('%s.ORIG' % self.packagelst): os.rename('%s.ORIG' % self.packagelst, self.packagelst) else: self.__getPackageChanges() try: self.__removePackages() self.__removeDeps() self.__installPackages() if os.path.exists('%s.ORIG' % self.packagelst): os.unlink('%s.ORIG' % self.packagelst) except: # Revert old package list so we can retry if os.path.exists('%s.ORIG' % self.packagelst): os.rename('%s.ORIG' % self.packagelst, self.packagelst) # Mark the node as Installed instead of Expired datafile = '' url = "http://%s/repos/nodeboot.cgi?state=Installed" % self.bestinstaller try: (datafile, header) = urllib.urlretrieve(url) except: pass def updateFiles (self, force): """updatefiles - Update files""" self.log("Updating Files\n") # Update the package list. attr = (self.cfmfilelst, 'root', 'root', 0600) self.__getFile('cfmfiles.lst', attr, 1) self.__findOlderFiles(force) self.__installNewFiles() def updateRepo (self): """updateRepo - Update all new install files in repo""" self.log("Updating To New Repo Packages\n") self.__haveLocalAccess() # Just running: yum update if self.osname in ['rhel', 'fedora', 'centos']: global YUMCONF self.__setupForYum() cmd = "/usr/bin/yum -y -c %s clean all >> %s 2>&1" % (YUMCONF, LOGFILE) self.__runCommand2(cmd) cmd = "/usr/bin/yum -y -c %s update >> %s 2>&1" % (YUMCONF, LOGFILE) self.__runCommand2(cmd) elif self.osname in ['sles', 'opensuse', 'suse']: self.__setupZypperSource() cmd = "echo y | %s >> %s 2>&1" % (Dispatcher.get('zypper_update_cmd'), LOGFILE) self.__runCommand2(cmd) def run (self): """run - Entry point for CFM client""" self.parseargs() # Check if root user if os.geteuid(): print "ERROR: Only root can run this tool\n" sys.exit(-1) # Delete changed file if os.path.exists(CHGLIST): os.unlink(CHGLIST) if self.installers[0] != 'self': self.setupNIIVars() else: self.selfupdate = True # This is the installer to try the database self.ngid = 1 #try: # from kusu.core.db import KusuDB #except: # print "Database modules are unavailable!" # sys.exit(-1) # # db = KusuDB() # db.connect('kusudb', 'apache') # query = ('select repos.repoid, repos.ostype from repos, nodegroups where nodegroups.ngid=1 and nodegroups.repoid=repos.repoid') # try: # db.execute(query) # data = db.fetchone() # except: # print "Failed to connect to database!" # sys.exit(-1) # # self.repoid = data[0] # self.ostype = data[1] # self.CFMBaseDir = db.getAppglobals('CFMBaseDir') # XXX: Workaround until we have a profile.nii on installer self.repoid = 1000 # Exit if os is not supported if not (self.osname in ['rhel', 'centos', 'fedora', 'sles', 'opensuse', 'suse']): sys.exit(-1) self.CFMBaseDir = '/opt/kusu/cfm' self.bestinstaller = '127.0.0.1' global UPDATEFILE global UPDATEPACKAGE global FORCEFILES global UPDATEREPO if not self.installers or not self.type: print "Usage: {cmd} -t [TYPE] -i {Installer list}" sys.exit(-1) force = 0 if self.type & FORCEFILES: self.log("INFO: Forcing update of all files.\n") force = 1 if self.type & UPDATEFILE: self.updateFiles(force) if self.type & UPDATEPACKAGE: self.updatePackages() if (self.type & UPDATEPACKAGE) or (self.type & UPDATEFILE): if (self.type & UPDATEFILE) and len(self.newfiles): self.__runPlugins() elif self.type & UPDATEPACKAGE: self.__runPlugins() if self.type & UPDATEREPO: self.updateRepo() sys.exit(0) if __name__ == '__main__': app = CFMClient(sys.argv) app.run()