#!/usr/bin/python import xmlrpclib import sys import getopt from string import atoi, atoi_error import datetime import os ############################################################################################## # Variable descriptions: # # SATELLITE_URL: The URL of the RHEL Satellite server. # DATE_FORMAT: Format of the date and time. The format must coincide with the date format from # xmlrpclib, and should only be changed if that changes. # EMAIL_SUBJECT: Subject heading of the email. # MAIL_EXEC: The command used to send the email. The assumption is that sendmail is used, but # any Linux command line mail sender will likely work. # # Copyright 2011, Regents of the University of Michigan. Free for # non-profit use. Any redistribution of this code must be accompanied # by this copyright notice. ############################################################################################## SATELLITE_URL = "http:///rpc/api" DATE_FORMAT = "%Y%m%dT%H:%M:%S" EMAIL_SUBJECT = "RHN Satellite Automatic Removal of Inactive Profiles" EMAIL_ADDRESS = ["@umich.edu"] EMAIL_EXEC = "/usr/sbin/sendmail" EMAIL_MESSAGE = """The following is an overview of the inactive profiles on the Satellite, and details when inactive profiles will be deleted. You can add the word \"HIATUS\" or \"DUALBOOT\" (case-insensitive) to the end of the name of any profile on the server to have it excluded from this script. Any questions should be sent to LSA RSG (lsa-rsg@umich.edu). """ DELETE_DAYS = 365 CAUTION_DAYS = [100, 200, 300] WARNING_DAYS = [DELETE_DAYS - 14, DELETE_DAYS - 7] VERBOSE = False LOG_FILE = "/var/log/remove-inactive-profiles.log" INCLUDE_VIRTUAL = False EXCLUDE = [] DRY = False SEND = False SAFE_WORD = ["hiatus", "dualboot", "duelboot"] GRACE_PERIOD = 7 def usage(): print "Use this script to automatically remove old profiles from" print "the satellite server." print "" print "Required Arguments:" print " -l, --login=FILE FILE should be a two-line file, where the first line is" print " the account name used to log into the satellite, and" print " the second line is the password associated with the" print " account." print "" print "Control Arguments:" print " -c, --caution-days=[100,200,300]" print " Takes a comma separated list of integers. Profiles inactive for" print " each of these numbers of days (within grace period) will be listed." print " -w, --warning-days=[351,358]" print " Takes a comma separated list of integers. Profiles inactive for" print " each of these numbers of days receive a warning." print " -d, --delete-days=[365] The threshold for which a profile is considered old." print " Profiles that have been inactive for at least this long" print " will be deleted." print " -g, --grace=[7] The size of the window for machines that will receive cautions, in" print " days. Does not affect warnings or deletions. It is recommended this" print " be set to the frequency this script is run." print " -s --send=[you@umich.edu]" print " Send an email to the user specified." print " By default, no emails will be sent." print " -f, --log-file=[/var/log/remove-inactive-profiles.log]" print " The log file where output is stored." print " -e, --exclude=KEYWORD Use this option to prevent this script from deleting" print " profile names which contain the given KEYWORD. This check is" print " case-insensitive." print " -i, --include-virtual Include this argument if you would also like to delete" print " old profiles of virtual systems. By default, virtualized" print " systems will NOT be deleted." print "" print "Other Arguments:" print " -h, --help Display this help and exit" print " -v, --verbose Display the logging messages to stdout as well." print " -D, --dry Do output as normal, except make no changes." print "" return def email(fromName, fromAddress, ccList, bccList, toAddresses, subject, message): outFileName = "/tmp/mail-parse-data-to-mailer" cmd = "rm -f " + outFileName os.system(cmd) outFile = open(outFileName, 'a') addressees = toAddresses[0] for i in toAddresses[1:]: addressees += ", " addressees += i outFile.write("To: " + addressees + "\n") outFile.write("From: \"" + fromName + "\" <" + fromAddress + ">\n") if ccList != "": outFile.write("Cc: " + ccList + "\n") if bccList != "": outFile.write("Bcc: " + bccList + "\n") outFile.write("Subject: " + subject + "\n") #outFile.write("Content-Type: text/html; charset=\"us-ascii\"\n\n") #outFile.write("\n") #outFile.write("\n") for line in message: outFile.write(line) outFile.write("\n\n") #outFile.write("\n") #outFile.write("\n") outFile.close() #cmd = EMAIL_EXEC + " -s \"%s\" %s < \"%s\"" % (subject, address, message) cmd = EMAIL_EXEC + " -i -t < %s" % outFileName #print cmd #system("cat " + outFileName) rc = os.system(cmd) return rc #### Handle Command-line Arguments #### try: opts, args = getopt.getopt(sys.argv[1:], "l:f:e:c:w:d:g:ihvsD", ["login=", "log-file=", "exclude=", "caution-days=", "warning-days=", "delete-days=", "grace=", "include-virtual", "help", "verbose", "send", "dry"]) except getopt.GetoptError: usage() sys.exit(1) for opt, arg in opts: if opt in ("-h", "--help"): usage() sys.exit(0) elif opt in ("-v", "--verbose"): VERBOSE = True elif opt in ("-l", "--login"): credentialsFileName = arg elif opt in ("--caution-days"): CAUTION_DAYS = [] s = str(arg) s = s.strip() list = s.split(',') for i in list: try: CAUTION_DAYS.append(atoi(arg)) except: print "\nValue for caution-days must be comma-separated list of integers\n" sys.exit(1) elif opt in ("--warning-days"): WARNING_DAYS = [] s = str(arg) s = s.strip() list = s.split(',') for i in list: try: WARNING_DAYS.append(atoi(arg)) except: print "\nValue for warning-days must be comma-separated list of integers\n" sys.exit(1) elif opt in ("--delete-days"): DELETE_DAYS = [] s = str(arg) s = s.strip() try: DELETE_DAYS = atoi(arg) except: print "\nValue for delete-days must be a positive integer\n" sys.exit(1) elif opt in ("-f", "--log-file"): LOG_FILE = arg elif opt in ("-i", "--include-virtual"): INCLUDE_VIRTUAL = True elif opt in ("-e", "--exclude"): SAFE_WORD.append(arg) elif opt in ("-D", "--dry"): DRY = True elif opt in ("-s", "--send"): SEND = True elif opt in ("-g", "--grace"): try: GRACE_PERIOD = atoi(arg) except: print "\nValue for grace must be a positive integer\n" sys.exit(1) login = "" password = "" credentialsFile = open(credentialsFileName, 'r') login = credentialsFile.readline() password = credentialsFile.readline() credentialsFile.close() login = login.strip() password = password.strip() if login == "" or password == "": print "\nPlease supply a login name and password." usage() sys.exit(4) ###end argument processing log = open(LOG_FILE, 'a', 0) #append, unbuffered log.write("\n\n") log.write(" ".join(sys.argv[0:])) out = "" if DRY == True: out = "Doing a DRY RUN------Profiles will NOT be deleted!" log.write("\n" + out) if VERBOSE : print out ###Connect to the server try: client = xmlrpclib.Server(SATELLITE_URL, verbose=0) out = "Connected to %s successfully." % SATELLITE_URL log.write("\n" + out) if VERBOSE : print out except: out = "Unable to connect to server %s\nExiting\n" % SATELLITE_URL log.write("\n" + out) if VERBOSE : print out sys.exit(2) ###Open a session try: session = client.auth.login(login, password) out = "Session created with login = %s and supplied password" % login log.write("\n" + out) if VERBOSE : print out except: out = "Unable to create session with login = %s and supplied password\n" % login log.write("\n" + out) if VERBOSE : print out sys.exit(3) ##### rawr allGroupsRaw = client.systemgroup.listAllGroups(session) #list of all system groups visible to user allGroups = [] for group in allGroupsRaw: allGroups.append(group.get('name')) cautionDates = {} warningDates = {} cautionSystems = {} removeFromCautionSystems = {} warningSystems = {} deleteSystems = {} today = datetime.datetime.today() out = "Today is %s" % today.strftime(DATE_FORMAT) deletionDate = today - datetime.timedelta(days=DELETE_DAYS) for number in CAUTION_DAYS: cautionDates[number] = today - datetime.timedelta(days=number) #gets the dates out += "\nProfiles inactive for %s days (since %s) are marked for future deletion." % (number, cautionDates[number].strftime(DATE_FORMAT)) cautionSystems[number] = {} removeFromCautionSystems[number] = {} for group in allGroups: cautionSystems[number][group] = client.systemgroup.listInactiveSystemsInGroup(session, group, number) #gets systems removeFromCautionSystems[number][group] = client.systemgroup.listInactiveSystemsInGroup(session, group, number + GRACE_PERIOD) #gets systems for number in WARNING_DAYS: warningDates[number] = today - datetime.timedelta(days=number) #gets the dates out += "\nProfiles inactive for %s days (since %s) will be deleted on %s." % (number, warningDates[number].strftime(DATE_FORMAT), deletionDate.strftime(DATE_FORMAT)) warningSystems[number] = {} for group in allGroups: warningSystems[number][group] = client.systemgroup.listInactiveSystemsInGroup(session, group, number) #gets systems out += "\nProfiles inactive for %s days (since %s) will be deleted." % (DELETE_DAYS, deletionDate.strftime(DATE_FORMAT)) for group in allGroups: deleteSystems[group] = client.systemgroup.listInactiveSystemsInGroup(session, group, DELETE_DAYS) log.write("\n" + out) if VERBOSE : print out del allGroups ###Construct sets of systems by sysid -- note that "sys" here is a sysid cautionIds = {} warningIds = {} deleteIds = {} names = {} working = 0 for number in cautionSystems.keys(): for group in cautionSystems[number].keys(): for sys in cautionSystems[number][group]: working += 1 if VERBOSE and working % 25 == 1: print "." sysName = client.system.getName(session, sys) names[sys] = sysName.get('name') isSafe = True for word in SAFE_WORD: if word.lower() in str(sysName).lower(): isSafe = False break if isSafe == True: if number not in cautionIds: cautionIds[number] = {} if group not in cautionIds[number]: cautionIds[number][group] = set() cautionIds[number][group].add(sys) for number in warningSystems.keys(): for group in warningSystems[number].keys(): for sys in warningSystems[number][group]: working += 1 if VERBOSE and working % 25 == 1: print "." sysName = client.system.getName(session, sys) names[sys] = sysName.get('name') isSafe = True for word in SAFE_WORD: if word.lower() in str(sysName).lower(): isSafe = False break if isSafe == True: if number not in warningIds: warningIds[number] = {} if group not in warningIds[number]: warningIds[number][group] = set() warningIds[number][group].add(sys) for group in deleteSystems.keys(): for sys in deleteSystems[group]: working += 1 if VERBOSE and working % 25 == 1: print "." sysName = client.system.getName(session, sys) names[sys] = sysName.get('name') isSafe = True for word in SAFE_WORD: if word.lower() in str(sysName).lower(): isSafe = False break if isSafe == True: if group not in deleteIds: deleteIds[group] = set() deleteIds[group].add(sys) print "\n" del cautionSystems del warningSystems del deleteSystems if INCLUDE_VIRTUAL == False: #Get the list virtHosts = client.system.listVirtualHosts(session) virtIdList = set() for host in virtHosts: guestsOnHost = client.system.listVirtualGuests(session, host.get('id')) for guest in guestsOnHost: virtIdList.add(guest.get('id')) guestsOnHost = [] ###Remove any overlap numbers = sorted(cautionIds.keys()) for i in range(len(numbers)): groups = cautionIds[numbers[i]].keys() for j in range(i+1, len(cautionIds)): for group in groups: if group in cautionIds[numbers[j]].keys(): cautionIds[numbers[i]][group] = cautionIds[numbers[i]][group] - cautionIds[numbers[j]][group] for warningNumber in warningIds.keys(): for group in groups: if group in warningIds[warningNumber].keys(): cautionIds[numbers[i]][group] = cautionIds[numbers[i]][group] - warningIds[warningNumber][group] for group in groups: if group in deleteIds.keys(): cautionIds[numbers[i]][group] = cautionIds[numbers[i]][group] - deleteIds[group] #cautionIds[numbers[i]][group] = cautionIds[numbers[i]][group] - virtHostIds if INCLUDE_VIRTUAL == False: cautionIds[numbers[i]][group] = cautionIds[numbers[i]][group] - virtIdList numbers = sorted(warningIds.keys()) for i in range(len(numbers)): groups = warningIds[numbers[i]].keys() for j in range(i+1, len(warningIds)): for group in groups: if group in warningIds[numbers[j]].keys(): warningIds[numbers[i]][group] = warningIds[numbers[i]][group] - warningIds[numbers[j]][group] for group in groups: if group in deleteIds.keys(): warningIds[numbers[i]][group] = warningIds[numbers[i]][group] - deleteIds[group] #warningIds[numbers[i]][group] = warningIds[numbers[i]][group] - virtHostIds if INCLUDE_VIRTUAL == False: warningIds[numbers[i]][group] = warningIds[numbers[i]][group] - virtIdList for group in deleteIds.keys(): #deleteIds[group] = deleteIds[group] - virtHostIds if INCLUDE_VIRTUAL == False: deleteIds[group] = deleteIds[group] - virtIdList ### Here we get a list of all VM hosts, and remove those WITH guests from the above lists ### This way we'll never remove an inactive host profile that has active guests # for number in cautionIds.keys(): # for group in cautionIds[number].keys(): # if host.get('id') in cautionIds[number][group]: # cautionIds[number][group].remove(id) # for number in warningIds.keys(): # for group in warningIds[number].keys(): # if host.get('id') in warningIds[number][group]: # warningIds[number][group].remove(id) # for group in deleteIds.keys(): # if host.get('id') in deleteIds[group]: # deleteIds[group].remove(id) #virtHosts= [] #Get a list of all the virtual profiles/systems, and remove them #from the list of profiles to delete # for id in virtIdList: # for number in cautionIds.keys(): # for group in cautionIds[number].keys(): # if id in cautionIds[number][group]: # cautionIds[number][group].remove(id) # for number in warningIds.keys(): # for group in warningIds[number].keys(): # if id in warningIds[number][group]: # warningIds[number][group].remove(id) # for group in deleteIds.keys(): # if id in deleteIds[group]: # deleteIds.remove(id) ###Generate the caution output cautionOutput = "" for number in sorted(cautionIds.keys()): cautionOutput += "\nThe following systems have now been inactive for %s days:\n" % number for group in cautionIds[number].keys(): if len(cautionIds[number][group]) > 0: cautionOutput += "---%s\n" % str(group).upper() for id in cautionIds[number][group]: system = client.system.getName(session, id) cautionOutput += "------Profile with name=%s, id=%s, and last_checkin=%s\n" % (system.get('name'), system.get('id'), system.get('last_checkin')) warningOutput = "" for number in sorted(warningIds.keys()): warningOutput += "\nThe following systems have been inactive for %s days\n" % number warningOutput += "They WILL BE DELETED in %s days on %s\n" % (DELETE_DAYS - number, (today + datetime.timedelta(days=number)).strftime(DATE_FORMAT)) for group in warningIds[number].keys(): if len(warningIds[number][group]) > 0: warningOutput += "---%s\n" % str(group).upper() for id in warningIds[number][group]: system = client.system.getName(session, id) warningOutput += "------Profile with name=%s, id=%s, and last_checkin=%s\n" % (system.get('name'), system.get('id'), system.get('last_checkin')) ###Generate the first final warning output deleteOutput = "The following systems have been DELETED because they were inactive for %s days\n" % DELETE_DAYS for group in deleteIds.keys(): if len(deleteIds[group]) > 0: deleteOutput += "---%s\n" % str(group).upper() for id in deleteIds[group]: system = client.system.getName(session, id) deleteOutput += "------Profile with name=%s, id=%s, and last_checkin=%s\n" % (system.get('name'), system.get('id'), system.get('last_checkin')) ###Construct user email emailOutput = "" if DRY == True: emailOutput += "This is a DRY RUN. No profiles have been deleted." emailOutput += "\n\n" + deleteOutput emailOutput += "\n\n" + warningOutput emailOutput += "\n\n" + cautionOutput log.write("\n\n\nThe contents of the email:\n" + EMAIL_MESSAGE + emailOutput + "\n\n\n") if VERBOSE : print "\n\nThe contents of the email:\n" + EMAIL_MESSAGE + emailOutput + "\n\n" ###Prepare for output if DRY == False: #Really delete them try: for group in deleteIds.keys(): client.system.deleteSystems(session, list(deleteIds[group])) out = "Profiles deleted successfully." log.write("\n" + out) if VERBOSE: print out except: out = "The Satellite could not delete the profiles.\nUnexpected error:", sys.exc_info()[0] log.write("\n" + out) if VERBOSE: print out sys.exit(42) ###Send the email if SEND == True: out = "Sending email to the following addresses: %s" % EMAIL_ADDRESS rc = email("GROUP", "@umich.edu", "@umich.edu", "", EMAIL_ADDRESS, EMAIL_SUBJECT, EMAIL_MESSAGE + emailOutput) if rc == 0: out += "\nSent successfully." else: out += "\nCould not send the email. Error code: %s" % rc else: out = "User opted to not send email." log.write("\n" + out) if VERBOSE : print out ###Close the session client.auth.logout(session) out = "Exited normally.\nSession closed." log.write("\n" + out) if VERBOSE : print out log.close()