From 5a95a0974eaf576a1ff1f01529d06f4e1a348770 Mon Sep 17 00:00:00 2001 From: moshix Date: Thu, 12 Sep 2024 07:17:58 +0200 Subject: [PATCH] Create SUBMIT.py in python by John Murray --- SUBMIT.py | 404 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 SUBMIT.py diff --git a/SUBMIT.py b/SUBMIT.py new file mode 100644 index 0000000..07c87f8 --- /dev/null +++ b/SUBMIT.py @@ -0,0 +1,404 @@ +#!/usr/local/bin/python3 + +# Copyright © 2018 by John Murray +# No rights reserved. + +from ftplib import FTP + +import argparse +import multiprocessing +import os +import re +import subprocess +import sys +import time + +envEditor = '' + +def opts(): + parser = argparse.ArgumentParser(description='Submit a job to Z/OS, wait for it to end and get the output back.', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog='Notes.\nUserid and password will be obtained from environment variables ZOSUSER and ZOSPSWD if defined.\n' + 'Hostname will be obtained from environment variables ZOSHOST if defined.\n' + 'A directory called "listing" will be created if one does not exist. The script uses this directory for temporary files and listings.\n' + 'If the user decides to edit the listing, the name of the editor will be obtained from environment variable ZOSEDITOR. If the variable is not defined, the editor will default to "x"') + parser.add_argument('-d', + action='store_true', + dest='debug', + default=False, + help='Run the command in debug mode') + parser.add_argument('-e', + action='store_true', + dest='edit', + default=False, + help='Edit the listing file') + parser.add_argument('-w', + default=5, + action='store', + dest='wait', + metavar='seconds', + help='Seconds to sleep before checking for job completion', + type=int) + parser.add_argument('-u', + action='store', + dest='user', + metavar='userid', + help='Userid to sign-in to zOS with') + parser.add_argument('-p', + action='store', + dest='pswd', + metavar='password', + help='Password') + parser.add_argument('-s', + action='store', + dest='host', + metavar='hostname', + help='Hostname of host to submit job to') + parser.add_argument('JCLFile', + action='store', + help='JCL file to submit') + + args = parser.parse_args() + + return args + +def settleEnvs(args): + global envEditor + + rc = True + + if args.user == None: + user = os.getenv('ZOSUSER') + if user != None and len(user) != 0: + args.user = user + else: + print('User not sepcified and ZOSUSER not defined') + rc = False + + if args.pswd == None: + pswd = os.getenv('ZOSPSWD') + if pswd != None and len(pswd) != 0: + args.pswd = pswd + else: + print('Password not sepcified and ZOSPSWD not defined') + rc = False + + if args.host == None: + host = os.getenv('ZOSHOST') + if host != None and len(host) != 0: + args.host = host + else: + print('Host not sepcified and ZOSHOST not defined') + rc = False + + if args.edit: + envEditor = os.getenv('ZOSEDITOR') + if envEditor == None or len(envEditor) == 0: + envEditor = 'x' + + return rc + +def debugOpts(args): + if args.debug: + print('Options:') + print('\tedit ' + str(args.edit)) + print('\twait ' + str(args.wait)) + print('\tuser ' + str(args.user)) + print('\tpswd ' + str(args.pswd)) + print('\thost ' + str(args.host)) + print('\tJCLFile ' + args.JCLFile) + print() + +class Error(Exception): + pass + +class SubmitError(Error): + def __init__(self, + expression, + message): + self.expression = expression + self.message = message + +class Submit: + def __init__(self, + debug=False, + edit=False, + wait=5, + user=None, + pswd=None, + host=None, + editor=None, + JCLFile=None): + self.debug = debug + self.edit = edit + self.wait = wait + self.user = user + self.pswd = pswd + self.host = host + self.editor = editor + self.JCLFile = JCLFile + self.jesLevelRe = re.compile(r'JESINTERFACELEVEL is ([0-9])') + self.jobNameRe = re.compile(r'\/\/([a-zA-Z0-9$#@]+)\s+JOB') + self.jobIDRe = re.compile(r'It is known to JES as ([a-zA-Z0-9$#@]+)') + self.userRe = re.compile(r'\?user\?') + self.pswdRe = re.compile(r'\?pswd\?') + self.includeRe = re.compile(r'\?([^\?]+)\?') + self.endRe = re.compile(r'^\/\/ *$') + self.jesStatus1Re = re.compile(r'^\s*(\S+)\s+(\S+)\s+(\S+)\s*') + self.jesStatus2Re = re.compile(r'^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+') + + def processJob(self): + if not os.path.exists('listings'): + os.mkdir('listings') + + self.openFTP() + + self.jesLevel() + + tmpFileName = self.buildJob() + + jobID = self.sendFTP(tmpFileName) + + listing = self.getJobListing(jobID) + + listingFileName = self.saveListing(jobID, + listing) + + if self.edit: + self.editListing(listingFileName) + + self.closeFTP(jobID) + + self.cleanup(tmpFileName) + + def openFTP(self): + self.ftp = FTP(host=self.host, + user=self.user, + passwd=self.pswd) + + self.ftp.sendcmd('site filetype=jes') + + def jesLevel(self): + stat = self.ftp.sendcmd('stat') + + jesLevelMatch = self.jesLevelRe.search(stat) + + if jesLevelMatch == None: + raise SubmitError('jesLevelInterface', + 'JESINTERFACELEVEL not found via stat command') + + self.jesLevelInterface = jesLevelMatch.group(1) + + if self.debug: + print('jesLevel: ' + self.jesLevelInterface) + + def buildJob(self): + try: + jobNameFound = False + + jclCards = self.getFile(self.JCLFile) + + jclCards = self.fixJobName(jclCards) + + jclCards = self.repUserPswd(jclCards) + + jclCards = self.includes(jclCards) + + tmpFileName = self.createJCLFile(jclCards) + + if self.debug: + print('buildJob: ' + tmpFileName) + + except OSError as osError: + raise SubmitError('buildJob', + 'Problem opening JCLFile ' + self.JCLFile + '(' + str(osError) + ')') + + return tmpFileName + + def getFile(self, + fileName): + file = open(fileName, 'r') + + lines = file.readlines() + + file.close() + + lines = ''.join(lines) + + return lines + + def fixJobName(self, + jclCards): + jobNameMatch = self.jobNameRe.search(jclCards) + + if jobNameMatch != None: + if self.jesLevelInterface == '1': + self.jobName = self.user + '0' + else: + self.jobName = jobNameMatch.group(1) + + jclCards = jclCards.replace(jobNameMatch.group(1), + self.jobName, + 1) + if self.debug: + print('fixJobName:' + self.jobName) + else: + raise SubmitError('buildJob', + 'Job card not found in ' + self.JCLFile) + + return jclCards + + def repUserPswd(self, + jclCards): + jclCards = jclCards.replace('?user?', + self.user, + 1) + + jclCards = jclCards.replace('?pswd?', + self.pswd, + 1) + return jclCards + + def includes(self, + jclCards): + while True: + includeMatch = self.includeRe.search(jclCards) + + if includeMatch != None: + includeCards = self.getFile(includeMatch.group(1)) + + jclCards = jclCards.replace(includeMatch.group(0) + "\n", + includeCards, + 1) + else: + break + + return jclCards + + def createJCLFile(self, + jclCards): + tmpFileName = 'listings/' + os.path.basename(self.JCLFile) + '.' + str(multiprocessing.current_process().pid) + + tmpFile = open(tmpFileName, 'w') + + for jclCard in jclCards.split(sep='\n'): + tmpFile.write(jclCard + '\n') + + if re.search(r'^\/\/ *$', jclCard) != None: + break + + tmpFile.close() + + return tmpFileName + + def sendFTP(self, + tmpFileName): + zosMessage = self.ftp.storlines('stor ' + os.path.basename(tmpFileName), open(tmpFileName, 'rb')) + + return self.getJobID(zosMessage) + + def getJobID(self, + zosMessage): + jobIDMatch = self.jobIDRe.search(zosMessage) + + if jobIDMatch == None: + raise SubmitError('getJobID', + 'Job ID not found in: ' + zosMessage) + + jobID = jobIDMatch.group(1) + + print('Job ' + self.JCLFile + ' submitted for execution on ' + self.host + ' as ' + jobID) + + return jobID + + def getJobListing(self, + jobID): + notFound = True + heldOutList = [] + listingList = [] + + while notFound: + time.sleep(int(self.wait)) + + # These are closures: it has access to heldOutList which is in the current scope + # + def ftpDir(line): + heldOutList.append(line) + + def ftpRetr(line): + listingList.append(line) + + self.ftp.dir(ftpDir) + + for heldOut in heldOutList: + if self.jesLevelInterface == '1': + jesStatus1Match = self.jesStatus1Re.match(heldOut) + + job, jobid, state = jesStatus1Match.group(1, 2, 3) + else: + jesStatus2Match = self.jesStatus2Re.match(heldOut) + + job, jobid, owner, state = jesStatus2Match.group(1, 2, 3, 4) + + if jobid == jobID: + if state == 'OUTPUT': + self.ftp.retrlines('RETR ' + jobid, ftpRetr) + + listing = '\n'.join(listingList) + + notFound = False + + break + return listing + + def saveListing(self, + jobID, + listing): + listingFileName = 'listings/' + os.path.basename(self.JCLFile) + '.' + jobID + + listingFile = open(listingFileName, 'w') + + listingFile.write(listing.encode(encoding="ascii", errors="replace").decode()) + + listingFile.close() + + print('Job output for ' + self.JCLFile + ' is in ' + listingFileName) + + return listingFileName + + def editListing(self, + listingFileName): + completed = subprocess.run([self.editor, listingFileName]) + + def closeFTP(self, + jobID): + self.ftp.delete(jobID) + + self.ftp.close() + + def cleanup(self, + tmpFileName): + completed = subprocess.run(['rm', tmpFileName]) + +def main(): + try: + args = opts() + + if settleEnvs(args): + debugOpts(args) + + submit = Submit(debug=args.debug, + edit=args.edit, + wait=args.wait, + user=args.user, + pswd=args.pswd, + host=args.host, + editor=envEditor, + JCLFile=args.JCLFile) + + submit.processJob() + except SubmitError as submitError: + print('SubmitError: ' + submitError.expression + ' - ' + submitError.message) + +if __name__ == '__main__': + main()