1+ # Copyright (c) IBM Corporation 2019, 2020
2+ # Apache License, Version 2.0 (see https://opensource.org/licenses/Apache-2.0)
3+
4+ from tempfile import NamedTemporaryFile
5+ from os import chmod , path , remove
6+ import json
7+ import re
8+
9+ def job_output (module , job_id = '' , owner = '' , job_name = '' , dd_name = '' ):
10+ """Get the output from a z/OS job based on various search criteria.
11+
12+ Arguments:
13+ module {AnsibleModule} -- The AnsibleModule object from the running module.
14+
15+ Keyword Arguments:
16+ job_id {str} -- The job ID to search for (default: {''})
17+ owner {str} -- The owner of the job (default: {''})
18+ job_name {str} -- The job name search for (default: {''})
19+ dd_name {str} -- The data definition to retrieve (default: {''})
20+
21+ Raises:
22+ RuntimeError: When job output cannot be retrieved successfully but job exists.
23+ RuntimeError: When no job output is found
24+
25+ Returns:
26+ dict[str, list[dict]] -- The output information for a given job.
27+ """
28+ job_detail_json = {}
29+ rc , out , err = _get_job_json_str (module , job_id , owner , job_name , dd_name )
30+ if rc != 0 :
31+ raise RuntimeError (
32+ 'Failed to retrieve job output. RC: {} Error: {}' .format (str (rc ), str (err )))
33+ if not out :
34+ raise RuntimeError (
35+ 'Failed to retrieve job output. No job output found.' )
36+ job_detail_json = json .loads (out , strict = False )
37+ for job in job_detail_json .get ('jobs' ):
38+ job ['ret_code' ] = {} if job .get ('ret_code' ) == None else job .get ('ret_code' )
39+ job ['ret_code' ]['code' ] = _get_return_code_num (job .get ('ret_code' , {}).get ('msg' , '' ))
40+ job ['ret_code' ]['msg_code' ] = _get_return_code_str (job .get ('ret_code' , {}).get ('msg' , '' ))
41+ job ['ret_code' ]['msg_txt' ] = ''
42+ return job_detail_json
43+
44+
45+ def _get_job_json_str (module , job_id = '' , owner = '' , job_name = '' , dd_name = '' ):
46+ """Generate JSON output string containing Job info from SDSF.
47+ Writes a temporary REXX script to the USS filesystem to gather output.
48+
49+ Arguments:
50+ module {AnsibleModule} -- The AnsibleModule object from the running module.
51+
52+ Keyword Arguments:
53+ job_id {str} -- The job ID to search for (default: {''})
54+ owner {str} -- The owner of the job (default: {''})
55+ job_name {str} -- The job name search for (default: {''})
56+ dd_name {str} -- The data definition to retrieve (default: {''})
57+
58+ Returns:
59+ tuple[int, str, str] -- RC, STDOUT, and STDERR from the REXX script.
60+ """
61+ get_job_detail_json_rexx = """/* REXX */
62+ arg options
63+ parse var options param
64+ upper param
65+ parse var param 'JOBID=' jobid ' OWNER=' owner,
66+ ' JOBNAME=' jobname ' DDNAME=' ddname
67+
68+ rc=isfcalls('ON')
69+
70+ jobid = strip(jobid,'L')
71+ if (jobid <> '') then do
72+ ISFFILTER='JobID EQ '||jobid
73+ end
74+ owner = strip(owner,'L')
75+ if (owner <> '') then do
76+ ISFOWNER=owner
77+ end
78+ jobname = strip(jobname,'L')
79+ if (jobname <> '') then do
80+ ISFPREFIX=jobname
81+ end
82+ ddname = strip(ddname,'L')
83+ if (ddname == '?') then do
84+ ddname = ''
85+ end
86+
87+ Address SDSF "ISFEXEC ST (ALTERNATE DELAYED)"
88+ if rc<>0 then do
89+ Say '{"jobs":[]}'
90+ Exit 0
91+ end
92+ if isfrows == 0 then do
93+ Say '{"jobs":[]}'
94+ end
95+ else do
96+ Say '{"jobs":['
97+ do ix=1 to isfrows
98+ linecount = 0
99+ if ix<>1 then do
100+ Say ','
101+ end
102+ Say '{'
103+ Say '"'||'job_id'||'":"'||value('JOBID'||"."||ix)||'",'
104+ Say '"'||'job_name'||'":"'||value('JNAME'||"."||ix)||'",'
105+ Say '"'||'subsystem'||'":"'||value('ESYSID'||"."||ix)||'",'
106+ Say '"'||'owner'||'":"'||value('OWNERID'||"."||ix)||'",'
107+ Say '"'||'ret_code'||'":{"'||'msg'||'":"'||value('RETCODE'||"."||ix)||'"},'
108+ Say '"'||'class'||'":"'||value('JCLASS'||"."||ix)||'",'
109+ Say '"'||'content_type'||'":"'||value('JTYPE'||"."||ix)||'",'
110+ Address SDSF "ISFACT ST TOKEN('"TOKEN.ix"') PARM(NP ?)",
111+ "("prefix JDS_
112+ lrc=rc
113+ if lrc<>0 | JDS_DDNAME.0 == 0 then do
114+ Say '"ddnames":[]'
115+ end
116+ else do
117+ Say '"ddnames":['
118+ do jx=1 to JDS_DDNAME.0
119+ if jx<>1 & ddname == '' then do
120+ Say ','
121+ end
122+ if ddname == '' | ddname == value('JDS_DDNAME'||"."||jx) then do
123+ Say '{'
124+ Say '"'||'ddname'||'":"'||value('JDS_DDNAME'||"."||jx)||'",'
125+ Say '"'||'record_count'||'":"'||value('JDS_RECCNT'||"."||jx)||'",'
126+ Say '"'||'id'||'":"'||value('JDS_DSID'||"."||jx)||'",'
127+ Say '"'||'stepname'||'":"'||value('JDS_STEPN'||"."||jx)||'",'
128+ Say '"'||'procstep'||'":"'||value('JDS_PROCS'||"."||jx)||'",'
129+ Say '"'||'byte_count'||'":"'||value('JDS_BYTECNT'||"."||jx)||'",'
130+ Say '"'||'content'||'":['
131+ Address SDSF "ISFBROWSE ST TOKEN('"token.ix"')"
132+ untilline = linecount + JDS_RECCNT.jx
133+ startingcount = linecount + 1
134+ do kx=linecount+1 to untilline
135+ if kx<>startingcount then do
136+ Say ','
137+ end
138+ linecount = linecount + 1
139+ Say '"'||escapeNewLine(escapeDoubleQuote(isfline.kx))||'"'
140+ end
141+ Say ']'
142+ Say '}'
143+ end
144+ end
145+ Say ']'
146+ end
147+ Say '}'
148+ end
149+ Say ']}'
150+ end
151+
152+ rc=isfcalls('OFF')
153+
154+ return 0
155+
156+ escapeDoubleQuote: Procedure
157+ Parse Arg string
158+ out=''
159+ Do While Pos('"',string)<>0
160+ Parse Var string prefix '"' string
161+ out=out||prefix||'\\ "'
162+ End
163+ Return out||string
164+
165+ escapeNewLine: Procedure
166+ Parse Arg string
167+ Return translate(string, '4040'x, '1525'x)
168+ """
169+ try :
170+ dirname , scriptname = _write_script (get_job_detail_json_rexx )
171+ if dd_name is None or dd_name == '?' :
172+ dd_name = ''
173+ jobid_param = 'jobid=' + job_id
174+ owner_param = 'owner=' + owner
175+ jobname_param = 'jobname=' + job_name
176+ ddname_param = 'ddname=' + dd_name
177+ cmd = [dirname + '/' + scriptname , jobid_param , owner_param ,
178+ jobname_param , ddname_param ]
179+
180+ rc , out , err = module .run_command (args = " " .join (cmd ),
181+ cwd = dirname ,
182+ use_unsafe_shell = True )
183+ except Exception :
184+ raise
185+ finally :
186+ remove (dirname + "/" + scriptname )
187+ return rc , out , err
188+
189+
190+ def _write_script (content ):
191+ """Write a script to the filesystem.
192+ This includes writing and setting the execute bit.
193+
194+ Arguments:
195+ content {str} -- The contents of the script
196+
197+ Returns:
198+ tuple[str, str] -- The directory and script names
199+ """
200+ delete_on_close = False
201+ try :
202+ tmp_file = NamedTemporaryFile (delete = delete_on_close )
203+ with open (tmp_file .name , 'w' ) as f :
204+ f .write (content )
205+ chmod (tmp_file .name , 0o755 )
206+ dirname = path .dirname (tmp_file .name )
207+ scriptname = path .basename (tmp_file .name )
208+ except Exception :
209+ remove (tmp_file )
210+ raise
211+ return dirname , scriptname
212+
213+ def _get_return_code_num (rc_str ):
214+ """Parse an integer return code from
215+ z/OS job output return code string.
216+
217+ Arguments:
218+ rc_str {str} -- The return code message from z/OS job log (eg. "CC 0000")
219+
220+ Returns:
221+ Union[int, NoneType] -- Returns integer RC if possible, if not returns NoneType
222+ """
223+ rc = None
224+ match = re .search (r'\s*CC\s*([0-9]+)' , rc_str )
225+ if match :
226+ rc = int (match .group (1 ))
227+ return rc
228+
229+ def _get_return_code_str (rc_str ):
230+ """Parse an intestrger return code from
231+ z/OS job output return code string.
232+
233+ Arguments:
234+ rc_str {str} -- The return code message from z/OS job log (eg. "CC 0000" or "ABEND")
235+
236+ Returns:
237+ Union[str, NoneType] -- Returns string RC or ABEND code if possible, if not returns NoneType
238+ """
239+ rc = None
240+ match = re .search (r'(?:\s*CC\s*([0-9]+))|(?:ABEND\s*((?:S|U)[0-9]+))' , rc_str )
241+ if match :
242+ rc = match .group (1 ) or match .group (2 )
243+ return rc
0 commit comments