def __init__(self, name): self.locked = False self.name = name self.tempFilePath = os.path.join(BASEDIR, "{}.lock".format(self.name)) self.tempFilePath = os.path.abspath(self.tempFilePath) logger.info("Temp File: %s", self.tempFilePath) #Windows if sys.platform == "win32": try: if os.path.exists(self.tempFilePath): os.unlink(self.tempFilePath) logger.debug("Unlink %s", self.tempFilePath) self.tempFile = os.open(self.tempFilePath, os.O_CREAT | os.O_EXCL | os.O_RDWR) self.locked = True except Exception as e: if e.errno == 13: logger.error("Another Instance of %s is already running!", self.name) else: logger.error(e) #Linux else: import fcntl self.tempFile = open(self.tempFilePath, "w") self.tempFile.flush() try: fcntl.lockf(self.tempFile, fcntl.LOCK_EX | fcntl.LOCK_NB) self.locked = True except IOError: logger.error("Another Instance of %s is already running", self.name)
def offlineThisNodeHandler(self): response = self.thisNode.offline() self.updateThisNodeInfo() if response: logger.info("Node Offlined") else: logger.error("Node could not be Offlined!")
def getOffThisNodeHandler(self): response = self.thisNode.getOff() self.updateThisNodeInfo() if response: logger.info("Node Offlined and Task Killed") else: logger.error("Node could not be Onlined or the Task could not be killed!")
def softwareUpdater(): hydraPath = os.getenv("HYDRA") if not hydraPath: logger.error("HYDRA enviromental variable does not exit!") return False hydraPath, thisVersion = os.path.split(hydraPath) try: currentVersion = float(thisVersion.split("_")[-1]) except ValueError: logger.warning("Unable to obtain version number from file path. Assuming version number from Constants") currentVersion = Constants.VERSION versions = os.listdir(hydraPath) versions = [float(x.split("_")[-1]) for x in versions if x.startswith("dist_")] if not versions: return False highestVersion = max(versions) logger.debug("Comparing versions. Env: %s Latest: %s", currentVersion, highestVersion) if highestVersion > currentVersion: logger.info("Update found! Current Version is %s / New Version is %s", currentVersion, highestVersion) newPath = os.path.join(hydraPath, "dist_{}".format(highestVersion)) response = changeHydraEnviron(newPath) if not response: logger.critical("Could not update to newest environ for some reason!") return response else: return False
def unstickTask(self): """Cleanup task if the node starts with one assigned to it (Like if the node crashed/restarted)""" #self.thisNode will be updated in in init statement if self.thisNode.task_id: logger.info("Rouge task discovered. Unsticking...") self.HydraTask = hydra_taskboard.fetch("WHERE id = %s", (self.thisNode.task_id,), cols=["id", "job_id", "renderLayer", "status", "exitCode", "endTime", "host", "currentFrame"]) self.HydraJob = hydra_jobboard.fetch("WHERE id = %s", (self.HydraTask.job_id,), cols=["jobType", "renderLayerTracker"]) self.logPath = self.HydraTask.getLogPath() self.HydraTask.kill(CRASHED, False) self.progressUpdate() self.thisNode.status = IDLE if self.thisNode.status == STARTED else OFFLINE self.thisNode.task_id = None elif self.thisNode.status in [STARTED, PENDING]: logger.warning("Reseting bad status, node set %s but no task found!", self.thisNode.status) self.thisNode.status = IDLE if self.thisNode.status == STARTED else OFFLINE
def __init__(self): QMainWindow.__init__(self) self.setupUi(self) #Class Variables self.thisNodeName = Utils.myHostName() logger.info("This host is %s", self.thisNodeName) self.username = Utils.getInfoFromCFG("database", "username") self.userFilter = False self.showArchivedFilter = False self.statusMsg = "ERROR" self.currentJobSel = None with open(Utils.findResource("styleSheet.css"), "r") as myStyles: self.setStyleSheet(myStyles.read()) #My UI Setup Functions self.setupTreeViews() self.connectButtons() self.setupHotkeys() self.setWindowIcon(QIcon(Utils.findResource("Images/FarmView.png"))) #Make sure this node exists self.thisNodeButtonsEnabled = True self.thisNodeExists = self.findThisNode() #Setup Signals SIGNAL("doUpdate") QObject.connect(self, SIGNAL("doUpdate"), self.doUpdate) #Start autoUpdater and then fetch data from DB self.autoUpdateThread = stoppableThread(self.doUpdateSignaler, 10, "AutoUpdate_Thread") self.doFetch()
def tgt(self): while not self.stopEvent.is_set(): #target = schedulerMain from the RenderNodeMainUI class self.interval = self.targetFunction() hours = int(self.interval / 60 / 60) minutes = int(self.interval / 60 % 60) logger.info("Scheduler Sleeping for %d hours and %d minutes", hours, minutes) self.stopEvent.wait(self.interval)
def killTask(self, statusAfterDeath=KILLED): if self.status == "S" and self.task_id: taskOBJ = hydra_taskboard.fetch("WHERE id = %s", self.task_id, cols=["host", "status", "exitCode", "endTime"]) logger.debug("Killing task %d on %s", self.task_id, self.host) return taskOBJ.kill(statusAfterDeath, True) else: logger.info("No task to kill on %s", self.host) return True
def shutdown(self): logger.info("Shutting down...") if self.schedThreadStatus: self.schedThread.terminate() if self.autoUpdateStatus: self.autoUpdateThread.terminate() if self.renderServerStatus: self.renderServer.shutdown() logger.debug("All Servers Shutdown") self.trayIcon.hide() event.accept() sys.exit(0)
def update(self, trans): names = list(self.__dirty__) if not names: logger.info("Nothing to update on %s", self.tableName()) return values = ([getattr(self, n) for n in names] + [getattr(self, self.primaryKey)]) assignments = ", ".join(["{} = %s".format(n) for n in names]) query = "UPDATE {0} SET {1} WHERE {2} = %s".format(self.tableName(), assignments, self.primaryKey) logger.debug((query, values)) trans.cur.executemany(query, [values]) return True
def startScheduleThread(self): """This is in it's own function because it starts and stops often""" #pylint: disable=W0703,W0201 if bool(self.currentScheduleEnabled) and self.currentSchedule: self.schedThread = schedulerThread(self.schedulerMain, "Schedule_Thread", None) self.schedThreadStatus = True self.scheduleThreadPixmap.setPixmap(self.donePixmap) logger.info("Schedule Thread started!") else: logger.info("Schedule disabled. Running in manual control mode.") self.scheduleThreadPixmap.setPixmap(self.nonePixmap)
def changeHydraEnviron(newEnviron): if sys.platform == "win32": logger.info("Changing Hydra Environ to %s", newEnviron) proc = subprocess.Popen(["setx", "HYDRA", newEnviron], stdout=subprocess.PIPE) out, _ = proc.communicate() if out.find("SUCCESS") > 0: os.environ["HYDRA"] = newEnviron return True else: logger.critical("Could not change enviromental variable!") return False else: raise "Not Implemented!"
def main(): logger.info("Starting in %s", os.getcwd()) logger.info("arglist %s", sys.argv) #Check for other RenderNode isntances lockFile = InstanceLock("HydraRenderManager") lockStatus = lockFile.isLocked() logger.debug("Lock File Status: %s", lockStatus) if not lockStatus: logger.critical("Only one RenderManager is allowed to run at a time! Exiting...") sys.exit(-1) socketServer = RenderManagementServer() socketServer.createIdleLoop("Process_Render_Tasks_Thread", socketServer.processRenderTasks, interval=15)
def __init__(self): #Setup Class Variables self.renderThread = None self.childProcess = None self.PSUtilProc = None self.statusAfterDeath = None self.childKilled = 0 self.HydraJob = None self.HydraTask = None self.logPath = None #Get this node data from the database and make sure it exists self.thisNode = getThisNodeOBJ() logger.debug(self.thisNode) if not self.thisNode: logger.critical( "This node does not exist in the database! Please Register this node and try again." ) sys.exit(-1) return #Detect RedShift GPUs self.rsGPUs = Utils.getRedshiftPreference("SelectedCudaDevices") if self.rsGPUs: self.rsGPUs = self.rsGPUs.split(",")[:-1] self.rsGPUids = [x.split(":")[0] for x in self.rsGPUs] if len(self.rsGPUs) != len(self.rsGPUids): logger.warning("Problems parsing Redshift Preferences") logger.info("%s Redshift Enabled GPU(s) found on this node", len(self.rsGPUs)) logger.debug("GPUs available for rendering are %s", self.rsGPUs) else: logger.warning("Could not find available Redshift GPUs") #Create RenderLog Directory if it doesn't exit if not os.path.isdir(Constants.RENDERLOGDIR): os.makedirs(Constants.RENDERLOGDIR) self.unstickTask() self.thisNode.software_version = Constants.VERSION with transaction() as t: self.thisNode.update(t) #Run The Server port = int(Utils.getInfoFromCFG("network", "port")) self.startServerThread(port)
def getAverageRenderTime(self): totalTime = self.getTotalRenderTime() if totalTime: frameCount = self.getTotalFrameCount() frameTime = totalTime / frameCount seconds = int(frameTime.total_seconds()) else: frameTimes = self.getEachFrameRenderTime() if not frameTimes: logger.info("Could not find any frame render times in %s", self.fp) return None #Add up seconds, divide by frame count seconds = sum([int(ft.total_seconds()) for ft in frameTimes]) / len(frameTimes) frameTime = datetime.timedelta(seconds=seconds) return frameTime
def nodeEditorHandler(self): response = self.nodeEditor() if response: logger.info("Updating this node...") logger.info("Node updated!") else: logger.info("No changes detected. Nothing was changed.")
def startupServers(self): logger.debug("Firing up main threads") #Start Render Server self.renderServer = RenderNode.RenderTCPServer() self.renderServerStatus = True self.renderServerPixmap.setPixmap(self.donePixmap) logger.info("Render Server Started!") #Start Pulse Thread self.renderServer.createIdleLoop("Pulse_Thread", pulse, 60) self.pulseThreadStatus = True self.pulseThreadPixmap.setPixmap(self.donePixmap) logger.info("Pulse Thread started!") #Start Auto Update Thread SIGNAL("updateThisNodeInfo") QObject.connect(self, SIGNAL("updateThisNodeInfo"), self.updateThisNodeInfo) self.autoUpdateStatus = True self.autoUpdateThread = stoppableThread(self.updateThisNodeInfoSignaler, 15, "AutoUpdate_Thread") self.startScheduleThread()
def getInfoFromCFG(section, option): """Return information from the local configuration file.""" config = ConfigParser.RawConfigParser() #Create a copy if it doesn't exist if not os.path.exists(Constants.SETTINGS): folder = os.path.dirname(Constants.SETTINGS) logger.info("Check for folder %s", folder) if os.path.exists(folder): logger.info("%s Exists", folder) else: logger.info("Make %s", folder) os.mkdir(folder) cfgFile = findResource(os.path.basename(Constants.SETTINGS)) logger.info("Copy %s", cfgFile) shutil.copyfile(cfgFile, Constants.SETTINGS) config.read(Constants.SETTINGS) return config.get(section=section, option=option)
def shutdown(self): """Offline node, Kill current job, shutdown servers, reset node status""" currentStatus = self.thisNode.status self.thisNode.offline() if currentStatus in [STARTED, PENDING]: logger.info("Attempting to kill current job.") self.killCurrentJob(KILLED) logger.info("Kill Response Code: %s", self.childKilled) TCPServer.shutdown(self) #Online AFTER servers are shutdown if currentStatus == STARTED: self.thisNode.online() logger.info("RenderNode Servers Shutdown")
def __init__(self): QMainWindow.__init__(self) self.setupUi(self) with open(Utils.findResource("styleSheet.css"), "r") as myStyles: self.setStyleSheet(myStyles.read()) self.thisNode = NodeUtils.getThisNodeOBJ() self.isVisable = True self.pulseThreadStatus = False self.renderServerStatus = False self.schedThreadStatus = False self.autoUpdateStatus = False if not self.thisNode: self.offlineButton.setEnabled(False) self.getoffButton.setEnabled(False) logger.error("Node does not exist in database!") aboutBox(self, "Error", "This node was not found in the database! If you wish to render " "on this node it must be registered with the databse. Run " "Register.exe or Register.py to regiester this node and " " try again.") sys.exit(1) self.currentSchedule = self.thisNode.weekSchedule self.currentScheduleEnabled = self.thisNode.scheduleEnabled self.buildUI() self.connectButtons() self.updateThisNodeInfo() self.startupServers() logger.info("Render Node Main is live! Waiting for tasks...") try: autoHide = True if str(sys.argv[1]).lower() == "true" else False logger.info(autoHide) except IndexError: autoHide = False if autoHide and self.trayIconBool: logger.info("Autohide is enabled!") self.sendToTrayHandler() else: self.show()
def killCurrentJob(self, statusAfterDeath): """Kills the render node's current job if it's running one. Return Codes: 1 = process killed, -1 = parent could not be killed, -9 = child could not be killed, -10 = child and parent could not be killed""" self.statusAfterDeath = statusAfterDeath self.childKilled = 1 if not self.childProcess or not self.PSUtilProc: logger.info("No task is running!") return #Gather subprocesses just in case if self.PSUtilProc.is_running(): childrenProcs = self.PSUtilProc.children(recursive=True) else: logger.info( "PID '%s' could not be found! Task is probably already dead.", self.childProcess.pid) return #Try to kill the main process #terminate() = SIGTERM, kill() = SIGKILL logger.info("Killing main task with PID %s", self.PSUtilProc.pid) self.PSUtilProc.terminate() _, alive = psutil.wait_procs([self.PSUtilProc], timeout=15) if len(alive) > 0: self.PSUtilProc.kill() _, alive = psutil.wait_procs([self.PSUtilProc], timeout=15) if len(alive) > 0: logger.error("Could not kill PID %s", self.PSUtilProc.pid) self.childKilled = -1 #Try to kill the children if they are still running _ = [proc.terminate() for proc in childrenProcs if proc.is_running()] _, alive = psutil.wait_procs(childrenProcs, timeout=15) if len(alive) > 0: _ = [proc.kill() for proc in alive] _, alive = psutil.wait_procs(alive, timeout=15) if len(alive) > 0: #ADD negative 10 to the return code self.childKilled += -10
while not self.stopEvent.is_set(): #target = schedulerMain from the RenderNodeMainUI class self.interval = self.targetFunction() hours = int(self.interval / 60 / 60) minutes = int(self.interval / 60 % 60) logger.info("Scheduler Sleeping for %d hours and %d minutes", hours, minutes) self.stopEvent.wait(self.interval) def pulse(): host = Utils.myHostName() with transaction() as t: t.cur.execute("UPDATE hydra_rendernode SET pulse = NOW() " "WHERE host = '{0}'".format(host)) if __name__ == "__main__": logger.info("Starting in %s", os.getcwd()) logger.info("arglist is %s", sys.argv) app = QApplication(sys.argv) app.quitOnLastWindowClosed = False lockFile = InstanceLock("HydraRenderNode") lockStatus = lockFile.isLocked() logger.debug("Lock File Status: %s", lockStatus) if not lockStatus: logger.critical("Only one RenderNode is allowed to run at a time! Exiting...") aboutBox(None, "ERROR", "Only one RenderNode is allowed to run at a time! Exiting...") sys.exit(-1) window = RenderNodeMainUI() retcode = app.exec_()
def shutdownEvent(self): logger.info("Triggering Shutdown Event") self.offlineThisNodeHandler()
def startupEvent(self): logger.info("Triggering Startup Event") self.onlineThisNodeHandler()
def clearOutputHandler(self): choice = yesNoBox(self, "Confirm", "Really clear output?") if choice == QMessageBox.Yes: self.outputTextEdit.clear() logger.info("Output cleared")
"""Registers a node with the database.""" #Standard import os import sys #Third Party #pylint: disable=E0611 from MySQLdb import IntegrityError #Hydra from Setups.LoggingSetup import logger from Setups.MySQLSetup import hydra_rendernode, OFFLINE, transaction import Utilities.Utils as Utils if __name__ == "__main__": me = Utils.myHostName() hydraPath, execFile = os.path.split(sys.argv[0]) logger.info(hydraPath) response = Utils.changeHydraEnviron(hydraPath) if response: try: with transaction() as t: hydra_rendernode(host=me, status=OFFLINE, minPriority=0).insert(t) except IntegrityError: logger.info("Host %s already exists in the hydra_rendernode table on the databse", me) else: logger.error("Could not set Hydra Environ! No changes where made. Exiting...") raw_input("\nPress enter to exit...")
def storeCredentials(username, _password): keyring.set_password("Hydra", username, _password) logger.info("Password Stored in Credentials Vault")
def launchRenderTask(self, HydraJob, HydraTask): """Does the actual rendering, then records the results on the database""" logger.info("Starting task with id %s on job with id %s", HydraTask.id, HydraJob.id) self.HydraJob = HydraJob self.HydraTask = HydraTask self.childKilled = 0 self.statusAfterDeath = None self.childProcess = None self.PSUtilProc = None originalCurrentFrame = int(self.HydraTask.currentFrame) renderTaskCMD = self.HydraTask.createTaskCMD(self.HydraJob, sys.platform) logger.debug(renderTaskCMD) self.logPath = self.HydraTask.getLogPath() logger.info("Starting render task %s", self.HydraTask.id) try: log = file(self.logPath, 'w') except (IOError, OSError, WindowsError) as e: logger.error(e) self.thisNode.getOff() return log.write('Hydra log file {0} on {1}\n'.format(self.logPath, self.HydraTask.host)) log.write('RenderNode is {0}\n'.format(sys.argv)) log.write('Command: {0}\n\n'.format(renderTaskCMD)) Utils.flushOut(log) progressUpdateThread = stoppableThread(self.progressUpdate, 300, "Progress_Update_Thread") #Run the job and keep track of the process self.childProcess = subprocess.Popen(renderTaskCMD, stdout=log, stderr=log, **Utils.buildSubprocessArgs(False)) logger.info("Started PID %s to do Task %s", self.childProcess.pid, self.HydraTask.id) self.PSUtilProc = psutil.Process(self.childProcess.pid) #Wait for task to finish self.childProcess.communicate() #Get Exit Code, Record the results self.HydraTask.exitCode = self.childProcess.returncode if self.childProcess else 1234 logString = "\nProcess exited with code {0} at {1} on {2}\n" nowTime = datetime.datetime.now().replace(microsecond=0) log.write(logString.format(self.HydraTask.exitCode, nowTime, self.thisNode.host)) progressUpdateThread.terminate() #Update HydraTask and HydraJob with currentFrame, MPF, and RLTracker self.progressUpdate(commit=False) #EndTime self.HydraTask.endTime = datetime.datetime.now() #Work around for batch files if self.HydraJob.jobType == "BatchFile" and self.HydraTask.exitCode == 0: self.HydraTask.currentFrame = (self.HydraTask.endFrame + 1) self.HydraJob.renderLayerTracker = str((self.HydraTask.endFrame + 1)) #Status, Attempts. Failures if self.childKilled == 1: self.HydraTask.status = self.statusAfterDeath self.HydraTask.exitCode = 1 else: if self.HydraTask.exitCode == 0 and self.HydraTask.currentFrame >= originalCurrentFrame: status = FINISHED else: if self.HydraTask.exitCode == 0: log.write("\n\nERROR: Task returned exit code 0 but it appears to have not actually rendered any frames.") status = ERROR self.HydraJob.attempts += 1 if not self.HydraJob.failures or self.HydraJob.failures == "": self.HydraJob.failures = self.thisNode.host else: self.HydraJob.failures += ",{0}".format(self.thisNode.host) self.HydraTask.status = status #Update data on the DB with transaction() as t: self.HydraTask.update(t) self.HydraJob.update(t) self.resetThisNode() log.close() logger.info("Done with render task %s", self.HydraTask.id) self.childProcess = None self.PSUtilProc = None self.HydraJob = None self.HydraTask = None self.logPath = None
def loadCredentials(username): logger.info("Fetching login for %s", username) return keyring.get_password("Hydra", username)
def submitButtonHandler(self): ####################################################################### #TODO: Rework this to work with multiple job types ####################################################################### #Getting data in same order as JobTicket execName = str(self.executableComboBox.currentText()) baseCMD = str(self.cmdLineEdit.text()) startFrame = int(self.startSpinBox.value()) endFrame = int(self.endSpinBox.value()) byFrame = int(self.testFramesSpinBox.value()) taskFile = str(self.sceneLineEdit.text()) priority = int(self.prioritySpinBox.value()) phase = 0 #Placeholder, set this later when building the commands jobStatus = self.getJobStatus() niceName = str(self.niceNameLineEdit.text()) owner = transaction().db_username compatabilityList = self.getReqs() maxNodesP1 = int(self.maxNodesP1SpinBox.value()) maxNodesP2 = int(self.maxNodesP2SpinBox.value()) timeout = int(self.timeoutSpinbox.value()) projectName = str(self.projectNameLineEdit.text()) renderLayers = str(self.renderLayersLineEdit.text()).replace(" ", "") jobType = str(self.jobTypeComboBox.currentText()) proj = str(self.projLineEdit.text()) #Error Checking if len(baseCMD) > 1000: aboutBox(self, "baseCMD too long!", "baseCMD must be less than 1000 characters!") logger.error("baseCMD too long! baseCMD must be less than 1000 characters!") return if startFrame > endFrame: aboutBox(self, "startFrame is greater than endFrame!", "startFrame must be less than the endFrame!") logger.error("startFrame is greater than endFrame!") return if startFrame > 65535 or endFrame > 65535 or startFrame < 0 or endFrame < 0: aboutBox(self, "Frame range out of range!", "Start/End frames must be between 0 and 65535!") logger.error("Frame range out of range! Start/End frames must be between 0 and 65535!") return if byFrame > 255 or byFrame < 0: aboutBox(self, "byFrame out of range!", "byFrame must be between 0 and 255!") logger.error("byFrame out of range! byFrame must be between 0 and 255!") return if len(taskFile) > 255 or len(taskFile) < 5: aboutBox(self, "taskFile out of range!", "taskFile must be greater than 5 and less than 255 characters") logger.error("taskFile out of range! taskFile path must be greater than 5 and less than 255 characters!") return if priority > 255 or priority < 0: aboutBox(self, "Priority out of range!", "Priority must be between 0 and 255!") logger.error("Priority out of range! Priority must be between 0 and 255!") return if len(niceName) > 60 or len(niceName) < 2: aboutBox(self, "NiceName out of range!", "NiceName must be more than 1 and less than 60 characters!") logger.error("NiceName out of range! NiceName must be more than 1 and less than 60 characters!") return if len(owner) > 45: aboutBox(self, "Owner out of range!", "Owner must be less than 45 characters!") logger.error("Owner out of range! Owner must be less than 45 characters!") return if len(projectName) > 60: aboutBox(self, "Project Name out of range!", "Project name must be less than 60 characters!") return if projectName == "": projectName = "UnkownProject" if jobType not in ["BatchFile", "FusionComp"]: baseCMD += " -proj {0}".format(proj) phase01Status = False if self.testCheckBox.isChecked(): logger.info("Building Phase 01") #Phase specific overrides phase = 1 #This is cool because at least for now anything with a phase one SHOULD be a Maya Job baseCMDOverride = baseCMD + " -x 640 -y 360" if ((endFrame - startFrame) % byFrame) != 0: phase01FinalFrame = True #This looks kinda dumb but works because Python 2.7 divide returns an int endFrameOverride = ((((endFrame - startFrame) / byFrame) * byFrame) + startFrame) else: phase01FinalFrame = False endFrameOverride = endFrame phase01 = submitJob(niceName, projectName, owner, jobStatus, compatabilityList, execName, baseCMDOverride, startFrame, endFrameOverride, byFrame, renderLayers, taskFile, int(priority * 1.25), phase, maxNodesP1, timeout, 10, jobType) logger.info("Phase 01 submitted with id: %s", phase01.id) if phase01FinalFrame: niceNameOverride = "{0}_FinalFrame".format(niceName) byFrameOverride = 1 phase01FinalFrameJob = submitJob(niceNameOverride, projectName, owner, jobStatus, compatabilityList, execName, baseCMDOverride, endFrame, endFrame, byFrameOverride, renderLayers, taskFile, int(priority * 1.25), phase, maxNodesP1, timeout, 10, jobType) logger.info("Phase 01 final frame workaround submitted with id: %s", phase01FinalFrameJob.id) phase01Status = True if self.finalCheckBox.isChecked(): logger.info("Building Phase 02") #Phase specific overrides phase = 2 byFrame = 1 if phase01Status: jobStatusOverride = "U" else: jobStatusOverride = jobStatus phase02 = submitJob(niceName, projectName, owner, jobStatusOverride, compatabilityList, execName, baseCMD, startFrame, endFrame, byFrame, renderLayers, taskFile, priority, phase, maxNodesP2, timeout, 10, jobType) logger.info("Phase 02 submitted with id: %s", phase02.id) self.submitButton.setEnabled(False) self.submitButton.setText("Job Submitted! Please close window.")