/* REXX */
/*--------------------------------------------------------------------*/
/* ZFSDIAG - z/OS zFS space / allocation diagnostic                  */
/*                                                                    */
/* Author  : SARATH S                                                 */
/* Version : 1.5                                                      */
/* Purpose : Read-only first-response analysis of zFS full, low-space */
/*           and grow/allocation conditions.                          */
/*                                                                    */
/* Examples:                                                          */
/*   TSO ZFSDIAG                                                      */
/*   TSO ZFSDIAG OMVS.APP.ZFS                                        */
/*   TSO ZFSDIAG /var/app                                            */
/*   TSO ZFSDIAG DSN OMVS.APP.ZFS                                    */
/*   TSO ZFSDIAG PATH /var/app                                       */
/*   TSO ZFSDIAG OMVS.APP.ZFS FILES                                  */
/*   TSO ZFSDIAG /var/app FILES                                      */
/*   TSO ZFSDIAG HELP                                                 */
/*   TSO ZFSDIAG SAMPLE                                               */
/*                                                                    */
/* Output:                                                            */
/*   <userid>.ZFSDIAG.RPT  - persistent latest diagnostic report.     */
/*   <userid>.ZFSDIAG.HELP - persistent README / HELP text.           */
/*   <userid>.ZFSDIAG.SAMPLE - persistent sample report.              */
/*                                                                    */
/* v1.5 fixes:                                                        */
/*   - Resolves USS path case safely when an ISPF field folds to upper. */
/*   - Reports received path, resolved path and case-resolution status. */
/*   - Refuses ambiguous case-insensitive matches instead of guessing.  */
/*   - Initializes command variables so skipped stages display NOT RUN. */
/*                                                                    */
/* v1.4 fixes:                                                        */
/*   - Passes BPXWUNIX stdout/stderr/env as literal STEM NAMES.         */
/*   - Captures the actual USS command error text on failures.          */
/*   - Displays NOT RUN instead of -1 for skipped command stages.       */
/*                                                                    */
/* v1.3 fixes:                                                        */
/*   - Supplies PATH/LIBPATH to BPXWUNIX from TSO/E.                    */
/*   - Captures D OMVS,F,NAME= with CONSOLE + GETMSG.                   */
/*   - Surfaces collection failures in TRIAGE SUMMARY.                  */
/*                                                                    */
/* Safety:                                                            */
/*   ZFSDIAG does not issue ZFSADM GROW, DELETE, ALTER, MOUNT,         */
/*   UNMOUNT, REMOUNT, SETOMVS or any corrective/change command.      */
/*   FILES mode performs a recursive read-only file scan and is       */
/*   intentionally optional because it can create noticeable I/O.     */
/*--------------------------------------------------------------------*/

numeric digits 20
signal on syntax name FatalSyntax

/*--------------------------- Defaults ------------------------------*/
/* BPXWUNIX from normal TSO/E does not inherit the USS shell PATH. */
zEnv. = ''
zEnv.0 = 2
zEnv.1 = 'PATH=/bin:/usr/bin:/usr/sbin:/usr/lpp/dfs/global/bin'
zEnv.2 = 'LIBPATH=/lib:/usr/lib'

mode = 'ALL'
targetType = ''
targetDsn = ''
targetPath = ''
filesMode = 0

rpt. = ''
rpt.0 = 0
rptCount = 0
ispfActive = 0

localSys = translate(MVSVAR('SYSNAME'))
uid = sysvar('SYSUID')
reportDsn = uid'.ZFSDIAG.RPT'
helpDsn = uid'.ZFSDIAG.HELP'
sampleDsn = uid'.ZFSDIAG.SAMPLE'

if sysvar('SYSISPF') = 'ACTIVE' then ispfActive = 1

call InitData

parse arg rawArgs
call ParseArguments rawArgs
if result <> 0 then do
  call PresentReport
  exit 12
end

if mode = 'HELP' then do
  call HelpReport
  call PresentReport
  exit 0
end

if mode = 'SAMPLE' then do
  call SampleReport
  call PresentReport
  exit 0
end

if mode = 'ALL' then do
  call RunExceptionScan
  call Header
  call ExceptionReport
  call Notes
  call Footer
  call PresentReport
  exit 0
end

call RunTargeted
call DetermineAssessment

call Header
call ReportInformation
call TriageSummary
call TargetDetails
call CapacityDetails
call VSAMDetails
call FileScanReport
call Analysis
call Recommendations
call CommandReference
call EvidenceReport
call Notes
call Footer
call PresentReport
exit 0


/*====================================================================*/
/* INITIALIZATION                                                     */
/*====================================================================*/

InitData:
/* Command text / return codes. */
exCmd = ''
dfCmd = ''
fsCmd = ''
agCmd = ''
fileCmd = ''
caseCmd = ''

omvsRc = -1
dfRc = -1
dfAll = 0
fsRc = -1
agRc = -1
lcRc = -1
fileRc = -1
exRc = -1

/* Captured output stems. */
omvsOut. = ''
omvsOut.0 = 0
dfOut. = ''
dfOut.0 = 0
dfErr. = ''
dfErr.0 = 0
fsOut. = ''
fsOut.0 = 0
fsErr. = ''
fsErr.0 = 0
agOut. = ''
agOut.0 = 0
agErr. = ''
agErr.0 = 0
lcOut. = ''
lcOut.0 = 0
fileOut. = ''
fileOut.0 = 0
fileErr. = ''
fileErr.0 = 0
exOut. = ''
exOut.0 = 0
exErr. = ''
exErr.0 = 0

/* Target / mount data. */
inputPath = ''
pathResolveStatus = 'NOT APPLICABLE'
pathResolveDetail = ''
pathResolveRc = -1
mountPath = ''
fsOwner = ''
fsMode = ''
fsClient = ''
fsAutomove = ''

/* zfsadm fsinfo data. */
fsName = ''
fsSizeText = ''
fsSizeK = -1
fsFree8K = -1
fsVersion = ''
fsStatus = ''
fsGrow = ''
fsSpaceMon = ''
fsLastGrow = ''
fsEnospc = -1
fsIoErrors = -1

/* zfsadm aggrinfo data. */
aggrFreeK = -1
aggrTotalK = -1
aggrState = ''
usedPct = -1

/* LISTCAT / VSAM data. */
spaceType = ''
spacePri = ''
spaceSec = ''
hiARba = -1
hiURba = -1
extAddr = 'UNKNOWN'
extFormat = 'UNKNOWN'
storClas = ''
dataClas = ''
mgmtClas = ''
listExtents = -1
maxExtentNumber = -1
extentRecords = 0
volCount = 0
vol. = ''
volSeen. = 0

/* Derived data. */
allocGapBytes = 0
allocGapK = 0
severity = 'UNKNOWN'
diagnosis = 'INSUFFICIENT EVIDENCE'
confidence = 'LOW'
flagGF = 0
flagGD = 0
flagIE = 0
growOff = 0
quiesced = 0
nonEANearLimit = 0
highExtent = 0
veryHighExtent = 0
zeroSecondary = 0
return


/*====================================================================*/
/* ARGUMENTS                                                          */
/*====================================================================*/

ParseArguments:
parse arg paRaw
paRaw = strip(paRaw)

if paRaw = '' then do
  mode = 'ALL'
  return 0
end

paFirst = word(paRaw,1)
paUpper = translate(paFirst)

if paUpper = 'HELP' | paUpper = 'H' | paUpper = '?' then do
  mode = 'HELP'
  return 0
end

if paUpper = 'SAMPLE' then do
  mode = 'SAMPLE'
  if words(paRaw) > 1 then do
    call Emit 'ZFSDIAG: SAMPLE does not accept additional parameters.'
    return 12
  end
  return 0
end

if paUpper = 'ALL' then do
  mode = 'ALL'
  if words(paRaw) > 1 then do
    call Emit 'ZFSDIAG: ALL does not accept additional parameters.'
    return 12
  end
  return 0
end

mode = 'TARGET'

if paUpper = 'DSN' | paUpper = 'ZFS' then do
  targetType = 'DSN'
  targetDsn = translate(word(paRaw,2))
  paNext = 3
  if targetDsn = '' then do
    call Emit 'ZFSDIAG: DSN/ZFS requires a zFS data set name.'
    return 12
  end
end
else if paUpper = 'PATH' then do
  targetType = 'PATH'
  targetPath = word(paRaw,2)
  inputPath = targetPath
  paNext = 3
  if targetPath = '' then do
    call Emit 'ZFSDIAG: PATH requires an absolute USS path.'
    return 12
  end
end
else if left(paFirst,1) = '/' then do
  targetType = 'PATH'
  targetPath = paFirst
  inputPath = targetPath
  paNext = 2
end
else do
  targetType = 'DSN'
  targetDsn = translate(paFirst)
  paNext = 2
end

do paI = paNext to words(paRaw)
  paWord = translate(word(paRaw,paI))
  if paWord = 'FILES' then filesMode = 1
  else do
    call Emit 'ZFSDIAG: Unknown parameter: 'word(paRaw,paI)
    return 12
  end
end

if targetType = 'DSN' then do
  if ValidateDsn(targetDsn) = 0 then do
    call Emit 'ZFSDIAG: Invalid or unsupported data set name: 'targetDsn
    return 12
  end
end
else do
  if ValidatePath(targetPath) = 0 then do
    call Emit 'ZFSDIAG: PATH must be an absolute path using common'
    call Emit 'non-shell-metacharacter characters.'
    return 12
  end
end

return 0


ValidateDsn:
procedure
parse upper arg vd
if vd = '' | length(vd) > 44 then return 0
if left(vd,1) = '.' | right(vd,1) = '.' then return 0
if pos('..',vd) > 0 then return 0
allowed = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$.-'
if verify(vd,allowed) <> 0 then return 0
return 1


ValidatePath:
procedure
parse arg vp
if vp = '' | left(vp,1) <> '/' then return 0
if length(vp) > 1023 then return 0
if vp = '/.' | vp = '/..' then return 0
if pos('/./',vp) > 0 | pos('/../',vp) > 0 then return 0
if right(vp,2) = '/.' | right(vp,3) = '/..' then return 0
/* Deliberately reject shell metacharacters because FILES mode builds */
/* a shell pipeline. Common USS path characters remain supported. */
allowed = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
allowed = allowed'0123456789/._-+@#$%:,=~'
if verify(vp,allowed) <> 0 then return 0
return 1


/*====================================================================*/
/* SYSTEM-WIDE EXCEPTION MODE                                         */
/*====================================================================*/

RunExceptionScan:
exCmd = 'zfsadm fsinfo -exceptions'
exRc = BPXWUNIX(exCmd,,'exOut.','exErr.','zEnv.')
return


ExceptionReport:
call Section 'REPORT INFORMATION'
call KV 'Utility', 'ZFSDIAG'
call KV 'Report type', 'LIVE SYSTEM-WIDE EXCEPTION SCAN'
call KV 'System', localSys
call KV 'Execution user', uid
call KV 'Generated', DisplayDate()' 'time('N')
call KV 'Output data set', reportDsn
call KV 'Changes performed', 'NONE'

call Section 'TRIAGE SUMMARY'
call KV 'Scope', 'ALL MOUNTED zFS EXCEPTIONS'
call KV 'Command', 'zfsadm fsinfo -exceptions'
call KV 'Command RC', RcText(exRc)

if exRc <> 0 then do
  call KV 'Assessment', 'EXCEPTION SCAN COULD NOT BE COMPLETED'
  call KV 'Severity', 'UNKNOWN'
end
else if exOut.0 = 0 then do
  call KV 'Assessment', 'NO EXCEPTION OUTPUT RETURNED'
  call KV 'Severity', 'LOW'
end
else do
  call KV 'Assessment', 'zFS EXCEPTION OUTPUT RETURNED - REVIEW BELOW'
  call KV 'Severity', 'REVIEW'
end

call Section 'TARGET / SCOPE'
call KV 'Target', 'SYSTEM-WIDE'
call KV 'System', localSys
call KV 'Specific aggregate', 'NONE - EXCEPTION DISCOVERY MODE'

call Section 'OBSERVATIONS'
if exRc <> 0 then do
  call Emit 'The zFS exception scan could not be completed.'
  call Emit 'This normally indicates that zfsadm was unavailable to the'
  call Emit 'caller or the USS command could not be executed.'
end
else if exOut.0 = 0 then do
  call Emit 'No output was returned by zfsadm fsinfo -exceptions.'
  call Emit 'No zFS exception condition can be confirmed from this run.'
end
else do
  call Emit 'zfsadm returned exception-state information. Use the raw'
  call Emit 'evidence below to select a specific affected aggregate.'
end

call Section 'ANALYSIS / INTERPRETATION'
if exRc <> 0 then
  call Emit 'No root-cause interpretation is attempted without command data.'
else if exOut.0 = 0 then
  call Emit 'No current exception is visible through this discovery command.'
else do
  call Emit 'This mode is intentionally discovery-oriented. It identifies'
  call Emit 'candidate zFS aggregates but does not perform the deeper VSAM,'
  call Emit 'extent, allocation and capacity correlation.'
end

call Section 'RECOMMENDED DEBUGGING / POSSIBLE RESOLUTION'
if exRc = 0 & exOut.0 > 0 then do
  call Emit '1. Identify the affected aggregate in RAW COMMAND EVIDENCE.'
  call Emit '2. Rerun: TSO ZFSDIAG <aggregate-name>'
  call Emit '3. If content growth is suspected, consider FILES only after'
  call Emit '   the lightweight targeted diagnosis is complete.'
end
else do
  call Emit '1. Verify the caller has a usable OMVS environment and access.'
  call Emit '2. If the incident already gives a zFS name/path, bypass this'
  call Emit '   discovery mode and run targeted ZFSDIAG directly.'
end

call Section 'COMMAND REFERENCE'
call Emit 'Diagnostic command used by this mode:'
call Emit '  zfsadm fsinfo -exceptions'
call Emit ''
call Emit 'Targeted follow-up examples:'
call Emit '  TSO ZFSDIAG OMVS.APP.ZFS'
call Emit '  TSO ZFSDIAG /var/app'

call Section 'RAW COMMAND EVIDENCE'
call KV 'Command', 'zfsadm fsinfo -exceptions'
call KV 'RC', exRc
if exOut.0 > 0 then call EmitStem 'exOut.', 120
else do
  call Emit '   <no stdout captured>'
  if exErr.0 > 0 then call EmitStem 'exErr.', 20
end
return

/*====================================================================*/
/* TARGETED COLLECTION                                                */
/*====================================================================*/

RunTargeted:
if targetType = 'PATH' then do
  call ResolvePathCase targetPath

  call CaptureDF targetPath
  call ParseDF

  call CaptureFsInfoPath targetPath
  call ParseFsInfo

  if targetDsn = '' & fsName <> '' then targetDsn = translate(fsName)

  if targetDsn <> '' then do
    call CaptureOmvs targetDsn
    call ParseOmvs
  end
end
else do
  call CaptureOmvs targetDsn
  call ParseOmvs

  if mountPath <> '' then call CaptureDF mountPath
  else call CaptureDF ''

  call ParseDF

  if mountPath <> '' & dfAll = 1 then do
    call CaptureDF mountPath
    call ParseDF
  end

  call CaptureFsInfoDsn targetDsn
  call ParseFsInfo
end

/* A path-mode fsinfo result may resolve the target only after the first */
/* OMVS/df pass. Keep all three target fields synchronized. */
if targetDsn = '' & fsName <> '' then targetDsn = translate(fsName)
if fsName = '' & targetDsn <> '' then fsName = targetDsn

/* If a dataset target had no mount point from D OMVS,F,NAME=, an all-df */
/* fallback can still resolve a currently mounted aggregate. */
if targetType = 'DSN' & mountPath = '' & dfRc <> 0 then do
  call CaptureDF ''
  call ParseDF
end

if targetDsn <> '' then do
  call CaptureAggrInfo targetDsn
  call ParseAggrInfo

  call CaptureListcat targetDsn
  call ParseListcat
end

/* If path-mode df failed before the dataset was known, retry a direct */
/* df once the mount point has been learned from D OMVS,F. */
if mountPath <> '' & targetType = 'PATH' & dfRc <> 0 then do
  call CaptureDF mountPath
  call ParseDF
end

if filesMode = 1 then do
  if mountPath <> '' then call CaptureFiles mountPath
  else fileRc = 16
end
return



/*----------------------- USS path case resolver ---------------------*/
/* Some ISPF command fields fold TSO command operands to uppercase.   */
/* z/OS UNIX paths are case-sensitive. Resolve one component at a     */
/* time using the actual directory entries. No recursive tree scan is */
/* performed. If more than one case-insensitive match exists, do not  */
/* guess.                                                             */

ResolvePathCase:
parse arg rpInput

if inputPath = '' then inputPath = rpInput
targetPath = rpInput
pathResolveStatus = 'AS RECEIVED'
pathResolveDetail = ''
pathResolveRc = 0

if rpInput = '/' then return

rpWork = substr(rpInput,2)
rpWords = translate(rpWork,' ','/')
rpCurrent = '/'

do rpN = 1 to words(rpWords)
  rpPart = word(rpWords,rpN)

  caseOut. = ''
  caseOut.0 = 0
  caseErr. = ''
  caseErr.0 = 0

  caseCmd = "/bin/ls -1A '"rpCurrent"'"
  caseRc = BPXWUNIX(caseCmd,,'caseOut.','caseErr.','zEnv.')

  if caseRc <> 0 then do
    pathResolveRc = caseRc
    pathResolveStatus = 'UNRESOLVED'
    pathResolveDetail = 'PARENT DIRECTORY COULD NOT BE LISTED: 'rpCurrent
    targetPath = rpInput
    return
  end

  rpExact = ''
  rpMatch = ''
  rpMatches = 0

  do rpI = 1 to caseOut.0
    rpEntry = caseOut.rpI

    if rpEntry = rpPart then rpExact = rpEntry

    if translate(rpEntry) = translate(rpPart) then do
      rpMatches = rpMatches + 1
      rpMatch = rpEntry
    end
  end

  if rpExact <> '' then rpChosen = rpExact
  else if rpMatches = 1 then rpChosen = rpMatch
  else if rpMatches > 1 then do
    pathResolveRc = 8
    pathResolveStatus = 'AMBIGUOUS'
    pathResolveDetail = 'MULTIPLE CASE VARIANTS MATCH COMPONENT: 'rpPart
    targetPath = rpInput
    return
  end
  else do
    pathResolveRc = 4
    pathResolveStatus = 'NOT FOUND'
    pathResolveDetail = 'NO DIRECTORY ENTRY MATCHES COMPONENT: 'rpPart
    targetPath = rpInput
    return
  end

  if rpCurrent = '/' then rpCurrent = '/'rpChosen
  else rpCurrent = rpCurrent'/'rpChosen
end

targetPath = rpCurrent

if targetPath <> rpInput then do
  pathResolveStatus = 'CASE-CORRECTED'
  pathResolveDetail = rpInput' -> 'targetPath
end
else pathResolveStatus = 'AS RECEIVED'

pathResolveRc = 0
return


/*-------------------------- D OMVS,F -------------------------------*/

CaptureOmvs:
parse arg coDsn
omvsOut. = ''
omvsOut.0 = 0
omvsCtl. = ''
omvsCtl.0 = 0
omvsRc = -1

coOldSol = SYSVAR('SOLDISP')
coConsole = 'ZF' || left(translate(uid),4,'X') ||,
            substr(time('N'),7,2)

coTrap = OUTTRAP('omvsCtl.',,'NOCONCAT')
Address TSO "CONSPROF SOLDISPLAY(NO)"
Address TSO "CONSOLE ACTIVATE NAME("coConsole")"
coActRc = rc
coTrap = OUTTRAP('OFF')

if coActRc <> 0 then do
  omvsRc = coActRc
  Address TSO "CONSPROF SOLDISPLAY("coOldSol")"
  return
end

Address TSO "CONSOLE SYSCMD(D OMVS,F,NAME="coDsn")"
omvsRc = rc

coMrc = 0
do while coMrc = 0
  coMsg. = ''
  coMsg.0 = 0
  coMrc = GETMSG('coMsg.','SOL',,,3)

  if coMrc = 0 then do
    do coI = 1 to coMsg.0
      omvsOut.0 = omvsOut.0 + 1
      coN = omvsOut.0
      omvsOut.coN = coMsg.coI
    end
  end
end

coTrap = OUTTRAP('omvsCtl2.',,'NOCONCAT')
Address TSO "CONSOLE DEACTIVATE"
Address TSO "CONSPROF SOLDISPLAY("coOldSol")"
coTrap = OUTTRAP('OFF')
return

ParseOmvs:
if symbol('OMVSOUT.0') <> 'VAR' then return

do poI = 1 to omvsOut.0
  poLine = strip(omvsOut.poI)
  poU = translate(poLine)

  poVal = TokenAfter(poLine,'PATH=')
  if poVal <> '' then mountPath = poVal

  poVal = TokenAfter(poLine,'OWNER=')
  if poVal <> '' then fsOwner = translate(poVal)

  poVal = TokenAfter(poLine,'AUTOMOVE=')
  if poVal <> '' then fsAutomove = translate(poVal)

  poVal = TokenAfter(poLine,'CLIENT=')
  if poVal <> '' then fsClient = translate(poVal)

  if left(poU,3) = 'ZFS' | pos(' ZFS ',poU) > 0 then do
    if pos('RDWR',poU) > 0 then fsMode = 'READ/WRITE'
    if pos('RDONLY',poU) > 0 then fsMode = 'READ-ONLY'
  end
end
return


/*------------------------------ df ---------------------------------*/

CaptureDF:
parse arg cdPath
dfOut. = ''
dfOut.0 = 0
dfErr. = ''
dfErr.0 = 0

if strip(cdPath) = '' then do
  dfCmd = '/bin/df -kv'
  dfAll = 1
end
else do
  dfCmd = "/bin/df -kv '"cdPath"'"
  dfAll = 0
end

dfRc = BPXWUNIX(dfCmd,,'dfOut.','dfErr.','zEnv.')
return


ParseDF:
if dfRc <> 0 then return
if symbol('DFOUT.0') <> 'VAR' then return

pdTargetBlock = 0

do pdI = 1 to dfOut.0
  pdLine = strip(dfOut.pdI)
  pdU = translate(pdLine)

  /* df -kv tabular line: mount point followed by (aggregate-name). */
  if left(pdLine,1) = '/' & pos('(',pdLine) > 0 & pos(')',pdLine) > 0 then do
    pdL = pos('(',pdLine)
    pdR = pos(')',pdLine)
    pdTargetBlock = 0

    if pdR > pdL then do
      pdAgg = translate(strip(substr(pdLine,pdL+1,pdR-pdL-1)))

      if targetDsn = '' | pdAgg = targetDsn then do
        pdTargetBlock = 1
        targetDsn = pdAgg
        mountPath = word(pdLine,1)
      end
    end
  end

  if pos('AGGREGATE NAME',pdU) > 0 & pos(':',pdLine) > 0 then do
    pdP = pos(':',pdLine)
    pdVal = translate(strip(word(substr(pdLine,pdP+1),1)))

    if targetDsn = '' then do
      targetDsn = pdVal
      pdTargetBlock = 1
    end
    else if pdVal = targetDsn then pdTargetBlock = 1
  end

  if pos('FILE SYSTEM OWNER',pdU) > 0 & pos(':',pdLine) > 0 then do
    if pdTargetBlock then do
      pdP = pos(':',pdLine)
      pdVal = strip(substr(pdLine,pdP+1))
      if pdVal <> '' then fsOwner = translate(word(pdVal,1))
    end
  end

  if pos('ZFS,',pdU) > 0 & pdTargetBlock then do
    if pos('READ/WRITE',pdU) > 0 then fsMode = 'READ/WRITE'
    if pos('READ-ONLY',pdU) > 0 then fsMode = 'READ-ONLY'
  end
end
return

/*--------------------------- zfsadm fsinfo --------------------------*/

CaptureFsInfoPath:
parse arg cfPath
fsOut. = ''
fsOut.0 = 0
fsErr. = ''
fsErr.0 = 0
fsCmd = "zfsadm fsinfo -path '"cfPath"'"
fsRc = BPXWUNIX(fsCmd,,'fsOut.','fsErr.','zEnv.')
return


CaptureFsInfoDsn:
parse arg cfDsn
fsOut. = ''
fsOut.0 = 0
fsErr. = ''
fsErr.0 = 0
fsCmd = "zfsadm fsinfo -aggregate '"cfDsn"'"
fsRc = BPXWUNIX(fsCmd,,'fsOut.','fsErr.','zEnv.')
return


ParseFsInfo:
if fsRc <> 0 then return
if symbol('FSOUT.0') <> 'VAR' then return

do pfI = 1 to fsOut.0
  pfLine = strip(fsOut.pfI)
  pfU = translate(pfLine)

  pfVal = FirstAfter(pfLine,'FILE SYSTEM NAME:')
  if pfVal <> '' then fsName = translate(pfVal)

  pfVal = FirstAfter(pfLine,'OWNER:')
  if pfVal <> '' & fsOwner = '' then fsOwner = translate(pfVal)

  pfVal = FirstAfter(pfLine,'SIZE:')
  if pfVal <> '' then do
    fsSizeText = pfVal
    fsSizeK = SizeToK(pfVal)
  end

  pfVal = FirstAfter(pfLine,'FREE 8K BLOCKS:')
  if pfVal <> '' & datatype(StripNumber(pfVal),'N') then
    fsFree8K = StripNumber(pfVal)

  pfVal = FirstAfter(pfLine,'VERSION:')
  if pfVal <> '' then fsVersion = pfVal

  pfVal = FirstAfter(pfLine,'STATUS:')
  if pfVal <> '' then fsStatus = translate(pfVal)

  pfVal = FirstAfter(pfLine,'FILE SYSTEM GROW:')
  if pfVal <> '' then fsGrow = translate(pfVal)

  pfVal = FirstAfter(pfLine,'SPACE MONITORING:')
  if pfVal <> '' then fsSpaceMon = pfVal

  if pos('LAST GROW TIME:',pfU) > 0 then do
    pfP = pos('LAST GROW TIME:',pfU)
    pfRest = strip(substr(pfLine,pfP+15))
    if pfRest <> '' then fsLastGrow = pfRest
  end

  pfVal = FirstAfter(pfLine,'ENOSPC ERRORS:')
  if pfVal <> '' & datatype(StripNumber(pfVal),'N') then
    fsEnospc = StripNumber(pfVal)

  pfVal = FirstAfter(pfLine,'DISK IO ERRORS:')
  if pfVal <> '' & datatype(StripNumber(pfVal),'N') then
    fsIoErrors = StripNumber(pfVal)
end

if targetDsn = '' & fsName <> '' then targetDsn = translate(fsName)
return


/*-------------------------- zfsadm aggrinfo -------------------------*/

CaptureAggrInfo:
parse arg caDsn
agOut. = ''
agOut.0 = 0
agErr. = ''
agErr.0 = 0
agCmd = "zfsadm aggrinfo -aggregate '"caDsn"' -long"
agRc = BPXWUNIX(agCmd,,'agOut.','agErr.','zEnv.')
return


ParseAggrInfo:
if agRc <> 0 then return
if symbol('AGOUT.0') <> 'VAR' then return

do paI = 1 to agOut.0
  paLine = strip(agOut.paI)
  paU = translate(paLine)

  if pos(' K FREE OUT OF TOTAL ',paU) > 0 then do
    paP = pos(' K FREE OUT OF TOTAL ',paU)
    paLeft = strip(substr(paLine,1,paP-1))
    paRest = strip(substr(paLine,paP+length(' K FREE OUT OF TOTAL ')))
    paFree = StripNumber(word(paLeft,words(paLeft)))
    paTotal = StripNumber(word(paRest,1))

    if datatype(paFree,'N') then aggrFreeK = paFree
    if datatype(paTotal,'N') then aggrTotalK = paTotal
  end

  if pos('(',paLine) > 0 & pos('):',paLine) > pos('(',paLine) then do
    paL = pos('(',paLine)
    paR = pos('):',paLine)
    if paR > paL then aggrState = strip(substr(paLine,paL+1,paR-paL-1))
  end
end

if aggrTotalK > 0 & aggrFreeK >= 0 then
  usedPct = ((aggrTotalK-aggrFreeK) / aggrTotalK) * 100
return


/*------------------------------ LISTCAT -----------------------------*/

CaptureListcat:
parse arg clDsn
lcOut. = ''
lcOut.0 = 0

clTrap = outtrap('lcOut.')
Address TSO "LISTCAT ENT('"clDsn"') ALL"
lcRc = rc
clTrap = outtrap('OFF')
return


ParseListcat:
if lcRc <> 0 then return
if symbol('LCOUT.0') <> 'VAR' then return

do plI = 1 to lcOut.0
  plLine = strip(lcOut.plI)
  plU = translate(plLine)

  if spaceType = '' & pos('SPACE-TYPE',plU) > 0 then
    spaceType = CatValue(plLine,'SPACE-TYPE')

  if spacePri = '' & pos('SPACE-PRI',plU) > 0 then
    spacePri = CatValue(plLine,'SPACE-PRI')

  if spaceSec = '' & pos('SPACE-SEC',plU) > 0 then
    spaceSec = CatValue(plLine,'SPACE-SEC')

  if hiARba < 0 & pos('HI-A-RBA',plU) > 0 then do
    plVal = StripNumber(CatValue(plLine,'HI-A-RBA'))
    if datatype(plVal,'N') then hiARba = plVal
  end

  if hiURba < 0 & pos('HI-U-RBA',plU) > 0 then do
    plVal = StripNumber(CatValue(plLine,'HI-U-RBA'))
    if datatype(plVal,'N') then hiURba = plVal
  end

  if pos('EXT-ADDR',plU) > 0 then do
    plVal = translate(CatValue(plLine,'EXT-ADDR'))
    if plVal = 'YES' | plVal = 'NO' then extAddr = plVal
  end

  /* EXTENDED in LISTCAT is the extended-format indicator. */
  if pos('EXTENDED',plU) > 0 & pos('EXT-ADDR',plU) = 0 then do
    plVal = translate(CatValue(plLine,'EXTENDED'))
    if plVal = 'YES' | plVal = 'NO' then extFormat = plVal
  end

  if storClas = '' & pos('STORCLAS',plU) > 0 then
    storClas = CatValue(plLine,'STORCLAS')

  if dataClas = '' & pos('DATACLAS',plU) > 0 then
    dataClas = CatValue(plLine,'DATACLAS')

  if mgmtClas = '' & pos('MGMTCLAS',plU) > 0 then
    mgmtClas = CatValue(plLine,'MGMTCLAS')

  if pos('VOLSER',plU) > 0 then do
    plVal = translate(CatValue(plLine,'VOLSER'))
    if plVal <> '' & plVal <> '------------' then call AddVolume plVal
  end

  if pos('EXTENT-NUMBER',plU) > 0 then do
    plVal = StripNumber(CatValue(plLine,'EXTENT-NUMBER'))
    if datatype(plVal,'W') then do
      extentRecords = extentRecords + 1
      if plVal > maxExtentNumber then maxExtentNumber = plVal
    end
  end

  if pos('EXTENTS',plU) > 0 & pos('EXTENT-NUMBER',plU) = 0 then do
    plVal = StripNumber(CatValue(plLine,'EXTENTS'))
    if datatype(plVal,'W') then do
      if listExtents < plVal then listExtents = plVal
    end
  end
end

if listExtents < 0 & extentRecords > 0 then
  listExtents = extentRecords

if hiARba >= 0 & hiURba >= 0 & hiARba > hiURba then do
  allocGapBytes = hiARba - hiURba
  allocGapK = allocGapBytes / 1024
end
return


AddVolume:
parse arg av
av = translate(strip(av))
if av = '' then return

do avI = 1 to volCount
  if vol.avI = av then return
end

volCount = volCount + 1
vol.volCount = av
return


/*-------------------------- Optional FILES scan ---------------------*/

CaptureFiles:
parse arg cfMount
fileOut. = ''
fileOut.0 = 0
fileErr. = ''
fileErr.0 = 0

/* find -xdev keeps the scan on this file system.  The threshold avoids */
/* launching ls for every tiny file.  Results are sorted by byte size. */
fileCmd = "/bin/find '"cfMount"' -xdev -type f -size +2047 "
fileCmd = fileCmd"-exec /bin/ls -ln {} \; | /bin/sort -k5,5nr "
fileCmd = fileCmd"| /bin/head -30"

fileRc = BPXWUNIX(fileCmd,,'fileOut.','fileErr.','zEnv.')
return


/*====================================================================*/
/* ASSESSMENT                                                         */
/*====================================================================*/

DetermineAssessment:
collectionFailed = 0
if targetType = 'PATH' then do
  if targetDsn = '' then collectionFailed = 1
end
else do
  if mountPath = '' & fsRc <> 0 & lcRc <> 0 then collectionFailed = 1
end

flagGF = StatusFlag(fsStatus,'GF')
flagGD = StatusFlag(fsStatus,'GD')
flagIE = StatusFlag(fsStatus,'IE')

if left(translate(fsGrow),3) = 'OFF' then growOff = 1
if pos('QUIESC',translate(aggrState)) > 0 then quiesced = 1

if datatype(StripNumber(spaceSec),'N') then do
  if StripNumber(spaceSec) = 0 then zeroSecondary = 1
end

if listExtents >= 123 then highExtent = 1
if listExtents >= 255 then veryHighExtent = 1

fourGBBytes = 4294967296
fourGBK = 4194304

if extAddr = 'NO' then do
  if hiARba >= fourGBBytes * 0.95 then nonEANearLimit = 1
  else if aggrTotalK >= fourGBK * 0.95 then nonEANearLimit = 1
  else if fsSizeK >= fourGBK * 0.95 then nonEANearLimit = 1
end

/* Severity is incident pressure, not an IBM-defined threshold. */
if fsIoErrors > 0 | flagIE then severity = 'CRITICAL'
else if quiesced then severity = 'CRITICAL'
else if fsEnospc > 0 then severity = 'CRITICAL'
else if usedPct >= 99 then severity = 'CRITICAL'
else if usedPct >= 95 | flagGF | flagGD then severity = 'HIGH'
else if usedPct >= 85 then severity = 'MEDIUM'
else if usedPct >= 0 then severity = 'LOW'
else if fsStatus <> '' | lcRc = 0 then severity = 'UNKNOWN'
else severity = 'UNKNOWN'

/* Root-cause classification is deliberately conservative. */
if collectionFailed then do
  diagnosis = 'DIAGNOSTIC DATA COLLECTION FAILED'
  confidence = 'HIGH'
  severity = 'UNKNOWN'
end
else if fsIoErrors > 0 | flagIE then do
  diagnosis = 'zFS / DASD I/O ERROR CONDITION'
  confidence = 'HIGH'
end
else if quiesced then do
  diagnosis = 'zFS AGGREGATE IS QUIESCED'
  confidence = 'HIGH'
end
else if nonEANearLimit then do
  diagnosis = 'NON-EXTENDED-ADDRESSABLE SIZE LIMIT PRESSURE'
  confidence = 'HIGH'
end
else if usedPct >= 90 & allocGapBytes >= 8388608 then do
  diagnosis = 'ALLOCATED VSAM SPACE EXISTS BEYOND FORMATTED zFS SIZE'
  confidence = 'HIGH'
end
else if flagGF & zeroSecondary then do
  diagnosis = 'DYNAMIC GROW FAILURE WITH ZERO SECONDARY ALLOCATION'
  confidence = 'MEDIUM'
end
else if flagGF & highExtent then do
  diagnosis = 'DYNAMIC GROW FAILURE WITH HIGH EXTENT COUNT'
  confidence = 'MEDIUM'
end
else if flagGF then do
  diagnosis = 'PREVIOUS DYNAMIC zFS GROW FAILURE'
  confidence = 'HIGH'
end
else if flagGD then do
  diagnosis = 'DYNAMIC zFS GROW IS CURRENTLY DISABLED'
  confidence = 'HIGH'
end
else if growOff & usedPct >= 85 then do
  diagnosis = 'SPACE PRESSURE WITH AUTOMATIC FILE-SYSTEM GROW OFF'
  confidence = 'HIGH'
end
else if zeroSecondary & usedPct >= 85 then do
  diagnosis = 'SPACE PRESSURE WITH ZERO SECONDARY ALLOCATION'
  confidence = 'MEDIUM'
end
else if veryHighExtent & usedPct >= 85 then do
  diagnosis = 'SPACE PRESSURE WITH VERY HIGH EXTENT COUNT'
  confidence = 'MEDIUM'
end
else if usedPct >= 95 then do
  diagnosis = 'SEVERE zFS SPACE PRESSURE'
  confidence = 'HIGH'
end
else if usedPct >= 85 then do
  diagnosis = 'zFS SPACE PRESSURE'
  confidence = 'HIGH'
end
else if usedPct >= 0 then do
  diagnosis = 'NO CURRENT CRITICAL zFS SPACE PRESSURE CONFIRMED'
  confidence = 'MEDIUM'
end
else do
  diagnosis = 'CAPACITY COULD NOT BE FULLY DETERMINED'
  confidence = 'LOW'
end
return


/*====================================================================*/
/* REPORT                                                             */
/*====================================================================*/

Header:
call Emit ''
call Emit copies('=',78)
call Emit ' ZFSDIAG - zFS SPACE / ALLOCATION DIAGNOSTIC'
call KV 'Author', 'SARATH S'
call KV 'Version', '1.5'
call KV 'Date', DisplayDate()
call KV 'Time', time('N')
call KV 'System', localSys
call Emit copies('=',78)
return


ReportInformation:
call Section 'REPORT INFORMATION'
call KV 'Utility', 'ZFSDIAG'
call KV 'Report type', 'LIVE DIAGNOSTIC'
call KV 'System', localSys
call KV 'Execution user', uid
call KV 'Generated', DisplayDate()' 'time('N')
call KV 'Output data set', reportDsn
call KV 'Changes performed', 'NONE'
return


TriageSummary:
call Section 'TRIAGE SUMMARY'
call KV 'Severity', severity
call KV 'Assessment', diagnosis
call KV 'Assessment confidence', confidence

if usedPct >= 0 then call KV 'zFS used', FormatPct(usedPct)
else call KV 'zFS used', 'N/A'

if aggrFreeK >= 0 then call KV 'zFS free', HumanK(aggrFreeK)
else call KV 'zFS free', 'N/A'

if fsEnospc >= 0 then call KV 'Recorded ENOSPC errors', fsEnospc
else call KV 'Recorded ENOSPC errors', 'N/A'

if fsIoErrors >= 0 then call KV 'Recorded disk I/O errors', fsIoErrors
else call KV 'Recorded disk I/O errors', 'N/A'

call KV 'Dynamic grow failed (GF)', YesNo(flagGF)
call KV 'Dynamic grow disabled (GD)', YesNo(flagGD)
call KV 'FILES scan requested', YesNo(filesMode)

call Emit ''
call Emit 'Collection status:'
if targetType = 'PATH' then do
  call KV '  Path case resolution', pathResolveStatus
  call KV '  df -kv RC', RcText(dfRc)
  call KV '  zfsadm fsinfo RC', RcText(fsRc)
end
else do
  call KV '  D OMVS,F,NAME= RC', RcText(omvsRc)
  call KV '  zfsadm fsinfo RC', RcText(fsRc)
end
call KV '  zfsadm aggrinfo RC', RcText(agRc)
call KV '  LISTCAT RC', RcText(lcRc)

if collectionFailed then do
  call Emit ''
  call Emit '*** COLLECTION FAILURE - SEE RAW COMMAND EVIDENCE BELOW ***'
end
return


TargetDetails:
call Section 'TARGET / MOUNT INFORMATION'
call KV 'Input type', targetType

if targetType = 'PATH' then do
  call KV 'Path received by REXX', ValueOr(inputPath,targetPath)
  call KV 'Resolved USS path', targetPath
  call KV 'Path case handling', pathResolveStatus
  if pathResolveDetail <> '' then
    call KV 'Path resolution detail', pathResolveDetail
end
call KV 'zFS aggregate', ValueOr(targetDsn,'UNRESOLVED')
call KV 'Mount point', ValueOr(mountPath,'UNRESOLVED')
call KV 'Owner system', ValueOr(fsOwner,'N/A')
call KV 'Mount mode', ValueOr(fsMode,'N/A')
call KV 'AUTOMOVE', ValueOr(fsAutomove,'N/A')
call KV 'CLIENT', ValueOr(fsClient,'N/A')

call Emit ''
call KV 'D OMVS,F,NAME= capture RC', RcText(omvsRc)
call KV 'df -kv RC', RcText(dfRc)
call KV 'zfsadm fsinfo RC', RcText(fsRc)
return


CapacityDetails:
call Section 'zFS CAPACITY / GROW STATE'

if aggrTotalK >= 0 then call KV 'Aggregate total', HumanK(aggrTotalK)
else if fsSizeK >= 0 then call KV 'Aggregate size', HumanK(fsSizeK)
else call KV 'Aggregate size', 'N/A'

if aggrFreeK >= 0 then call KV 'Free space', HumanK(aggrFreeK)
else if fsFree8K >= 0 then call KV 'Free 8K blocks', fsFree8K
else call KV 'Free space', 'N/A'

if usedPct >= 0 then call KV 'Used percent', FormatPct(usedPct)
else call KV 'Used percent', 'N/A'

call KV 'zFS version', ValueOr(fsVersion,'N/A')
call KV 'zfsadm status flags', ValueOr(fsStatus,'N/A')
call KV 'File System Grow', ValueOr(fsGrow,'N/A')
call KV 'Space Monitoring', ValueOr(fsSpaceMon,'N/A')
call KV 'Last Grow Time', ValueOr(fsLastGrow,'N/A')
call KV 'aggrinfo state', ValueOr(aggrState,'N/A')
return


VSAMDetails:
call Section 'VSAM LDS / ALLOCATION OBSERVATIONS'
call KV 'LISTCAT RC', RcText(lcRc)
call KV 'Allocation unit', ValueOr(spaceType,'N/A')
call KV 'Primary allocation', ValueOr(spacePri,'N/A')
call KV 'Secondary allocation', ValueOr(spaceSec,'N/A')
call KV 'Extended addressable', extAddr
call KV 'Extended format', extFormat
call KV 'HI-A-RBA', NumOrNA(hiARba)
call KV 'HI-U-RBA', NumOrNA(hiURba)

if allocGapBytes > 0 then
  call KV 'Allocated beyond HI-U-RBA', HumanK(allocGapK)
else
  call KV 'Allocated beyond HI-U-RBA', 'NONE DETECTED'

call KV 'Extent count observed', NumOrNA(listExtents)
call KV 'Volumes observed', volCount

if volCount > 0 then do
  vs = ''
  do vdI = 1 to volCount
    if vdI = 1 then vs = vol.vdI
    else vs = vs', 'vol.vdI
  end
  call KV 'Volume list', vs
end

call KV 'Storage class', ValueOr(storClas,'N/A')
call KV 'Data class', ValueOr(dataClas,'N/A')
call KV 'Management class', ValueOr(mgmtClas,'N/A')

if highExtent then do
  call Emit ''
  call Emit 'EXTENT NOTE: An observed count of 123 or more is a review'
  call Emit 'signal, not automatic proof that the zFS must be reallocated.'
  call Emit 'The per-volume distribution, SMS rules and current allocation'
  call Emit 'must be checked before choosing multi-volume or migration.'
end
return


FileScanReport:
call Section 'OPTIONAL LARGE-FILE SCAN'

if filesMode = 0 then do
  call KV 'Scan status', 'NOT REQUESTED'
  call Emit 'No recursive file scan was performed. This keeps the normal'
  call Emit 'ZFSDIAG path lightweight. Add FILES only when content-level'
  call Emit 'space analysis is needed.'
  return
end

if mountPath = '' then do
  call KV 'Scan status', 'NOT RUN - MOUNT POINT UNRESOLVED'
  call Emit 'FILES scan was requested, but the mount point could not be'
  call Emit 'resolved. No recursive file scan was attempted.'
  return
end

call KV 'Scan status', 'REQUESTED'
call KV 'Mount point scanned', mountPath
call KV 'Command RC', fileRc
call Emit 'The scan stays on this file system (-xdev), selects larger'
call Emit 'regular files, sorts by size and returns at most 30 rows.'
call Emit ''

if fileRc <> 0 then do
  call Emit 'The FILES scan did not complete successfully.'
  call Emit 'STDERR:'
  call EmitStem 'fileErr.', 20
  return
end

if fileOut.0 = 0 then do
  call Emit 'No matching large files were returned by the bounded scan.'
  return
end

call Emit 'Largest matching files (ls -ln; size is the fifth field):'
call EmitStem 'fileOut.', 30
call Emit ''
call Emit 'These are review candidates only. ZFSDIAG does not decide that'
call Emit 'a file is obsolete and never deletes a file.'
return

Analysis:
call Section 'ANALYSIS / INTERPRETATION'

select
  when diagnosis = 'DIAGNOSTIC DATA COLLECTION FAILED' then do
    call Emit 'ZFSDIAG could not obtain the minimum command data required'
    call Emit 'to analyse this target. This is a collection/interface'
    call Emit 'failure, not evidence that the zFS itself is healthy.'
    call Emit ''
    call Emit 'Review the return codes and STDERR/control messages in'
    call Emit 'RAW COMMAND EVIDENCE.'
    if targetType = 'PATH' & pathResolveStatus <> 'AS RECEIVED' &,
       pathResolveStatus <> 'CASE-CORRECTED' then do
      call Emit ''
      call Emit 'Path case resolution did not produce a usable path:'
      call Emit '  'pathResolveStatus' - 'pathResolveDetail
    end
  end

  when diagnosis = 'zFS / DASD I/O ERROR CONDITION' then do
    call Emit 'The evidence contains zFS/DASD I/O-error indicators.'
    call Emit 'Treat this as an I/O/storage condition first; adding space'
    call Emit 'or deleting files would not address the underlying error.'
  end

  when diagnosis = 'zFS AGGREGATE IS QUIESCED' then do
    call Emit 'The aggregate state indicates that the zFS is quiesced.'
    call Emit 'A quiesced file system can look like an application write'
    call Emit 'problem even when raw free-space capacity is not the cause.'
  end

  when diagnosis = 'NON-EXTENDED-ADDRESSABLE SIZE LIMIT PRESSURE' then do
    call Emit 'LISTCAT indicates EXT-ADDR=NO and the allocation is close'
    call Emit 'to the traditional 4-GB non-EA boundary.'
    call Emit 'This is a structural allocation limit, not a cleanup-only'
    call Emit 'problem. Plan migration/reallocation to an EA aggregate.'
  end

  when diagnosis =,
    'ALLOCATED VSAM SPACE EXISTS BEYOND FORMATTED zFS SIZE' then do
    call Emit 'HI-A-RBA is greater than HI-U-RBA while the zFS is under'
    call Emit 'space pressure. The LDS already has additional allocation'
    call Emit 'that is not represented in the currently formatted zFS.'
    call Emit 'Review explicit zFS grow use before requesting more DASD.'
  end

  when diagnosis =,
    'DYNAMIC GROW FAILURE WITH ZERO SECONDARY ALLOCATION' then do
    call Emit 'zfsadm status records a failed dynamic grow and LISTCAT'
    call Emit 'shows zero secondary allocation. Automatic growth is not'
    call Emit 'properly provisioned for repeated space expansion.'
    call Emit 'An explicit grow can still be possible if allocation can'
    call Emit 'be obtained; do not equate secondary=0 with total inability'
    call Emit 'to grow.'
  end

  when diagnosis =,
    'DYNAMIC GROW FAILURE WITH HIGH EXTENT COUNT' then do
    call Emit 'A prior dynamic grow failed and the catalog shows a high'
    call Emit 'extent count. This makes allocation/extent pressure a strong'
    call Emit 'candidate, but this report does not assume 123 extents means'
    call Emit 'automatic reallocation; per-volume evidence still matters.'
  end

  when diagnosis = 'PREVIOUS DYNAMIC zFS GROW FAILURE' then do
    call Emit 'The GF status shows that a dynamic grow attempt failed.'
    call Emit 'The status alone does not identify whether the cause was'
    call Emit 'volume space, SMS allocation, extent pressure or another'
    call Emit 'allocation error. Use the VSAM/storage evidence above.'
  end

  when diagnosis = 'DYNAMIC zFS GROW IS CURRENTLY DISABLED' then do
    call Emit 'The GD status indicates that dynamic grow is disabled for'
    call Emit 'the current attached aggregate. The underlying failed-grow'
    call Emit 'cause should be corrected before relying on automatic grow.'
  end

  when diagnosis =,
    'SPACE PRESSURE WITH AUTOMATIC FILE-SYSTEM GROW OFF' then do
    call Emit 'The zFS is under space pressure and File System Grow is OFF.'
    call Emit 'Automatic dynamic growth should not be expected in this'
    call Emit 'state. Determine whether the site intends this setting.'
  end

  when diagnosis = 'SPACE PRESSURE WITH ZERO SECONDARY ALLOCATION' then do
    call Emit 'The zFS is under space pressure and LISTCAT shows no'
    call Emit 'secondary allocation. This weakens automatic-grow capability'
    call Emit 'and is also a permanent-allocation design issue to review.'
  end

  when diagnosis = 'SEVERE zFS SPACE PRESSURE' |,
       diagnosis = 'zFS SPACE PRESSURE' then do
    call Emit 'The aggregate is genuinely low on free space, but the'
    call Emit 'available evidence does not prove a structural grow limit.'
    call Emit 'The next decision is whether reclaimable files exist or'
    call Emit 'additional underlying allocation is required.'
  end

  when diagnosis = 'NO CURRENT CRITICAL zFS SPACE PRESSURE CONFIRMED' then do
    call Emit 'Current aggregate capacity does not show severe space'
    call Emit 'pressure. If the ticket reported a past failure, correlate'
    call Emit 'with the incident time and application messages.'
  end

  otherwise do
    call Emit 'The utility could not obtain enough capacity evidence to'
    call Emit 'assign a reliable root-cause branch.'
  end
end
return


Recommendations:
call Section 'RECOMMENDED DEBUGGING / POSSIBLE RESOLUTION'

rec = 0

if collectionFailed then do
  rec = rec + 1
  call Emit rec'. Fix/verify command collection before interpreting zFS'
  call Emit '   capacity. RAW COMMAND EVIDENCE now shows the command RC'
  call Emit '   and captured error/control text.'
  rec = rec + 1
  call Emit rec'. For a PATH target, verify /bin/df and zfsadm'
  call Emit '   work for the same userid and that the path exists.'
  rec = rec + 1
  call Emit rec'. For a DSN target, verify D OMVS,F,NAME=<dataset> and'
  call Emit '   the caller''s TSO CONSOLE/OPERCMDS authority.'
end

if fsIoErrors > 0 | flagIE then do
  rec = rec + 1
  call Emit rec'. Investigate the reported I/O/storage error before any'
  call Emit '   space-growth or cleanup action.'
end

if quiesced then do
  rec = rec + 1
  call Emit rec'. Determine why the aggregate is quiesced and follow the'
  call Emit '   owning application/storage recovery procedure.'
end

if nonEANearLimit then do
  rec = rec + 1
  call Emit rec'. Plan a replacement zFS with extended addressability and'
  call Emit '   migrate the existing contents using the site procedure.'
end

if allocGapBytes > 0 then do
  rec = rec + 1
  call Emit rec'. Review the existing HI-A-RBA versus HI-U-RBA allocation'
  call Emit '   before requesting additional DASD. Existing LDS space may'
  call Emit '   be usable by an explicit administrator grow.'
end

if flagGF | flagGD then do
  rec = rec + 1
  call Emit rec'. Review the failed/delayed grow condition and the actual'
  call Emit '   allocation failure messages. Do not repeatedly issue grow'
  call Emit '   without correcting the reason it failed.'
end

if zeroSecondary then do
  rec = rec + 1
  call Emit rec'. For a permanent fix, review reallocation/migration with'
  call Emit '   an appropriate secondary allocation for future growth.'
end

if highExtent then do
  rec = rec + 1
  call Emit rec'. Review extent distribution by volume, SMS allocation and'
  call Emit '   candidate-volume options. High extent count alone does not'
  call Emit '   prove that immediate reallocation is the only solution.'
end

if usedPct >= 85 then do
  if filesMode = 0 then do
    rec = rec + 1
    call Emit rec'. If old logs/temp files are plausible, run the optional:'
    if targetType = 'PATH' then
      call Emit '      TSO ZFSDIAG 'targetPath' FILES'
    else
      call Emit '      TSO ZFSDIAG 'targetDsn' FILES'
    call Emit '   FILES is intentionally separate because it recursively'
    call Emit '   scans the file system.'
  end
  else do
    rec = rec + 1
    call Emit rec'. Review the large-file candidates with the application'
    call Emit '   owner. Delete/rotate only files confirmed safe to remove.'
  end
end

if flagGF & zeroSecondary = 0 & highExtent = 0 then do
  rec = rec + 1
  call Emit rec'. If the zFS is structurally grow-capable, have storage/SMS'
  call Emit '   verify whether the current volume/allocation can obtain'
  call Emit '   additional space or requires another volume.'
end

if usedPct >= 95 then do
  rec = rec + 1
  call Emit rec'. For a critical file system that cannot be detached,'
  call Emit '   coordinate the least disruptive storage option. Expanding'
  call Emit '   the underlying volume is site/hardware dependent and only'
  call Emit '   helps when volume capacity is actually the bottleneck.'
end

if rec = 0 then do
  call Emit '1. No immediate capacity recovery action is indicated by the'
  call Emit '   current snapshot.'
  call Emit '2. If the incident is intermittent, rerun ZFSDIAG while the'
  call Emit '   write/ENOSPC condition is active.'
end
else do
  rec = rec + 1
  call Emit rec'. After approved remediation, rerun ZFSDIAG and verify'
  call Emit '   that free space, grow status and application writes recover.'
end

call Emit ''
call Emit 'ZFSDIAG intentionally recommends actions only; it does not issue'
call Emit 'grow, delete, ALTER, mount/unmount or storage-change commands.'
return


CommandReference:
call Section 'COMMAND REFERENCE'

call Emit 'DISPLAY / MODIFY COMMANDS'
call Emit '  D OMVS,F,NAME='ValueOr(targetDsn,'<zfs-dataset>')
call Emit '  F BPXOINIT,FILESYS=D,ALL'
call Emit ''

call Emit 'USS / zFS DIAGNOSTIC COMMANDS'
if targetType = 'PATH' then call Emit '  df -kv 'targetPath
else if mountPath <> '' then call Emit '  df -kv 'mountPath
else call Emit '  df -kv <mount-point>'
call Emit '  zfsadm fsinfo -aggregate 'ValueOr(targetDsn,'<zfs-dataset>')
call Emit '  zfsadm aggrinfo -aggregate 'ValueOr(targetDsn,'<zfs-dataset>')' -lo
ng'
call Emit ''

call Emit 'TSO / VSAM DIAGNOSTIC COMMAND'
call Emit "  LISTCAT ENT('"ValueOr(targetDsn,'<zfs-dataset>')"') ALL"
call Emit ''

call Emit 'POSSIBLE RECOVERY COMMAND - NEVER ISSUED BY ZFSDIAG'
call Emit '  zfsadm grow 'ValueOr(targetDsn,'<zfs-dataset>')' 0'
call Emit '  Use only after live evidence confirms that an explicit grow is'
call Emit '  appropriate. Size 0 requests growth using the LDS secondary'
call Emit '  allocation; it is not appropriate when that prerequisite is'
call Emit '  absent or when the allocation failure remains unresolved.'
return


EvidenceReport:
call Section 'RAW COMMAND EVIDENCE'

call Emit 'USS environment supplied to BPXWUNIX:'
call Emit '  PATH=/bin:/usr/bin:/usr/sbin:/usr/lpp/dfs/global/bin'
call Emit '  LIBPATH=/lib:/usr/lib'
call Emit ''

if targetType = 'PATH' then do
  call Emit 'PATH CASE RESOLUTION'
  call KV 'Received', ValueOr(inputPath,targetPath)
  call KV 'Resolved', targetPath
  call KV 'Status', pathResolveStatus
  if pathResolveDetail <> '' then call KV 'Detail', pathResolveDetail
  call Emit ''
end

call Emit '1. D OMVS,F,NAME=<aggregate>'
call KV 'RC', RcText(omvsRc)
if omvsOut.0 > 0 then call EmitStem 'omvsOut.', 40
else do
  call Emit '   No MVS command response captured.'
  if omvsCtl.0 > 0 then do
    call Emit '   TSO CONSOLE control messages:'
    call EmitStem 'omvsCtl.', 20
  end
end

call Emit ''
call Emit '2. df -kv'
call KV 'Executed', ValueOr(dfCmd,'NOT RUN')
call KV 'RC', RcText(dfRc)
if dfOut.0 > 0 then call EmitStem 'dfOut.', 40
else do
  call Emit '   No captured stdout.'
  if dfErr.0 > 0 then do
    call Emit '   STDERR:'
    call EmitStem 'dfErr.', 20
  end
  else if dfRc <> -1 then
    call Emit '   STDERR: <command returned no stderr text>'
end

call Emit ''
call Emit '3. zfsadm fsinfo'
call KV 'Executed', ValueOr(fsCmd,'NOT RUN')
call KV 'RC', RcText(fsRc)
if fsOut.0 > 0 then call EmitStem 'fsOut.', 60
else do
  call Emit '   No captured stdout.'
  if fsErr.0 > 0 then do
    call Emit '   STDERR:'
    call EmitStem 'fsErr.', 20
  end
  else if fsRc <> -1 then
    call Emit '   STDERR: <command returned no stderr text>'
end

call Emit ''
call Emit '4. zfsadm aggrinfo -long'
call KV 'Executed', ValueOr(agCmd,'NOT RUN')
call KV 'RC', RcText(agRc)
if agOut.0 > 0 then call EmitStem 'agOut.', 60
else do
  call Emit '   No captured stdout.'
  if agErr.0 > 0 then do
    call Emit '   STDERR:'
    call EmitStem 'agErr.', 20
  end
  else if agRc <> -1 then
    call Emit '   STDERR: <command returned no stderr text>'
end

call Emit ''
call Emit '5. TSO LISTCAT ENT(<aggregate>) ALL'
call KV 'RC', RcText(lcRc)
if lcOut.0 > 0 then call EmitStem 'lcOut.', 120
else call Emit '   No captured output.'
return


Notes:
call Section 'SAFETY / IMPORTANT NOTES'
call Emit '  * ZFSDIAG is read-only by design.'
call Emit '  * PATH mode can case-correct an uppercased ISPF path when the'
call Emit '    matching USS directory entry is unambiguous.'
call Emit '  * Normal path/DSN mode uses live system data only. For a'
call Emit '    fabricated demonstration, invoke TSO ZFSDIAG SAMPLE.'
call Emit '  * It does not automatically issue ZFSADM GROW.'
call Emit '  * It does not delete, rename, truncate or rotate USS files.'
call Emit '  * It does not ALTER VSAM, add volumes, mount or unmount zFS.'
call Emit '  * D OMVS,F,NAME= is best effort and depends on console authority.'
call Emit '  * USS commands depend on the caller''s OMVS environment and'
call Emit '    access to the target path/aggregate.'
call Emit '  * LISTCAT parsing is evidence-based; site/release formatting'
call Emit '    differences are preserved in the raw evidence section.'
call Emit '  * 123 extents is treated as a review signal, not an automatic'
call Emit '    instruction to reallocate the aggregate.'
call Emit '  * FILES mode is optional because recursive metadata scanning'
call Emit '    can cause noticeable I/O on very large file systems.'
call Emit '  * Severity labels are ZFSDIAG triage heuristics, not IBM'
call Emit '    severity thresholds.'
return


Footer:
call Emit ''
call Emit copies('=',78)
call Emit ' ZFSDIAG completed. No system or workload changes were made.'
call Emit copies('=',78)
return



/*====================================================================*/
/* SAMPLE REPORT                                                      */
/*====================================================================*/

SampleReport:
call Emit ''
call Emit copies('=',78)
call Emit ' ZFSDIAG - zFS SPACE / ALLOCATION DIAGNOSTIC'
call Emit ' *** SAMPLE REPORT - MOST VALUES BELOW ARE DEMONSTRATION DATA ***'
call Emit copies('=',78)

call Section 'REPORT INFORMATION'
call KV 'Utility', 'ZFSDIAG'
call KV 'Report type', 'SAMPLE / DEMONSTRATION'
call KV 'System', localSys
call KV 'Execution user', uid
call KV 'Generated', DisplayDate()' 'time('N')
call KV 'Output data set', sampleDsn
call KV 'Changes performed', 'NONE'
call KV 'Live system data used', 'SYSTEM NAME / USER / DATE-TIME ONLY'
call Emit ''
call Emit 'All aggregate names, mount points, capacities, extents, files and'
call Emit 'diagnostic findings below are fabricated sample values. The command'
call Emit 'syntax is shown so users know what ZFSDIAG uses/recommends.'

call Section 'TRIAGE SUMMARY'
call KV 'Severity', 'CRITICAL'
call KV 'Assessment', 'PREVIOUS DYNAMIC zFS GROW FAILURE'
call KV 'Assessment confidence', 'HIGH'
call KV 'zFS used', '99.6 %'
call KV 'zFS free', '164 MB'
call KV 'Recorded ENOSPC errors', '27'
call KV 'Recorded disk I/O errors', '0'
call KV 'Dynamic grow failed (GF)', 'YES'
call KV 'Dynamic grow disabled (GD)', 'YES'
call KV 'FILES scan requested', 'NO'

call Section 'TARGET / MOUNT INFORMATION'
call KV 'Input type', 'DSN'
call KV 'zFS aggregate', 'OMVS.PROD.APP.ZFS'
call KV 'Mount point', '/var/app'
call KV 'Owner system', localSys
call KV 'Mount mode', 'READ/WRITE'
call KV 'AUTOMOVE', 'UNMOUNT'
call KV 'CLIENT', 'N/A'
call KV 'D OMVS,F,NAME= capture RC', '0'
call KV 'df -kv RC', '0'
call KV 'zfsadm fsinfo RC', '0'

call Section 'zFS CAPACITY / GROW STATE'
call KV 'Aggregate total', '40.0 GB'
call KV 'Free space', '164 MB'
call KV 'Used percent', '99.6 %'
call KV 'zFS version', '1.5'
call KV 'zfsadm status flags', 'GF,GD'
call KV 'File System Grow', 'OFF'
call KV 'Space Monitoring', 'ENABLED'
call KV 'Last Grow Time', '20-AUG-2026 21:42:16'
call KV 'aggrinfo state', 'R/W COMP'

call Section 'VSAM LDS / ALLOCATION OBSERVATIONS'
call KV 'LISTCAT RC', '0'
call KV 'Allocation unit', 'CYLINDER'
call KV 'Primary allocation', '12000'
call KV 'Secondary allocation', '500'
call KV 'Extended addressable', 'YES'
call KV 'Extended format', 'YES'
call KV 'HI-A-RBA', '42949672960'
call KV 'HI-U-RBA', '42949672960'
call KV 'Allocated beyond HI-U-RBA', 'NONE DETECTED'
call KV 'Extent count observed', '118'
call KV 'Volumes observed', '1'
call KV 'Volume list', 'PRD901'
call KV 'Storage class', 'SCSTD'
call KV 'Data class', 'DCZFS'
call KV 'Management class', 'MCSTD'

call Section 'OPTIONAL LARGE-FILE SCAN'
call KV 'Scan status', 'NOT RUN IN THIS SAMPLE SCENARIO'
call Emit 'Example of what FILES mode could highlight:'
call Emit '  8.4 GB  /var/app/log/server.log'
call Emit '  5.7 GB  /var/app/archive/server.log.old'
call Emit '  3.9 GB  /var/app/archive/debug.log'
call Emit ''
call Emit 'ZFSDIAG would identify these only as review candidates. It would'
call Emit 'not state that they are safe to delete and would not delete them.'

call Section 'ANALYSIS / INTERPRETATION'
call Emit 'The sample zFS is genuinely almost full. Extended addressability'
call Emit 'and a non-zero secondary allocation are present, so the sample'
call Emit 'does not show a 4-GB non-EA limit or zero-secondary condition.'
call Emit ''
call Emit 'The GF/GD state indicates that a previous dynamic growth attempt'
call Emit 'failed and automatic growth is currently disabled for the attached'
call Emit 'aggregate. The high extent count increases suspicion of allocation'
call Emit 'pressure, but 118 extents alone does not prove reallocation is'
call Emit 'required. Volume capacity/SMS/allocation evidence must be checked.'
call Emit ''
call Emit 'If obsolete logs are confirmed by the application owner, cleanup'
call Emit 'could provide temporary recovery. The permanent correction depends'
call Emit 'on why the underlying VSAM LDS could not obtain additional space.'

call Section 'RECOMMENDED DEBUGGING / POSSIBLE RESOLUTION'
call Emit '1. Confirm the live mount/owner information.'
call Emit '2. Review zfsadm fsinfo/aggrinfo and LISTCAT allocation evidence.'
call Emit '3. Check the current volume/SMS allocation for available capacity.'
call Emit '4. If suitable old logs exist, review them with the application'
call Emit '   owner and remove/rotate only approved files.'
call Emit '5. If an explicit grow is appropriate after the allocation problem'
call Emit '   is corrected, an administrator can issue the candidate command'
call Emit '   shown in COMMAND REFERENCE.'
call Emit '6. If the aggregate has reached a structural allocation limitation,'
call Emit '   plan a larger/replacement zFS or multi-volume solution according'
call Emit '   to the site storage design.'
call Emit '7. For a critical zFS that cannot be unmounted, coordinate storage'
call Emit '   options rather than forcing an unmount solely to obtain space.'

call Section 'COMMAND REFERENCE'
call Emit 'DISPLAY COMMAND'
call Emit '  D OMVS,F,NAME=OMVS.PROD.APP.ZFS'
call Emit ''
call Emit 'MODIFY COMMAND - INFORMATION DISPLAY ONLY'
call Emit '  F BPXOINIT,FILESYS=D,ALL'
call Emit ''
call Emit 'USS / zFS DIAGNOSTIC COMMANDS'
call Emit '  df -kv /var/app'
call Emit '  zfsadm fsinfo -aggregate OMVS.PROD.APP.ZFS'
call Emit '  zfsadm aggrinfo -aggregate OMVS.PROD.APP.ZFS -long'
call Emit ''
call Emit 'TSO / VSAM DIAGNOSTIC COMMAND'
call Emit "  LISTCAT ENT('OMVS.PROD.APP.ZFS') ALL"
call Emit ''
call Emit 'POSSIBLE RECOVERY COMMAND - EXAMPLE ONLY'
call Emit '  zfsadm grow OMVS.PROD.APP.ZFS 0'
call Emit ''
call Emit 'ZFSDIAG NEVER issues this recovery command automatically. In a real'
call Emit 'incident, use it only after the live report shows that explicit grow'
call Emit 'is appropriate and the failed allocation condition is understood.'

call Section 'RAW COMMAND EVIDENCE'
call Emit 'The following is abbreviated SAMPLE evidence, not command output'
call Emit 'collected from this system.'
call Emit ''
call Emit 'D OMVS,F,NAME=OMVS.PROD.APP.ZFS'
call Emit '  BPXO045I ... FILESYSTEM DISPLAY'
call Emit '  FILESYSTEM NAME = OMVS.PROD.APP.ZFS'
call Emit '  MOUNT POINT     = /var/app'
call Emit '  OWNER           = 'localSys
call Emit ''
call Emit 'zfsadm fsinfo -aggregate OMVS.PROD.APP.ZFS'
call Emit '  Status: GF,GD'
call Emit '  File System Grow: OFF'
call Emit '  ENOSPC Errors: 27'
call Emit ''
call Emit 'zfsadm aggrinfo -aggregate OMVS.PROD.APP.ZFS -long'
call Emit '  SAMPLE: 164000 K free out of total 41943040'
call Emit ''
call Emit "LISTCAT ENT('OMVS.PROD.APP.ZFS') ALL"
call Emit '  SAMPLE: SPACE-SEC=500 EXT-ADDR=YES EXTENDED=YES'
call Emit '  SAMPLE: VOLSER=PRD901 EXTENTS=118'

call Section 'SAFETY / IMPORTANT NOTES'
call Emit '  * THIS IS A SAMPLE REPORT. Do not treat sample values as live data.
'
call Emit '  * ZFSDIAG is read-only by design.'
call Emit '  * It never automatically grows a zFS or changes the VSAM LDS.'
call Emit '  * It never deletes or truncates USS files.'
call Emit '  * 123 extents is a review signal, not an automatic reallocation'
call Emit '    rule; per-volume and SMS allocation must also be considered.'
call Emit '  * FILES mode is intentionally optional because recursive file'
call Emit '    scanning can create noticeable metadata I/O on a very large zFS.'
call Emit '  * Any cleanup/grow/migration/storage action requires normal site'
call Emit '    authority, change controls and application/storage judgement.'

call Emit ''
call Emit copies('=',78)
call Emit ' ZFSDIAG SAMPLE completed. No live diagnostic/change was performed.'
call Emit copies('=',78)
return


/*====================================================================*/
/* HELP / README                                                      */
/*====================================================================*/

HelpReport:
call Emit ''
call Emit copies('=',78)
call Emit ' ZFSDIAG - README / HELP'
call Emit copies('=',78)
call Emit ''
call Emit 'WHAT IS ZFSDIAG?'
call Emit copies('-',78)
call Emit 'ZFSDIAG is a read-only first-response utility for diagnosing'
call Emit 'zFS full, low-space and failed-grow incidents. It correlates'
call Emit 'the mounted-file-system view with the underlying VSAM LDS'
call Emit 'allocation so the operator can see why growth is constrained.'
call Emit ''
call Emit 'WHEN TO USE'
call Emit copies('-',78)
call Emit '  * A ticket reports zFS full / ENOSPC / unable to write.'
call Emit '  * A zfsadm grow attempt failed.'
call Emit '  * You need to know whether extents/allocation are limiting.'
call Emit '  * You suspect the aggregate is near a non-EA size boundary.'
call Emit '  * You need bounded large-file evidence before cleanup.'
call Emit ''
call Emit 'WHAT IT DOES'
call Emit copies('-',78)
call Emit '  1. No target: runs zfsadm fsinfo -exceptions.'
call Emit '  2. Dataset target: starts with D OMVS,F,NAME=<zfsname>.'
call Emit '  3. Path target: resolves the containing aggregate with df and'
call Emit '     zfsadm fsinfo -path.'
call Emit '  4. Runs zfsadm fsinfo for status/grow/ENOSPC evidence.'
call Emit '  5. Runs zfsadm aggrinfo -long for capacity information.'
call Emit '  6. Runs TSO LISTCAT ALL against the underlying VSAM LDS.'
call Emit '  7. Correlates EA, primary/secondary, HI-A/HI-U, volumes and'
call Emit '     observed extent information.'
call Emit '  8. Optionally scans larger files when FILES is requested.'
call Emit ''
call Emit 'SYNTAX'
call Emit copies('-',78)
call Emit '  TSO ZFSDIAG'
call Emit '      Lightweight system-wide zFS exception scan.'
call Emit ''
call Emit '  TSO ZFSDIAG OMVS.APP.ZFS'
call Emit '      Diagnose a known zFS aggregate data set.'
call Emit ''
call Emit '  TSO ZFSDIAG /var/app'
call Emit '      Diagnose the zFS containing a USS path.'
call Emit ''
call Emit '  TSO ZFSDIAG DSN OMVS.APP.ZFS'
call Emit '  TSO ZFSDIAG PATH /var/app'
call Emit '      Explicit forms of the same targeted modes.'
call Emit ''
call Emit '  TSO ZFSDIAG OMVS.APP.ZFS FILES'
call Emit '  TSO ZFSDIAG /var/app FILES'
call Emit '      Add a bounded recursive large-file scan.'
call Emit ''
call Emit '  TSO ZFSDIAG HELP'
call Emit '      Open this README / HELP report.'
call Emit ''
call Emit '  TSO ZFSDIAG SAMPLE'
call Emit '      Generate a demonstration report using fabricated zFS data.'
call Emit '      Do NOT pass a made-up path or data set name to normal mode;'
call Emit '      normal targeted mode always queries the live system.'
call Emit '      System/user/date-time and the documented command syntax are'
call Emit '      real; sample incident values do not come from the system.'
call Emit ''
call Emit 'OUTPUT DATA SETS'
call Emit copies('-',78)
call Emit 'Diagnostic report:'
call Emit '  'reportDsn
call Emit ''
call Emit 'HELP / README:'
call Emit '  'helpDsn
call Emit ''
call Emit 'SAMPLE report:'
call Emit '  'sampleDsn
call Emit ''
call Emit 'All three data sets remain cataloged after ISPF Browse ends.'
call Emit 'A later diagnostic run replaces only ZFSDIAG.RPT.'
call Emit 'HELP replaces only ZFSDIAG.HELP.'
call Emit 'SAMPLE replaces only ZFSDIAG.SAMPLE and never overwrites the'
call Emit 'latest real diagnostic report.'
call Emit ''
call Emit 'STANDARD REPORT STRUCTURE'
call Emit copies('-',78)
call Emit 'Live targeted and SAMPLE reports use the same core sequence:'
call Emit '  1. REPORT INFORMATION'
call Emit '  2. TRIAGE SUMMARY'
call Emit '  3. TARGET / MOUNT INFORMATION'
call Emit '  4. zFS CAPACITY / GROW STATE'
call Emit '  5. VSAM LDS / ALLOCATION OBSERVATIONS'
call Emit '  6. OPTIONAL LARGE-FILE SCAN'
call Emit '  7. ANALYSIS / INTERPRETATION'
call Emit '  8. RECOMMENDED DEBUGGING / POSSIBLE RESOLUTION'
call Emit '  9. COMMAND REFERENCE'
call Emit ' 10. RAW COMMAND EVIDENCE'
call Emit ' 11. SAFETY / IMPORTANT NOTES'
call Emit ''
call Emit 'SAFETY / RESOURCE IMPACT'
call Emit copies('-',78)
call Emit 'Normal ZFSDIAG mode performs metadata/query operations and is'
call Emit 'intended to be low impact. It never grows or changes the zFS.'
call Emit ''
call Emit 'FILES is deliberately optional. It recursively walks one mounted'
call Emit 'file system (-xdev) looking for larger regular files. On a very'
call Emit 'large zFS this can create noticeable metadata I/O and CPU, so use'
call Emit 'FILES only when content analysis is actually required.'
call Emit ''
call Emit 'ZFSDIAG NEVER AUTOMATES'
call Emit copies('-',78)
call Emit '  * zfsadm grow'
call Emit '  * rm / delete / truncate / log rotation'
call Emit '  * IDCAMS ALTER / ADDVOLUMES'
call Emit '  * mount / unmount / remount'
call Emit '  * volume-capacity or storage-subsystem changes'
call Emit ''
call Emit 'LIMITATIONS'
call Emit copies('-',78)
call Emit '  * D OMVS,F,NAME= requires suitable CONSOLE/OPERCMDS authority.'
call Emit '  * ZFSDIAG supplies PATH/LIBPATH to BPXWUNIX explicitly.'
call Emit '  * USS commands still require a usable OMVS environment/access.'
call Emit '  * Some ISPF command fields uppercase path operands. ZFSDIAG'
call Emit '    automatically resolves path components against real USS names'
call Emit '    when there is exactly one case-insensitive match.'
call Emit '  * If two entries differ only by case, ZFSDIAG will not guess;'
call Emit '    use a mixed-case command field such as ISPF option 6.'
call Emit '  * zFS/DFSMS output can differ by z/OS level; raw command output'
call Emit '    is kept in the report so parser conclusions can be verified.'
call Emit '  * ZFSDIAG cannot prove that an old/large file is safe to delete.'
call Emit '  * It cannot determine hardware-team volume expansion policy.'
call Emit '  * High extent count is not by itself proof that reallocation is'
call Emit '    mandatory; volume/SMS distribution must also be reviewed.'
call Emit ''
call Emit copies('=',78)
return


/*====================================================================*/
/* REPORT PRESENTATION                                                */
/*====================================================================*/

PresentReport:
/* ZDIAG BATCH BYPASS */
if sysvar('SYSISPF') <> 'ACTIVE' then do
  if rptCount = 0 then return
  address TSO 'EXECIO 'rptCount' DISKW ZREPORT (STEM rpt. FINIS)'
  zdbw = rc
  if zdbw <> 0 then do
    say 'ZDIAG BATCH: ZREPORT write failed RC='zdbw
    exit 20
  end
  return
end
if rptCount = 0 then return

if mode = 'HELP' then prDsn = helpDsn
else if mode = 'SAMPLE' then prDsn = sampleDsn
else prDsn = reportDsn

prDdn = 'ZFRPT001'

/* Replace the prior copy at the start of a new run. The newly written */
/* report is intentionally retained after ISPF Browse ends. */
prTrap = outtrap('zfQuiet.')
Address TSO "DELETE '"prDsn"'"
prTrap = outtrap('OFF')

prAlloc = "ALLOC DA('"prDsn"') F("prDdn") NEW CATALOG"
prAlloc = prAlloc ' TRACKS SPACE(10 5) DSORG(PS)'
prAlloc = prAlloc ' RECFM(F B) LRECL(133) BLKSIZE(0)'

Address TSO prAlloc
prArc = rc

if prArc <> 0 then do
  say 'ZFSDIAG: Unable to allocate output data set:'
  say "  '"prDsn"'"
  do prI = 1 to rptCount
    say rpt.prI
  end
  return
end

Address TSO 'EXECIO 'rptCount' DISKW 'prDdn' (STEM rpt. FINIS)'
prWrc = rc
Address TSO 'FREE F('prDdn')'

if prWrc <> 0 then do
  say 'ZFSDIAG: Unable to write output data set:'
  say "  '"prDsn"'"
  do prI = 1 to rptCount
    say rpt.prI
  end
  return
end

if ispfActive = 0 then do
  say "ZFSDIAG output saved in '"prDsn"'"
  do prI = 1 to rptCount
    say rpt.prI
  end
  return
end

Address ISPEXEC 'CONTROL ERRORS RETURN'
Address ISPEXEC "BROWSE DATASET('"prDsn"')"
prBrc = rc
Address ISPEXEC 'CONTROL ERRORS CANCEL'

if prBrc > 4 then do
  say 'ZFSDIAG: ISPF Browse failed RC='prBrc
  say "ZFSDIAG output remains saved in '"prDsn"'"
  do prI = 1 to rptCount
    say rpt.prI
  end
end
return


/*====================================================================*/
/* HELPERS                                                            */
/*====================================================================*/

Section:
parse arg scTitle
call Emit ''
call Emit copies('-',78)
call Emit ' 'scTitle
call Emit copies('-',78)
return


KV:
parse arg kvLabel, kvValue
call Emit ' 'left(kvLabel,31)':' kvValue
return


Emit:
parse arg emText
if length(emText) > 132 then emText = left(emText,132)
rptCount = rptCount + 1
rpt.rptCount = emText
rpt.0 = rptCount
return


EmitStem:
parse arg esName, esMax
if esMax = '' then esMax = 40

interpret 'esCount = 'esName'0'

if esCount <= 0 then do
  call Emit '   <no output>'
  return
end

esShow = esCount
if esShow > esMax then esShow = esMax

do esI = 1 to esShow
  interpret 'esLine = 'esName||esI
  call Emit '   'esLine
end

if esCount > esShow then do
  esOmit = esCount - esShow
  call Emit '   ... 'esOmit' additional lines omitted ...'
end
return


TokenAfter:
procedure
parse arg taLine, taLabel
taU = translate(taLine)
taLab = translate(taLabel)
taP = pos(taLab,taU)
if taP = 0 then return ''
taRest = strip(substr(taLine,taP+length(taLabel)))
if taRest = '' then return ''
return word(taRest,1)


FirstAfter:
procedure
parse arg faLine, faLabel
faU = translate(faLine)
faLab = translate(faLabel)
faP = pos(faLab,faU)
if faP = 0 then return ''
faRest = strip(substr(faLine,faP+length(faLabel)))
if faRest = '' then return ''
return word(faRest,1)


CatValue:
procedure
parse arg cvLine, cvLabel
cvU = translate(cvLine)
cvLab = translate(cvLabel)
cvP = pos(cvLab,cvU)
if cvP = 0 then return ''

cvRest = substr(cvLine,cvP+length(cvLabel))

do while length(cvRest) > 0
  cvC = left(cvRest,1)
  if pos(cvC,'-=: ') = 0 then leave
  cvRest = substr(cvRest,2)
end

cvRest = strip(cvRest)
if cvRest = '' then return ''
return word(cvRest,1)


StripNumber:
procedure
parse arg sn
sn = strip(sn)
sn = space(translate(sn,' ',','),0)
do while length(sn) > 0
  c = right(sn,1)
  if datatype(c,'N') | c = '.' then leave
  sn = left(sn,length(sn)-1)
end
return sn


SizeToK:
procedure
parse arg sk
sk = strip(translate(sk))
sk = space(translate(sk,' ',','),0)
if sk = '' then return -1

unit = right(sk,1)
if datatype(unit,'N') | unit = '.' then do
  num = sk
  unit = 'K'
end
else num = left(sk,length(sk)-1)

if datatype(num,'N') = 0 then return -1

select
  when unit = 'K' then return num
  when unit = 'M' then return num * 1024
  when unit = 'G' then return num * 1048576
  when unit = 'T' then return num * 1073741824
  otherwise return -1
end


StatusFlag:
procedure
parse arg sfStatus, sfFlag
sfStatus = translate(space(sfStatus,0))
sfFlag = translate(sfFlag)
if sfStatus = '' then return 0
sfPadded = ','sfStatus','
if pos(','sfFlag',',sfPadded) > 0 then return 1
return 0


HumanK:
procedure
parse arg hk
if datatype(hk,'N') = 0 | hk < 0 then return 'N/A'

if hk >= 1073741824 then
  return strip(format(hk/1073741824,8,1))' TB'
if hk >= 1048576 then
  return strip(format(hk/1048576,8,1))' GB'
if hk >= 1024 then
  return strip(format(hk/1024,8,1))' MB'
return strip(format(hk,12,0))' KB'


FormatPct:
procedure
parse arg fp
if datatype(fp,'N') = 0 | fp < 0 then return 'N/A'
return strip(format(fp,5,1))' %'


NumOrNA:
procedure
parse arg nn
if datatype(nn,'N') = 0 | nn < 0 then return 'N/A'
return nn


ValueOr:
procedure
parse arg voVal, voDefault
voVal = strip(voVal)
if voVal = '' then return voDefault
return voVal


YesNo:
procedure
parse arg yn
if yn then return 'YES'
return 'NO'


RcText:
procedure
parse arg rr
if rr = -1 then return 'NOT RUN'
return rr


DisplayDate:
procedure
d = date('S')
yyyy = left(d,4)
mm = substr(d,5,2)
dd = right(d,2)
mons = 'JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC'
mon = word(mons,mm+0)
return dd || '-' || mon || '-' || yyyy


FatalSyntax:
say
say 'ZFSDIAG: REXX syntax/runtime error.'
say 'Condition:' condition('C')
say 'Description:' condition('D')
say 'Source line:' sigl
if datatype(sigl,'W') then say 'Statement:' sourceline(sigl)
exit 20
