#!/usr/bin/python # # decentprotect.py: Perforce decentralized protect daemon # # Copyright (C) 2003-2008 Servaas Goossens (sgoossens@ortec.nl) # # ---- # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License Version 3 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, see . # ---- # # This script implements a decentralized protection scheme for perforce # by combining multiple protect files located throughout the depot. # # Local permissions for a subtree are stored in a file in the root # of that subtree. The script combines all these files into one list # and applies it using p4 protect. # # It allows default permissions to be set at a higher level, which # can be overruled by subtrees. It is also possible to enforce certain # permissions, which cannot be overruled in a subtree. Permissions for # editing these permission files may be given explicitly in the same # files. # # There's one special file: the master protect file. It can be # submitted anywhere in the depot. Only the master protect file may # include lines with superuser privileges and may have absolute paths. # # Invoke this script with the -h option to get a usage summary. Or # see the usage() function below. # ######################### # CONFIGURATION VARIABLES # Set this to 1 in order to *prevent* # - actually changing the perforce permissions # - sending mail to perforce users # Additionally: # - ".test" is appended to the log file name # - ".test" is appended to the output file name # - repeat is set to zero (no looping) # The cache file and the protect counter are still updated justtesting = 0 # name of the files in the depot that contain protection info protectfile = 'p4.protect' # exact full path in the depot of the master protect file # - the filename should not be equal to the protectfile masterprotectfile = '//depot/admin/master.protect' # Whether to allow wildcard ('*') in user (or group) name for # non-exclusionary permission lines. This enforces the use of # predefined groups. allow_user_wildcards = 1 # filename where to store the resulting protections # or None if they should not be saved in a file. # This file contains comments indicating where each protect # line comes from. Useful for testing and diagnosing problems save_to_file = 'decentprotect.out' # newline may be '\n' or '\n\r' (used only for writing # protections) This is useful if this script runs on *NIX # and you want to view the generated protections file using # a (not so smart) Windows tool. newline = '\r\n' # When defined, some statistics will be written to the log # when new permissions are generated logfile = 'decentprotect.log' # Timeformat used in the log file timeformat = '%Y-%m-%d %H:%M:%S (%Z)' # The Cache file contains all protect files (it's too expensive # to ask Perforce everytime with p4 files //.../p4.protect) cachefile = 'decentprotect.cache' # email address of the person looking after this script. administrator = 'jdoe@example.com' # Send some statistics to the administrator everytime the # protections change? mail_statistics = 1 # For mails sent to a perforce user: whether to send a cc to # the administrator, and whether reply_to should be set to # the administrator bcc_admin = 1 reply_to_admin = 1 # what SMTP server to use when sending mail. mailhost = 'smtp.example.com' # the p4 command line client p4cmd ='/usr/local/bin/p4' # Repeat allows decentprotect to work as a deamon, which keeps # running. When set to 0, the script will poll once and then # exit. This only applies when the -D option is given. repeat = 1 # When working as a deamon and repeat is enabled, # decentprotect will sleep before polling again sleeptime = 5 # in seconds. # when working as a deamon and repeat is enabled: if an error # occurs, decentprotect will use increasing delays before # trying again. min_retry_delay = 60 # seconds max_retry_delay = 60 * 60 * 2 # seconds # name of the perforce counter used to determine whether # any changelists have been submitted since the previous run counter = 'protect' # This user must have Perforce superuser privileges p4user = 'administrator' p4passwd = 'mysecret' p4port = 'p4.example.com:1666' # ip address of the machine running this script, as seen by # the perforce server. This is used for the appended # permissions below. You may have to set this to 127.0.0.1 # if the script runs on the perforce server myipaddress = '192.168.1.111' # This text will always be appended to the permissions. # This will ensure that this script will never # accidentally lose the necessary permissions. append_block = \ ( '# Permissions appended by the decentprotect script' + newline + \ '\tsuper user %(user)s %(ipaddress)s //none' + newline + \ '\tread user %(user)s %(ipaddress)s //.../%(protectfile)s' + newline + \ '\tread user %(user)s %(ipaddress)s %(masterprotectfile)s' + newline ) % { 'ipaddress': myipaddress, 'user': p4user, 'protectfile': protectfile, 'masterprotectfile': masterprotectfile } # END OF CONFIGURATION VARIABLES ################################ import sys, os, math, string, smtplib, re, marshal, time, traceback, p4, getopt, shelve p4.p4 = p4cmd os.environ['P4USER'] = p4user os.environ['P4PASSWD'] = p4passwd os.environ['P4PORT'] = p4port # global var for delaying the retry after errors occur retrydelay = 0 # override some config params for testing if justtesting: repeat = 0 if save_to_file: save_to_file += ".test" if logfile: logfile += ".test" supportedsections = ['default', 'enforce', 'protect'] if administrator and reply_to_admin: replyto_line='Reply-To: '+administrator+'\n' else: replyto_line='' def formatException(exc_info): '''Returns a formatted traceback, given the output of sys.exc_info().''' type, value, tb = exc_info result = "Traceback (innermost last):\n" list = traceback.format_tb(tb, 1000) + traceback.format_exception_only(type, value) result = result + "%-20s %s" % ( string.join(list[:-1], ""), list[-1], ) return result def formatDuration(seconds): '''Returns a formatted duration string, given a number of seconds''' if seconds >= 60 * 60 * 24: hours = float(seconds) / (60 * 60) result = '%d day%s%d hour%s' % \ (int(math.floor(hours / 24)), (int(math.floor(hours / 24)) == 1) and ' ' or 's ', int(round(hours % 24)), (int(round(hours % 24)) == 1) and ' ' or 's ') elif seconds >= 60 * 60: minutes = float(seconds) / 60 result = '%d hour%s%d minute%s' % \ (int(math.floor(minutes / 60)), (int(math.floor(minutes / 60)) == 1) and ' ' or 's ', int(round(minutes % 60)), (int(round(minutes % 60)) == 1) and ' ' or 's ') elif seconds >= 60: seconds = float(seconds) result = '%d minute%s%d second%s' % \ (int(math.floor(seconds / (60))), (int(math.floor(seconds / (60))) == 1) and ' ' or 's ', int(round(seconds % 60)), (int(round(seconds % 60)) == 1) and ' ' or 's ') else: result = '%.1f seconds' % float(seconds) return result.strip() def log(text): '''writes the given text to the log file''' f = file(logfile, 'a') f.write('%s %s\n' % ( time.strftime(timeformat, time.localtime()), text)) f.close() theMailPort = None def mailport(): '''Lazy create function of smtp mailport. Returns the mailport.''' global theMailPort if not theMailPort: theMailPort = smtplib.SMTP(mailhost) return theMailPort def closemailport(): '''closes the mailport and clears the global mailport object.''' global theMailPort if theMailPort: theMailPort.quit() theMailPort = None def complain(mailport,complaint): '''Send a plaintive message to the human looking after this script if we have any difficulties. If no email address for such a human is given, send the complaint to stderr. ''' complaint = complaint + '\n' if administrator: mailport.sendmail('"Perforce Protect Daemon" <%s>' % administrator, [administrator], 'Subject: Perforce Protect Daemon Problem\n\n' + complaint) else: sys.stderr.write(complaint) def mailit(mailport, sender, recipients, message): '''Try to mail message from sender to list of recipients using SMTP object mailport. complain() if there are any problems. ''' try: failed = mailport.sendmail(sender, recipients, message) except: failed = 'Exception ' + repr(sys.exc_info()[0]) + ' was raised.\n%s\n.' % \ str(sys.exc_info()[1]) if failed: complain( mailport, 'The following errors\n' +\ str(failed) +\ '\noccurred while trying to email from\n' + str(sender) + '\nto ' +\ str(recipients) + '\nwith body\n\n' + str(message)) def complain_to_submitter(mailport, file, changenr, complaint): '''Send a message to the submitter of the given change, with a cc to the person responsible for the protections (if configured). ''' user, email, fullname = '', '', '' try: # find email address of the submitter change = p4.describe(change) username = change['user'] user = p4.user(username) email = user['Email'] fullname = user['FullName'] if justtesting: # don't send mail to perforce users email = administrator recipients_with_fullname = '"%s" <%s>' % (fullname, email) recipients = [email] if bcc_admin and administrator and (administrator <> email): recipients.append(administrator) message = ('From: "Perforce Protection Daemon" <%s>\n' % administrator) +\ 'To: "%s" <%s>\n' % (fullname, email) + \ 'Subject: Error in p4.protect file: change %s.\n' % (str(changenr)) +\ replyto_line +\ '\n' +\ 'The following problem(s) occured when processing the file\n' + \ '\t' + file + '\n\n' + \ complaint mailit(mailport, administrator, recipients, message) except: complain(mailport, ('Could not send mail to the submitter of change %s.\n' % str(changenr)) + \ 'Exception ' + repr(sys.exc_info()[0]) + ' was raised.' + \ str(sys.exc_info()[1]) + \ ('File %s has the following problems:\n\n%s' % (file, complaint))) def protectFilesInChange(change): '''Returns a tuple of two lists (newandupdated, deleted) of the protect files contained in the given change.''' newandupdated, deleted = [], [] i = 0 while ('depotFile' + str(i)) in change: filename = change['depotFile'+str(i)] if filename.endswith('/' + protectfile) or (filename == masterprotectfile): if change['action' + str(i)] == 'delete': deleted.append(filename) else: newandupdated.append(filename) i += 1 return newandupdated, deleted def generate_protectline(fields, sectionname, path, allow_superuser=0, allow_absolutepath=0): '''Fields are checked for their validity. Returns a tuple (success, resultline). If any field is invalid, then a line with the reason is returned, otherwise the fields are turned into a line in the format accepted by p4 protect: mode group/user name host path ''' reason = '' # a protect line contains the following fields, the last two are optional: # mode, 'group' or 'user', name, host, path modes = ['list', 'read', 'open', 'write', 'admin', 'super', 'review'] # if len(fields) == 3: # fields.append('*') # default host # if len(fields) == 4: # fields.append('...') # default (relative) path if len(fields) != 5: # reason = 'Each protect line must have at least 3 and at most 5 fields separated by spaces.' reason = 'Each protect line must have exactly 5 fields separated by spaces.' elif not (fields[0] in modes): reason = 'Mode must be one of %s.' % str(modes)[1:-1] elif not (fields[1] in ['group', 'user']): reason = "The second field must be either 'group' or 'user'." elif fields[4][-1] == '/': reason = "A null directory (path ending with '/') is not allowed." elif (fields[0] == 'super') and not allow_superuser: reason = 'Superuser privileges are not allowed here.' elif not allow_absolutepath and \ ((fields[4][0] == '/') or (fields[4][0:2] == '-/')): reason = "An absolute path (beginning with '/') is not allowed here." elif not allow_user_wildcards and \ (fields[4][0] <> '-') and ('*' in fields[2]): reason = "A wildcard ('*') is not allowed in the username or groupname, except when the path begins with a dash ('-')." elif (sectionname == 'protect') and not (fields[0] in ['write', 'read']): reason = 'Only write and read permissions are allowed in the protect section.' elif (sectionname == 'protect') and \ (fields[4][-len(protectfile):] != protectfile): # in the protect section, lines that specify anything other than a # protectfile are not allowed reason = "In the protect section, the path field should end with '%s'." %\ protectfile else: # Everything Ok. if (fields[4][0] != '/') and (fields[4][0:2] != '-/'): # make relative path absolute (take care to keep the dash in front) if fields[4][0] == '-': path = '-' + path fields[4] = fields[4][1:] fields[4] = path + fields[4] line = ' '.join(fields) if reason: # include reason and the invalid line as a comment return 0, reason + '\nThe line where the problem occured is:\n\t' + line + '\n' else: return 1, '\t' + line def parse_protectfile(content, filespec, path, change, allow_superuser=0, allow_absolutepath=0): '''Parse the contents of a protectfile. Return the dictionary {sectionname: [protectlines], ... } A protect file constists of comments, sectionheaders and indented lines. Sections default, enforce and protect are supported. ''' sections = {} for sect in supportedsections: sections[sect] = [] sectionheader = '' sectionlines = [] problems = '' linenr = 0 for line in content.split('\n'): linenr += 1 try: # ignore empty lines and lines beginning with # if not line.strip() or (line.strip()[0] == '#'): continue elif line[0] in string.whitespace: # indented line, should be a protect line fields = line.split() success, result = generate_protectline(fields, sectionheader, path, allow_superuser, allow_absolutepath) if not success: problems += 'Error in line %d (the line has been skipped)\n%s\n' %\ (linenr, result) else: sectionlines.append(result) else: # start of new section, save contents of previous section if sectionheader in supportedsections: sections[sectionheader] = sectionlines sectionlines = [] sectionheader = line.strip().lower() if sectionheader[-1] == ':': sectionheader = sectionheader[:-1] if (sectionheader in sections.keys()) and sections[sectionheader]: raise Exception('Section %s should not occur more than once' %\ sectionheader) if sectionheader not in supportedsections: problems += \ 'Error in line %d: Unsupported section %s has been skipped.\n' % \ sectionheader except: raise Exception('Error when parsing %s, line %d:\n%s.' % (filespec, linenr, str(sys.exc_info()[1]))) if sectionheader in supportedsections: sections[sectionheader] = sectionlines if problems: complain_to_submitter(mailport(), filespec, change, problems) return sections def compare_depotPath(f1, f2): '''Compare function for sorting a filelist according to the depotPath value. f1 and f2 are dictionaries.''' return cmp(f1['depotPath'], f2['depotPath']) def depotFileToDepotPath(depotFile): '''converts the depotFile attribute to a depotPath attribute. DepotPath for the masterprotectfile is '//' to ensure it is first in the sorted list ''' if depotFile == masterprotectfile: return '//' elif depotFile.endswith(protectfile): return depotFile[:-len(protectfile)] else: raise Exception('Cannot convert to depotPath (not a protect file): ' + depotFile); def get_protectfiles(filenames = 'all'): '''Retrieves the requested protectfiles, or all protectfiles if filenames is 'all'. Returns a list of dictionaries (as generated by perforce, plus a depotPath key). Warning: retrieving all protectfiles may be an expensive operation for Perforce. ''' if filenames == 'all': command = p4.p4 + ' -G files %s //.../%s' % (masterprotectfile, protectfile) elif not filenames: return [] else: # just to be sure, strip the filenames (newlines!) command = p4.p4 + ' -G files ' + ' '.join(map(string.strip, filenames)) filelist = p4.executeM(command) # remove deleted files filelist = filter(lambda x: x['action'] != 'delete', filelist) # add depotPath to all items for item in filelist: item['depotPath'] = depotFileToDepotPath(item['depotFile']) # sort by depotPath filelist.sort(compare_depotPath) # Retrieve file contents from perforce depot. filenames = map(lambda item: item['depotFile'] + '@' + str(item['change']), filelist) data = p4.readM(filenames) # copy file content from elements of 'data' to the elements of 'filelist' for filename, element in zip(filenames, filelist): element['content'] = data[filename]['content'] return filelist def generate_localprotections(filelist, allow_superuser=0, allow_absolutepath=0): '''Recursive backend for generate_protections(). Returns a tuple (remainingfilelist, mainblock, protectblock) where remainingfilelist contains all elements of filelist not processed by this call, mainblock contains default and enforce sections and protectblock contains protect sections for files that belong to the hierarchy of the first file in filelist. This assumes that filelist is sorted. ''' if not filelist: return ([], '', '') file = filelist.pop(0) changenr = file['change'] filename = file['depotFile'] + '@' + str(changenr) path = file['depotPath'] content = file['content'] try: sections = parse_protectfile(content, filename, path, changenr, allow_superuser, allow_absolutepath) except: complain_to_submitter(mailport(), filename, changenr, ('An error occured when parsing %s.\n' % filename) +\ 'This file is therefore not included in the updated protections.\n' +\ ('Exception Info: %s' % str(sys.exc_info()[1])) ) skipped = '# File %s was skipped due to a parse error.%s' % (filename, newline) sections = {'default':[skipped], 'enforce':[skipped], 'protect':[skipped]} mainblock = '' protectblock = '' if sections['default']: mainblock += '# default section from ' + filename + newline mainblock += newline.join(sections['default']) mainblock += newline # recurse: process all protectfiles below the current path while filelist and (filelist[0]['depotPath'].find(path) == 0): filelist, newmainblock, newprotectblock = generate_localprotections(filelist) mainblock += newmainblock protectblock += newprotectblock if sections['enforce']: mainblock += '# enforce section from ' + filename + newline mainblock += newline.join(sections['enforce']) mainblock += newline if sections['protect']: protectblock += '# protect section from ' + filename + newline protectblock += newline.join(sections['protect']) protectblock += newline return filelist, mainblock, protectblock def generate_protections(filelist): '''Generate the protections from given list containing dictionaries with file data. The generated protect file is returned as a string. ''' lastchange = reduce(lambda change, item: max(change, int(item['change'])), filelist, 0) header = '# Generated by decentprotect.py on %s.%s# Last change: %s.%sProtections:' % \ (time.strftime(timeformat, time.localtime()), newline, str(lastchange), newline + newline) # delegate the real work to recursive back-end filelist, mainblock, protectblock = generate_localprotections(filelist, 1, 1) return header + newline + \ mainblock + newline + \ protectblock + newline + \ append_block def applyprotections(protections): '''Applies the protections in Perforce. Returns True if protections were actually modified, False otherwise.''' # Will provide regular text as input. (p4 cmd without -G option) # So I need to interpret the output text of the regular command command = p4.p4 + ' protect -i' # outstream is stdout + stderr instream, outstream = os.popen4(command, 't') try: instream.write(protections) instream.close() output = outstream.read() finally: instream.close() # duplicate, in case the write fails outstream.close() if output == 'Protections not changed.\n': return False elif output == 'Protections saved.\n': return True else: raise Exception('The command "%s" gives unexpected results:\n%s' % \ (command, output)) def getNewChanges(): '''Returns the list of new change descriptions, submitted since the last processed change. Uses p4 -G review -t protect.''' reviewlist = p4.review_t(counter) changenrs = map(lambda x: x['change'], reviewlist) return p4.describeM(changenrs) def writeCacheFile(filelist=None): '''Stores the list of files in the cache file (a shelf). If no filelist is provided, it is retrieved from perforce,''' cache = shelve.open(cachefile, flag='n') try: if not filelist: filelist = get_protectfiles() for item in filelist: key = item['depotPath'] cache[key] = item finally: cache.close() log('Cache file (re-)created with %d items' % len(filelist)) def updateCacheFile(updates, deletes): '''Writes given updates to the cache file. deletes is a list of filenames. updates is a list of dictionaries containing metadata and contents of protect files.''' if not updates and not deletes: return None cache = shelve.open(cachefile, flag='w') try: for fname in deletes: key = depotFileToDepotPath(fname) if key in cache: del cache[key] for item in updates: key = item['depotPath'] cache[key] = item finally: cache.close() def verifyCacheFile(filelist=None): '''Compares the list of files with the contents of the cache. If no filelist is provided, it is retrieved from perforce, Returns differences as a list of tuples (filename, code) where code can be "differs", "missing" or "deleted".''' result = [] cache = shelve.open(cachefile, flag='r') try: if not filelist: filelist = get_protectfiles() keyset = {} # set() # for the keys found in filelist for item in filelist: key = item['depotPath'] keyset[key] = key # keyset.add(key) if key not in cache: result.append( (item['depotFile'], 'missing') ) elif cmp(item, cache[key]): result.append( (item['depotFile'], 'differs') ) for key in cache.keys(): if key not in keyset: item = cache[key] result.append( (item['depotFile'], 'deleted') ) finally: cache.close() return result def fixCacheFile(): '''Verifies the cache file and re-writes it when inconsistencies are found. Returns differences found by verifyCacheFile.''' filelist = get_protectfiles() result = verifyCacheFile(filelist) if result: log('Inconsistencies found in cache file:\n%s' % '\n'.join(map(lambda r: r[1] + ': ' + r[0], result))) writeCacheFile(filelist) return result def processChange(change): '''Reads any protect files contained in the given change description and updates the cache file accordingly. Returns the list of protect files contained in the change.''' newandupdatedfiles, deletedfiles = protectFilesInChange(change) if newandupdatedfiles: filelist = get_protectfiles(newandupdatedfiles) else: filelist = [] updateCacheFile(filelist, deletedfiles) return newandupdatedfiles + deletedfiles def updateProtections(dry_run=True): '''Generates protections and (if dry_run==False) applies the protections in Perforce. Returns True when protections in Perforce have been modified, False otherwise.''' # Note: dry_run=True by default, for safety reasons # read cache contents cache = shelve.open(cachefile, flag='r') try: filelist = [] for value in cache.itervalues(): filelist.append(value) finally: cache.close() # sort by depotPath filelist.sort(compare_depotPath) protections = generate_protections(filelist) if save_to_file: file = open(save_to_file, 'wb') try: file.write(protections) finally: file.close() if not dry_run: return applyprotections(protections) else: return False def reportSuccess(change, duration, updateditems): '''Write a short report to the logfile and also send it by mail to the administrator, (if that is what she wants)''' try: # in order to report latency, ask current time from server (just in case clocks are not synced) # format: "2008/02/05 10:30:01 +0100 W. Europe Standard Time" p4serverDate = p4.info()['serverDate'] localtime = time.time() # remove the timezone info (time module won't understand it) p4serverDate = ' '.join(p4serverDate.split()[:2]) p4time = time.mktime(time.strptime(p4serverDate, '%Y/%m/%d %H:%M:%S')) secondsSinceSubmit = p4time - int(change['time']) durationSinceSubmit = formatDuration(secondsSinceSubmit) if abs(p4time - localtime) > 2: # this has nothing to do with permissions anymore... p4timediff = 'Time on the Perforce Server differs from localtime by approximately %s.\n' % formatDuration(p4time - localtime) else: p4timediff = '' except: durationSinceSubmit = '(error)' p4timediff = '' logline = 'change %s processed %s after submit (processing duration %.1f s)\n%s%s' % \ ( str(change['change']), durationSinceSubmit, duration, p4timediff, '\n'.join(map(lambda x: '\t' + x, updateditems))) log(logline) # send administrator a notice of the statistics if mail_statistics and administrator: mailport().sendmail('"Perforce Protect Daemon" <%s>' % administrator, [administrator], 'Subject: Perforce Protect Daemon statistics\n\n' + logline) def loop_body(): '''Body of the main loop. Checks whether some protect files have been changed, If that is the case, the cache is updated and new protections are generated and stored.''' global retrydelay try: newChanges = getNewChanges() for change in newChanges: starttime = time.time() updateditems = processChange(change) if updateditems: updateProtections(dry_run=bool(justtesting)) reportSuccess(change, time.time() - starttime, updateditems) p4.setcounter(counter, change['change']) # reset retrydelay retrydelay = 0 except: body = formatException(sys.exc_info()) if repeat: # use increasing delays before retrying... retrydelay = min(max_retry_delay, max(min_retry_delay, retrydelay * 2)) body = "Will retry after %s.\n" % (formatDuration(retrydelay)) + body complain(mailport(), 'An error occured. The protections may not have been updated.\n%s\n' % body) # clean up smtp connection closemailport() if retrydelay: time.sleep(retrydelay) def usage(): '''print usage information''' print """\ Usage (initial setup and testing): decentprotect -c | -f | -h | -p | -P | -u | -v Usage (as a deamon): decentprotect -D Options: -c Create a new cache file from the perforce depot. -D Work as a deamon -f Fix the cache if inconsistencies with the perforce depot are found, inconsistencies are reported. -h print usage info. -u Update the cache based on the given changenr -p Generate protections based on the cache, but don't apply them in perforce (dry-run). -P Generate protections based on the cache and apply them in perforce. -v Verify the cache with the perforce depot and report any inconsistencies. Notes: """ print " * The name of the cache file is %s" % cachefile if save_to_file: print " * For -p and -P generated protections are saved to %s" % save_to_file if justtesting: print " =============================================================" print " * Now working in TEST MODE: no changes will be made in Perforce" print " =============================================================" print "" if __name__ == '__main__': if len(sys.argv) > 1: opts, args = getopt.getopt(sys.argv[1:], 'cDfhHpPvu:') if args or len(opts) != 1: raise Exception('Unrecognized commandline arguments. (-h for usage).') option, value = opts[0] if option == '-c': writeCacheFile() elif option == '-f': problems = fixCacheFile() for f, c in problems: print c + ':', f if problems: print "The cache has been re-created." elif option == '-v': problems = verifyCacheFile() for f, c in problems: print c + ':', f elif option == '-u': change = p4.describe(int(value)) processChange(change) elif option == '-p': updateProtections(dry_run=True) elif option == '-P': if updateProtections(): print 'Protections saved.\n' else: print 'Protections not changed.\n' elif option == '-D': # work as a deamon if repeat: log('Starting main loop (%ds sleeptime)' % sleeptime) while (repeat): loop_body() time.sleep(sleeptime) else: loop_body() else: usage() else: usage()