In <list-11290759@2rosenthals.com>, on 12/02/24
at 08:59 PM, "Massimo S." <ecs-isp@2rosenthals.com> said:
Hi massimo,
>should be possible to add the detection of this situation (here below) in
>the hook rexx script?
You can't fix this inside the hook script. What you need is a wrapper
script that retries the failing uacme issue request.
I've appended a copy of uacme-renew.cmd that Dan and I use. It uses my
uacme-hook.cmd, so you will not be able to the script without adapting it
to work with your hook scripts.
>I guess this is some sort of random outage or overload of LE services
You are using the internet. Failures happen and your tools need to be
able handle the known failure modes.
For the curious, Dan has some dual homed domains. For example
This is good for reliability, but complicates cert renewals because one
cannot predict which server will be asked to return the token file.
uacme-renew.cmd knows which domains are dual homed and knows how to push
the token files to all the relevant servers.
uacme-renew.cmd does not currently push the updated certs to the other
servers. This is handled by a simple rsync scrpt.
FIXME to explain how to swap staging and production private keys
FIXME to verify using staging key if staging requested
Copyright (c) 2024 Steven Levine and Associates, Inc.
All rights reserved.
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
2024-11-05 SHL Baseline
2024-11-07 SHL Drop excess code
2024-11-14 SHL Show certificate dates
2024-11-19 SHL Support multiple domains per certificate
*/
signal on Error
signal on Failure name Error
signal on Halt
signal on NotReady name Error
signal on NoValue name Error
signal on Syntax name Error
/* Support # and ; comments and blank lines and missing EOL */
gDomainsLists.0 = 0
drop gErrCondition
do while lines(gDomainListsFileName)
call on NotReady name CatchError /* Avoid death on missing NL */
line = linein(gDomainListsFileName)
signal on NotReady name Error
line = strip(line)
if line == '' then iterate
ch = left(line, 1)
if ch == '#' | ch == ';' then iterate
/* Verify cert accessible */
certdir = MakePath(GetEnv('UNIXROOT'), '\etc\ssl\uacme', domain)
if \ IsDir(certdir) then
call Die 'Cannot access' certdir 'certificate directory for' domain 'domain'
certfile = MakePath(certdir, 'cert.pem')
if \ IsFile(certfile) then
call Die 'Cannot access' certfile 'certificate file for' domain 'domain'
cmd = 'openssl x509 -in' certfile '-subject -dates -noout'
say
say 'Checking certificate dates'
cmd
if \ gBatch then do
call AskYNQ 'Try to renew' domain
if RESULT >= 2 then exit
if RESULT \= 0 then return
end
call LogWriteVTSC 'Trying to renew' domain 'domain'
gRenewalsRequested = gRenewalsRequested + 1
retries = 2 /* FIXME to be more maybe */
/* uacme return codes
0 - Success
1 - Certificate not reissued because it is still current
2 - Failure (syntax or usage error; configuration error; processing failure; unexpected error)
*/
do retry = 1 to retries
/* FIXME to capture output for log */
if gStaging then
cmd = 'uacme --staging --verbose --hook uacme-hook.cmd issue' domainlist
else
cmd = 'uacme --verbose --hook uacme-hook.cmd issue' domainlist
say
err = RunExtCmdNoDie(cmd)
select
when err = 0 then do
call LogWriteVTSC domain 'renew request succeeded on' retry 'of' retries
gCertsRenewed = gCertsRenewed + 1
leave
end
when err = 1 then do
call LogWriteVTSC 'Renewal request bypessed because certificate is current on retry' retry 'of' retries
leave
end
when err = 2 then do
call LogWriteVTSC 'Renewal request failed with error' err 'on retry' retry 'of' retries
if retry < retries then
call SysSleep 5
end
otherwise
call LogWriteVTSC 'Renewal request failed with unexpected error' err 'on retry' retry 'of' retries
gRenewalsFailed = gRenewalsFailed + 1
leave
end
select
when curSw == 'b' then
gBatch = 1
when curSw == 'd' then
gDbgLvl = gDbgLvl + 1
when curSw == 'h' | curSw == '?' then
call ScanArgsHelp
when curSw == 'n' then
gDryRun = 1
when curSw == 's' then
gStaging = 1
when curSw == 'v' then
gVerbose = gVerbose + 1
when curSw == 'V' then do
say
say gCmdName gVersion
exit
end
otherwise
call ScanArgsUsage 'Switch "-' || curSw || '" unexpected'
end /* select */
if gDomainListsFileName \= '' then
call ScanArgsUsage 'Domains file name already set to' gDomainListsFileName
if \ IsFile(curArg) then
call ScanArgsUsage 'Cannot access ' curArg 'domains file'
gDomainListsFileName = curArg
return
/* end ScanArgsArg */
/*=== ScanArgsTerm() ScanArgs scan end exit routine ===*/
ScanArgsTerm: procedure expose (Globals)
if gDomainListsFileName = '' then do
gDomainListsFileName = MakePath(gCmdName, '.domains')
if \ IsFile(gDomainListsFileName) then
call ScanArgsUsage 'Cannot access' gDomainListsFileName 'default domains file'
end
return
/* end ScanArgsTerm */
/*=== ScanArgsHelp() Display ScanArgs usage help exit routine ===*/
ScanArgsHelp:
say
say 'uacme certificate renew wrapper.'
say
say 'Usage:' gCmdName '[-b] [-d] [-h] [-n] [-s] [-v] [-V] [-?] domains-file'
say
say ' -b Enable batch mode'
say ' -d Enable debug mode, repeat for more verbosity'
say ' -h -? Display this message'
say ' -n Dry run - no files changed'
say ' -s Use staging account'
say ' -v Enable verbose messages, repeat for more verbosity'
say ' -V Display version number and quit'
say
say ' domains-file Domain list file, default is' gCmdName || '.domains'
exit 255
/* Take focus with 4OS2 or fail if cannot match window title */
/* If script designed for CMD too, use nofocus arg to avoid error noise */
signal off Error
/* Map 1st left bracket to wild card - [pid] seems to confuse activate */
if nofocus = '' | nofocus \= 1 then
'@if defined _WINTITLE activate "%@replace[[,*,%_WINTITLE]"'
signal on Error
/* Skip line unless suppressed by noskip arg - any non-zero value requests noskip */
if noskip = '' | noskip = 0 then
call lineout 'STDERR', ''
if msg == '' then
msg = 'Continue'
call charout 'STDERR', msg '(y/n/q) ? '
do forever
key = translate(SysGetKey('NOECHO'))
if key == 'Y' | key == 'N' then do
call lineout 'STDERR', key
if key == 'Y' then
ynq = 0
else
ynq = 1
leave
end
if key == 'Q' | c2x(key) == '1B' then do
call lineout 'STDERR', ''
ynq = 2
leave
end
end /* forever */
return ynq
/* end AskYNQ */
/*=== DieVTS(message) Write message to STDOUT and timestamped message to log file and die ===*/
Die:
/* Requires LogWriteVTS and dependents */
/* Requires LogWriteVTSC and dependents */
parse arg msg
callerSIGL = SIGL
/* Use say to avoid NotReady in case running detached - FIXME to be sure not ok to write to STDERR */
say
call LogWriteVTS msg
msg = gCmdName 'aborting at line' callerSIGL || '.'
call LogWriteVTSC msg
call beep 200, 300
exit 254
/* end DieVTS */
/*=== GetEnv(var) Return value for environment variable or empty string ===*/
GetEnv: procedure expose (Globals)
parse arg var
if var = '' then
call Die 'GetEnv requires an argument'
return value(var,, gEnv)
/* end GetEnv */
/*=== IsDir(dirName[, full]) return true if directory is valid, accessible directory ===*/
IsDir: procedure
/* If arg(2) not omitted, return full directory name or empty string */
parse arg dir, full
newdir = ''
do 1
if dir == '' then do
cwd = '' /* No restore needed */
leave
end
dir = translate(dir, '\', '/') /* Convert to OS/2 slashes */
s = strip(dir, 'T', '\') /* Chop trailing slashes unless root */
if s \== '' & right(s, 1) \== ":" then
dir = s /* Chop */
drv = filespec('D', dir)
cwd = directory() /* Remember */
/* If have drive letter and requested directory on some other drive */
if drv \== '' & translate(drv) \== translate(left(cwd, 2)) then do
/* Avoid slow failures and unwanted directory changes */
drvs = SysDriveMap('A:')
if pos(translate(drv), drvs) = 0 then
leave /* Unknown drive */
if SysDriveInfo(drv) == '' then
leave /* Drive not ready */
cwd2 = directory(drv) /* Remember current directory on other drive */
newdir = directory(dir) /* Try to change and get full path name */
call directory cwd2 /* Restore current directory on other drive */
leave
end
/* If no drive letter or same drive and not UNC name */
if left(dir, 2) \== '\\' then do
newdir = directory(dir) /* Try to change and get full path name */
leave
end
/* UNC name - hopefully server is accessible or this will be slow
Accept
\\server
\\server\
\\server\dir\
\\server\dir
*/
cwd = '' /* No restore needed */
wc = dir
if right(wc, 1) \== '\' then
wc = wc || '\'
i = lastpos('\', wc)
if substr(wc, 3, 1) == '\' then
leave /* Malformed UNC - no server name */
if pos('*', wc) > 0 | pos('?', wc) > 0 then
leave /* No wildcards allowed */
call SysFileTree wc, 'files', 'O'
if files.0 > 0 then do
s = files.1 /* Exists and is not empty */
i = lastpos('\', s)
newdir = left(s, i - 1) /* Extract directory name from full path name */
leave
end
/* Try wildcarded directory name */
wc = strip(wc, 'T', '\')
i = lastpos('\', wc)
base = substr(wc, i + 1)
if base == '' then
leave /* Should have matched above */
wc = substr(wc, 1, i) || '*' || base || '*'
call SysFileTree wc, 'files', 'DO'
do fileNum = 1 to files.0
/* Find directory name is list */
s = files.fileNum
i = lastpos('\', s)
s2 = substr(s, i + 1)
if translate(base) == translate(s2) then do
newdir = left(s, i - 1)
leave
end
end /* i */
end /* 1 */
if cwd \== '' then
call directory cwd /* Restore original directory and drive */
if full \== '' then
ret = newdir /* Return full directory name or empty string */
else
ret = newdir \== '' /* Return true if valid and accessible */
return ret
/* end IsDir */
/*=== IsFile(file) return true if arg is file and file exists ===*/
IsFile: procedure expose (Globals)
parse arg file
if file == '' then
yes = 0
else do
/* '.' and '..' returns files in '.' or '..' - so avoid false positives */
call SysFileTree file, 'files', 'F'
if RESULT \= 0 then
call Die 'SysFileTree' file 'failed'
/* Assume caller knows if arg contains wildcards */
yes = file \== '.' & file \== '..' & files.0 \= 0
end
return yes
/* end IsFile */
/*=== LogOpen() Open log file for append ===*/
LogOpen: procedure expose (Globals)
/* Requires LogSetName unless gLogFile defined */
/* Sets gLogFile if not defined */
/* Overrides gLogFile if open fails */
if symbol('gLogFile') \== 'VAR' then
call LogSetName
/* Assume closed */
call stream gLogFile, 'C', 'OPEN WRITE'
if stream(gLogFile) \== 'READY' then do
gLogFile = '\' || gCmdName || '.log' /* Try root */
call stream gLogFile, 'C', 'OPEN WRITE'
end
return
/* end LogOpen */
/*=== LogSetName() Set log file name ===*/
/**
* Sets gLogFile if not defined
* Sets gLogDir if not defined
*/
LogSetName: procedure expose (Globals)
/* Requires LogSetDir unless gLogDir defined */
/* Requires gCmdName */
if symbol('gLogFile') \== 'VAR' then do
if symbol('gLogDir') \== 'VAR' then
call LogSetDir
/* Ensure trailing backslash unless using current directory */
dir = gLogDir
if dir \== '' & right(dir, 1) \== ':' & right(dir, 1) \== '\' then
dir = dir || '\' /* Ensure trailing backslash */
gLogFile = dir || gCmdName'.log'
end
return
/* end LogSetName */
/*=== LogSetDir() Set gLogDir and provide trailing backslash if needed ===*/
/**
* Set gLogDir if gLogDir not defined
* Tries %LOGFILES gTmpDir %TMP
* Falls back to current directory and returns null string
*/
LogSetDir: procedure expose (Globals)
if symbol('gLogDir') \== 'VAR' then do
/* Try gLogDir %LOGFILES gTmpDir %TMP */
do 1
/* Try %LOGFILES */
gLogDir = value('LOGFILES',, gEnv)
if gLogDir \== '' then leave
/* Try gTmpDir */
if symbol('gTmpDir') == 'VAR' then do
gLogDir = gTmpDir
leave
end
/* Try %TMP - return empty string if TMP not defined */
gLogDir = value('TMP',, gEnv)
end
end
return
/* end LogSetDir */
/*=== LogWriteVTS(message,...) Write multi-line message to STDOUT and timestamped message to log file ===*/
LogWriteVTS: procedure expose (Globals)
/* Requires LogOpen */
/* Requires MakeTimestamp */
if symbol('gLogFile') \== 'VAR' then
call LogOpen
do i = 1 to arg()
say arg(i)
call lineout gLogFile, MakeTimestamp() arg(i)
if symbol('gLogWrites') == 'VAR' then
gLogWrites = gLogWrites + 1
end
return
/* end LogWriteVTS */
/*=== LogWriteVTSC(message,...) Write multi-line message to STDOUT and timestamped message to log file and close log ===*/
LogWriteVTSC: procedure expose (Globals)
/* Requires LogOpen */
/* Requires MakeTimestamp */
if symbol('gLogFile') \== 'VAR' then
call LogOpen
ts = MakeTimestamp()
do i = 1 to arg()
say ts arg(i)
call lineout gLogFile, ts arg(i)
if symbol('gLogWrites') == 'VAR' then
gLogWrites = gLogWrites + 1
end
call stream gLogFile, 'C', 'CLOSE'
return
/* end LogWriteVTSC */
/*=== MakePath(pathparts,...) Make path name from parts ===*/
MakePath: procedure
/* All parts optional - code guesses what caller means.
If last arg begins with a dot and is not .. and does not
contain a slash, it is assumed to be a file extension.
To avoid this behavior, pass empty arg as last arg.
Empty args are ignored.
Automatically converts unix slashes to dos slashes.
If 1st arg is drive letter, it must have trailing colon.
*/
argCnt = arg()
path = ''
do argNum = 1 to argCnt
s = arg(argNum)
s = translate(s, '\', '/') /* Ensure DOS */
if s == '' & argNum = argCnt then
iterate /* Ignore nul last arg */
if argNum = 1 then
path = s
else do
lead = left(s, 1)
tail = right(path, 1)
if tail == ':' & argNum = 2 then
path = path || s /* Append path part to drive spec */
else if lead == '.' & argNum = argCnt & s \== '..' & pos('\', s) = 0 then
path = path || s /* Assume extension unless .. or contains \ */
else if tail == '\' & lead == '\' then
path = path || substr(s, 2) /* Drop extra backslash */
else if path \== '' & tail \== '\' & lead \== '\' then
path = path || '\' || s /* Ensure have backslash */
else
path = path || s
end
end /* for */
return path
/* end MakePath */
/*=== MakeTimestamp() Convert current date/time to sorted, delimited timestamp - yyyy/mm/dd-hh:mm:ss ===*/
/*=== Plural(cnt, units, suffix, suffix1, no) Return formatted cnt and units ===*/
/**
* Designed for strings that will display in interior of line so 0 cnt displays as "no"
* unless overridden
* @param cnt is non-negative quantity
* @param units is unit of measure for quantity 1
* @param suffix is optional suffix for quantities other than 1, defaults to es or s
* @param suffix1 is optional suffix for quantity 1, defaults to empty string
* @param no is optional override for 0 cnt display, defaults to no
* @notes tries to guess right suffix if suffix omitted
*/
Plural: procedure
parse arg cnt, units, suffix, suffix1, no
if cnt = 1 then do
if suffix1 \== '' then
units = units || suffix1
end
else do
/* Not 1 */
select
when suffix \== '' then
units = units || suffix
when right(units, 1) == 's' then
units = units || 'es'
when right(units, 2) == 'ch' then
units = units || 'es'
otherwise
units = units || 's'
end
end
if cnt = 0 then do
if no == '' then
no = 'no' /* Assume count not at start of line */
s = no units
end
else
s = cnt units
return s
/* end Plural */
/*=== RunExtCmdNoDie(cmd, die) Run external command that sets RC and return RC ===*/
RunExtCmdNoDie: procedure expose (Globals) SIGL
/* Requires GetEnv */
/* Return error code on error unless overridden */
parse arg cmd, die
if cmd = '' then
call Die 'Required cmd omitted at' SIGL
if pos('echo off', cmd) = 0 then do
say
'@echo on'
end
signal off Error
cmd
signal on Error
err = RC
'@echo off'
/* if piped cmd of form ( extcmd %+ set E=%_? | rxqueue ) */
if err = 0 & (pos('E=%?', cmd) > 0 | pos('E=%_?', cmd) > 0 )then
err = GetEnv('E') /* Retrieve status */
if err \= 0 then do
/* Return error code unless die requested */
die = die = 1 | die == 'die' & die \== ''
if die then
call Die '', cmd 'failed with error' err
end
return err
return
/* end RunExtCmdNoDie */
/*==========================================================================*/
/*=== SkelRexx standards - Delete unused - Move modified above this mark ===*/
/*==========================================================================*/
/*=== Error() Set gErrCondition; report to STDOUT; trace and exit or return if called ===*/
Error:
say
parse source . . cmd
gErrCondition = condition('C')
say gErrCondition 'signaled at line' SIGL 'of' cmd || '.'
if condition('D') \== '' then
say 'REXX reason =' condition('D') || '.'
if gErrCondition == 'SYNTAX' & symbol('RC') == 'VAR' then
say 'REXX error =' RC '-' errortext(RC) || '.'
else if symbol('RC') == 'VAR' then
say 'RC =' RC || '.'
say 'Source =' sourceline(SIGL)
if condition('I') \== 'CALL' | gErrCondition == 'NOVALUE' | gErrCondition == 'SYNTAX' then do
trace '?A'
say 'Enter REXX commands to debug failure. Press enter to exit script.'
nop
if symbol('RC') \== 'VAR' then
RC = 255
exit RC
end
return
/* end Error */
/*=== Halt() Report HALT condition to STDOUT and exit ===*/
Halt:
say
parse source . . cmd
say condition('C') 'signaled at' cmd 'line' SIGL || '.'
say 'Source =' sourceline(SIGL)
say 'Sleeping for 2 seconds...'
call SysSleep 2
exit 253
LoadRexxUtil:
if RxFuncQuery('SysLoadFuncs') then do
call RxFuncAdd 'SysLoadFuncs', 'REXXUTIL', 'SysLoadFuncs'
if RESULT then
call Die 'Cannot load SysLoadFuncs.'
call SysLoadFuncs
end
return
/* end LoadRexxUtil */
/*=== ScanArgs(cmdLine) Scan command line ===*/
ScanArgs: procedure expose (Globals)
/* Calls user exits to process arguments and switches */
parse arg cmdTail
cmdTail = strip(cmdTail)
call ScanArgsInit
/* Ensure optional settings initialized */
if symbol('SWCTL') \== 'VAR' then
swCtl = '' /* Switches that take args, append ? if optional */
if symbol('KEEPQUOTED') \== 'VAR' then
keepQuoted = 0 /* Set to 1 to keep arguments quoted */
/* Scan */
curArg = '' /* Current arg string */
curSwList = '' /* Current switch list */
/* curSwArg = '' */ /* Current switch argument, if needed */
noMoreSw = 0 /* End of switches */
do while cmdTail \== '' | curArg \== '' | curSwList \== ''
/* If arg buffer empty, refill */
if curArg == '' then do
qChar = left(cmdTail, 1) /* Remember quote */
if \ verify(qChar,'''"', 'M') then
parse var cmdTail curArg cmdTail /* Not quoted */
else do
/* Arg is quoted */
curArg = ''
do forever
/* Parse dropping quotes */
parse var cmdTail (qChar)quotedPart(qChar) cmdTail
curArg = curArg || quotedPart
/* Check for escaped quote within quoted string (i.e. "" or '') */
if left(cmdTail, 1) \== qChar then do
cmdTail = strip(cmdTail) /* Strip leading whitespace */
leave /* Done with this quoted arg */
end
curArg = curArg || qChar /* Append quote */
if keepQuoted then
curArg = curArg || qChar /* Append escaped quote */
parse var cmdTail (qChar) cmdTail /* Strip quote */
end /* do forever */
if keepQuoted then
curArg = qChar || curArg || qChar /* requote */
end /* if quoted */
end /* if curArg empty */
/* If switch buffer empty, refill */
if curSwList == '' & \ noMoreSw then do
if left(curArg, 1) == '-' & curArg \== '-' then do
if curArg == '--' then
noMoreSw = 1
else
curSwList = substr(curArg, 2) /* Remember switch string */
curArg = '' /* Mark empty */
iterate /* Refill arg buffer */
end /* if switch */
end /* if curSwList empty */
/* If switch in progress */
if curSwList \== '' then do
curSw = left(curSwList, 1) /* Next switch */
curSwList = substr(curSwList, 2) /* Drop from pending */
/* Check switch allows argument, avoid matching ? */
if pos(curSw, translate(swCtl,,'?')) \= 0 then do
if curSwList \== '' then do
curSwArg = curSwList /* Use rest of switch string for switch argument */
curSwList = ''
end
else if curArg \== '' & left(curArg, 1) \== '-' then do
curSwArg = curArg /* Arg string is switch argument */
curArg = '' /* Mark arg string empty */
end
else if pos(curSw'?', swCtl) = 0 then
call ScanArgsUsage 'Switch "-' || curSw || '" requires an argument'
else
curSwArg = '' /* Optional arg omitted */
end
call ScanArgsSwitch /* Passing curSw and curSwArg */
drop curSwArg /* Must be used by now */
end /* if switch */
/* If arg */
else if curArg \== '' then do
noMoreSw = 1
call ScanArgsArg /* Passing curArg */
curArg = ''
end
end /* while not done */
call ScanArgsTerm
return
/* end ScanArgs */
/*=== SetCmdName() Set gCmdName to short script name ===*/
SetCmdName: procedure expose (Globals)
parse source . . cmd
cmd = filespec('N', cmd) /* Chop path */
c = lastpos('.', cmd)
if c > 1 then
cmd = left(cmd, c - 1) /* Chop extension */
gCmdName = translate(cmd, xrange('a', 'z'), xrange('A', 'Z')) /* Lowercase */
return
/* end SetCmdName */
/*=== SetTmpDir() Set gTmpDir to %TMP with trailing backslash or empty string ===*/
SetTmpDir: procedure expose (Globals)
s = value('TMP',,gEnv)
if s \== '' & right(s, 1) \== ':' & right(s, 1) \== '\' then
s = s'\' /* Stuff backslash */
gTmpDir = s
return