#!/usr/local/bin/python # # This script is intended to give some degree of version control and user # permissions on jobs. # It is currently under development and is neither complete nor of "production" # quality. Please bear that in mind when using it. I would STRONGLY advise # running it against a test server initially, just to check that it will do # what you expect it to before turning it loose on your live system since # some of the operation it performs can be destructive, deleting Perforce # jobs for example. # # The script is intended to be run either from a scheduling app, like cron, # or by setting the "repeat" variable to "1" and the "runinterval" variable # to the number of seconds between runs. # # In order to use this script you must enable the "p4 logger". # To do this simply run: # # p4 counter logger 0 # # from a Perforce command line client. # # Due to the use this script makes of the logger functionality, running it in # conjunction with P4DTI and possibly any other defect tracker will cause both # to misbehave and miss data. SO DON'T DO IT!! # # Since this script will create a file for each job with the same name as the # job you should be careful to check that all of the job names you have are # valid file names on your OS. If you are using the default job naming scheme # of job00001, job000002, job000003, etc. then this will be fine, if however # you give your jobs other, possibly more descriptive, names (eg. Semaphore?) # then this is something you should be aware of. # If you do need to rename jobs for this reason then please do all of this # BEFORE enabling the Perforce logger mechanism, otherwise the script will try # to version jobs which no longer exist. # # Please bear in mind that the one operation this script can have no control # over is unauthorised users deleting jobs from Perforce. This is because by # the time the daemon comes to check the job, there is no job to check!! # In this case the daemon will log this fact to the command line and the log # file and mark the head rev of the versioned job file as deleted. This way # you could manually reinstate the Perforce job from the versioned file if # you wanted to. # # # A few configuration tips: # # 1) The "localpath" variable needs the double slashes (\\) on Windows machines # otherwise Python interprets them differently. You do NOT need double # forward slashes (//) in a Unix environment. # This variable should also match the "root" value of the client used to run # the script. # NOTE: you will have to create this directory manually as the script won't # do it for you. Perhaps this is something which can bee added to the # script in a future version. # # 2) The "client" variable should be set to the name of a precreated client on # the machine which will run this script. # # 3) The "user" variable should be a user with write permission on the area of # the depot where the versioned job files will be stored, ie. the "depotpath". # # 4) The "authorised" variable contains a list of the names of users who are # authorised to alter jobs. To have multiple users authorised to alter jobs # then include them all in the "authorised" variable, like so: # 'authorised': ['steve','admin','perforce'] # # 5) The "userfield" variable should be the name of a field in your jobspec # which is always set to the name of the last user to edit the job. # e.g. 109 Altered word 32 always # and should have a "Presets:" value of $user, e.g. Altered $user # THIS PART OF THE CONFIGURATION IS VITAL. WITHOUT THIS SUPPORT IN THE JOBSPEC # THE AUTHORISATION OF JOBS SIMPLY WON'T WORK AND THE SCRIPT WILL FAIL. # # # Config Variables - What they do. # user Perforce user to run commands as # client Perforce client to sync files to # localpath local client files directory (same directory as client root) # jvpath depot path where versioned job files are stored # authorised list of users authorised to make changes to jobs # userfield the field to check for who last edited the job # repeat loop the script automatically # runinterval if so how long in seconds between runs # p4 the path to your p4 command # changedesc the text of the description field in submitted changelists # log a file to logg message in. Leave blank if you want # them all sent to stdout. # add a job added by an unauthorised user should be # added (add=1) or deleted (add=0). # revert a job altered by an unauthorised user should be # reverted (revert=1) in the Perforce job or ignored (revert=0). # I the changes are ignored in the Perforce job then the versioned # file which contains the job details will NOT be updated to reflect # the new contents of the job. This will only happen when the job is # altered by an authorised user. from marshal import load from os import popen from re import match from string import count from time import ctime, time, sleep config = {'user': 'steve', 'client': 'review-daemon', 'localpath': 'c:\\localwork\\vjobs\\', 'jvpath': '//depot/vjobs/', 'authorised': ['steve'], 'userfield': 'Altered', 'repeat': 1, 'runinterval': 30, 'p4': 'p4', 'changedesc': 'Jobs altered/created by Job Versioning Daemon', 'log': 'c:\\localwork\\vjobs\\vjobs.log', 'add': 1, 'revert': 0} class P4Command: def __init__(self): self.command = config['p4'] + \ ' -u ' + config['user'] + \ ' -c ' + config['client'] + ' ' def P4Run(self, args, mode = 'r'): return popen(self.command + args, mode) def P4Marshal(self, args): return load(popen(self.command + ' -G ' + args, 'rb')) class P4Job(P4Command): def __init__(self, jobname): P4Command.__init__(self) self.name = '' self.dict = {} self.have = '' self.action = '' self.name = jobname self.have = self.P4Run(' have ' + config['localpath'] + self.name).readline() self.dict = self.P4Marshal('job -o ' + self.name) # The message handling mechanism is currently pretty crude and simply # prints out what ever text it is sent to stdout and a log file if one # has been specified in the config variables. # The next step is to alter this to an email mechanism which will mail # the interested parties for each event of note and log them as well. def HandleMessage(self, msg): if config['log']: fh = open(config['log'], 'a') fh.write(ctime(time()) + ' - ' + msg + '\n') fh.close() return print ctime(time()) + ' - ' + msg + '\n' def GetAction(self): if not self.have and not self.dict: self.action = 'error' return if self.have and ((self.dict['code'] == 'error') or not self.dict): self.action = 'delete' return if not self.have and self.dict: self.action = 'add' del self.dict['code'] return del self.dict['code'] self.action = 'edit' def TakeAction(self): if self.action == 'error': self.HandleMessage('Error with job ' + self.name + '.\n' + \ 'The job was probably created and deleted between runs.') return if self.action == 'delete': self.DeleteJobFile('Delete job file ' + self.name) return if self.IsAuth(): if self.action == 'add': self.AddJobFile('Add job file ' + self.name) return self.EditJob('Edit job file ' + self.name) return if self.action == 'add': if config['add']: self.AddJobFile('Add job file ' + self.name + \ '.\nThis job was created by an unauthorised user!') return self.DeleteJob('Job ' + self.name + ' was created by an ' + \ 'unauthorised user.\n Deleting job from Perforce.') return if config['revert']: self.RevertJob('Job ' + self.name + ' altered by an ' + \ 'unauthorised user. Changes reverted in Perforce job.') return self.HandleMessage('Job ' + self.name + ' was altered by an ' + \ 'unauthorised user.\n' + \ 'The changes won\'t be written to the job file.') def IsAuth(self): if self.dict[config['userfield']] in config['authorised']: return 1 return 0 def EditJob(self, msg = None): if msg: self.HandleMessage(msg) self.P4Run('edit ' + config['jvpath'] + self.name) self.WriteJobFile() def DeleteJob(self, msg = None): if msg: msg = msg + '\n\n' for key in self.dict.keys: if key == 'code': continue msg = msg + key + ': ' + self.dict[key] + '\n' self.HandleMessage(msg) self.P4Run('job -d ' + self.name) def RevertJob(self, msg = None): if msg: self.HandleMessage(msg) fh = self.P4Run('job -i -f', 'w') for line in open(config['localpath'] + self.name, 'r').readlines(): if '\t' not in line: line = '\t' + line fh.write(line) fh.close() def DeleteJobFile(self, msg = None): if msg: self.HandleMessage(msg) self.P4Run('delete ' + config['localpath'] + self.name) def WriteJobFile(self): fh = open(config['localpath'] + self.name, 'w') for key in self.dict.keys(): fh.write(key + ':\t' + str(self.dict[key]) + '\n') fh.close() def AddJobFile(self, msg = None): if msg: self.HandleMessage(msg) self.WriteJobFile() self.P4Run('add ' + config['jvpath'] + self.name) class P4Logger(P4Command): def __init__(self): P4Command.__init__(self) self.joblist = [] for line in self.P4Run('logger').readlines(): (type, job) = match(r'^\d+ (\S+) (.+)', line).groups() if (type == 'change') or (job in self.joblist): continue self.joblist.append(job) def CleanUp(self): counter = self.P4Run('counter logger').readline() self.P4Run('logger -c ' + counter[:-1] + ' -t logger') class P4JobVersion(P4Command): def __init__(self): '''The class constructor''' # We actually just want to call the base classes constructor P4Command.__init__(self) def InitialAdd(self): '''Check whether we currently have any versioned files, if not then add all of the current Perforce jobs as new versioned files and submit them''' if self.P4Run('have ' + config['jvpath'] + '...').readline(): # Tell the caller that we do indeed already have versioned job files. return 0 # For each job in Perforce for line in self.P4Run('jobs').readlines(): # Find the job name jobname = match(r'(\S+) on', line).group(1) # Create a new P4Job object for this job thisjob = P4Job(jobname) # Create a file for this job and add it to Perforce thisjob.AddJobFile() # Submit the added files self.SubmitFiles() # Tell the caller that we have added and # submitted all the existing Perforce jobs. return 1 def SubmitFiles(self): '''Submit the default changelist with a standard change description''' # Initialise a list to hold the change form newchange = [] # for each line in the change form... for changeline in self.P4Run('change -o').readlines(): # check for the standard change description in this line if count(changeline, "") > 0: # if its in this line then change this line to # the default job versioner change description. changeline = '\t%s\n' % config['changedesc'] # Add this line to the list variable which contains the new change form newchange.append(changeline) # Run the submit and pass in the chage form from the # list variable instead of invoking the users editor self.P4Run('submit -i', 'w').writelines(newchange) def MainLoop(self): '''This is the main loop which handles the version of the job files''' # Create a new P4Logger object logger = P4Logger() # For each job in the logger objects joblist for job in logger.joblist: # Create a new P4Job object for this job, passing in the jobs name. thisjob = P4Job(job) # Work out what action has occured to this job in Perforce thisjob.GetAction() # Perform the neccesary steps to complete the action taken # with this job in Perforce thisjob.TakeAction() # We have finished with this particular job, so delete the object del thisjob # Once we have taken the relevent action for all the jobs in the # loggers joblist we submit any changes pending in the default changelist self.SubmitFiles() # Run the clear up routine for the logger object logger.CleanUp() # Delete the logger object del logger ########## ## Main ## ########## #if the repeat variable is set we loop forever while config['repeat']: # create a new P4JobVersion object versioner = P4JobVersion() # if the script has never been run before then add all # the files for the existing jobs, otherwise... if not versioner.InitialAdd(): # Run the main versioning loop versioner.MainLoop() # we've finished with this versioner, so delete it. del versioner # Inform the user what is going on print 'Sleeping for %d seconds' % config['runinterval'] # wait for the specified number of seconds before running this loop again sleep(config['runinterval']) # if the repeat variable is not set then only run once else: # create a new P4JobVersion object versioner = P4JobVersion() # if the script has never been run before then add all # the files for the existing jobs, otherwise... if not versioner.InitialAdd(): # Run the main versioning loop versioner.MainLoop()