https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=f1ccc9033618d24da93cd10529faea5a7eaa20be commit f1ccc9033618d24da93cd10529faea5a7eaa20be Author: Jon Turney <[email protected]> Date: Mon Apr 22 19:14:18 2024 +0100 Ignore sighup when daemonized It seems like systemd sends this as well as SIGTERM, so make sure to ignore it so we shut down cleanly. https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=3a18bba53957302246fc44c6dbbbd26520ed2080 commit 3a18bba53957302246fc44c6dbbbd26520ed2080 Author: Jon Turney <[email protected]> Date: Sat Feb 17 19:26:03 2024 +0000 Don't update setup.ini if we can't sign it Check with gpg-agent if the signing key(s) are available, and don't update setup.ini if we can't sign it. Also, since we want to do some logging about keygrips before we daemonize, move logging_setup earlier, and don't close the file descriptors it opens when we darmonize. Diff: --- calm/calm.py | 74 ++++++++++++++++++++++++++++++++++++++++++------ calm/common_constants.py | 5 ++++ calm/utils.py | 2 ++ 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/calm/calm.py b/calm/calm.py index ac662a2..02e613b 100755 --- a/calm/calm.py +++ b/calm/calm.py @@ -549,10 +549,39 @@ def do_main(args, state): return 0 +# +# verify signing key(s) are available in gpg-agent +# +def is_passphrase_cached(args): + passphrase_cached = set() + + for k in args.keygrips: + logging.debug('Querying gpg-agent on keygrip %s' % (k)) + key_details = utils.system("/usr/bin/gpg-connect-agent 'keyinfo %s' /bye" % k) + for l in key_details.splitlines(): + if l.startswith('S'): + # check for either PROTECTION='P' and CACHED='1' (passphrase is + # cached) or PROTECTION='C' (no passphrase) + keyinfo = l.split() + if keyinfo[2] == k: + if ((keyinfo[7] == 'P' and keyinfo[6] == '1') or + keyinfo[7] == 'C'): + passphrase_cached.add(k) + else: + logging.error("Signing key not available") + # Provide some help on the necessary runes: start agent + # with --allow-preset-passphrase so that the passphrase + # preloaded with gpg-preset-passphrase doesn't expire. + logging.error("Load it with '/usr/libexec/gpg-preset-passphrase --preset %s' then provide passphrase" % k) + break + + # return True if all keys are accessible + return passphrase_cached == set(args.keygrips) + + # # # - def do_output(args, state): # update packages listings # XXX: perhaps we need a --[no]listing command line option to disable this from being run? @@ -598,7 +627,13 @@ def do_output(args, state): changed = True # then update setup.ini - if changed: + if not changed: + logging.debug("removing %s, unchanged %s" % (tmpfile.name, inifile)) + os.remove(tmpfile.name) + elif not is_passphrase_cached(args): + logging.debug("removing %s, cannot sign" % (tmpfile.name)) + os.remove(tmpfile.name) + else: update_json = True if args.dryrun: @@ -633,10 +668,6 @@ def do_output(args, state): keys = ' '.join(['-u' + k for k in args.keys]) utils.system('/usr/bin/gpg ' + keys + ' --batch --yes -b ' + extfile) - else: - logging.debug("removing %s, unchanged %s" % (tmpfile.name, inifile)) - os.remove(tmpfile.name) - # write packages.json jsonfile = os.path.join(args.htdocs, 'packages.json.xz') if update_json or not os.path.exists(jsonfile): @@ -681,9 +712,19 @@ def do_daemon(args, state): logging.getLogger('inotify.adapters').propagate = False + def getLogFileDescriptors(logger): + """Get a list of fds from logger""" + handles = [] + for handler in logger.handlers: + handles.append(handler.stream.fileno()) + if logger.parent: + handles += getLogFileDescriptors(logger.parent) + return handles + context = daemon.DaemonContext( stdout=sys.stdout, stderr=sys.stderr, + files_preserve=getLogFileDescriptors(logging.getLogger()), umask=0o002, pidfile=lockfile.pidlockfile.PIDLockFile(args.daemon)) @@ -699,12 +740,15 @@ def do_daemon(args, state): running = False raise InterruptedError + def sighup(signum, frame): + logging.debug("SIGHUP") + context.signal_map = { signal.SIGTERM: sigterm, + signal.SIGHUP: sighup, } with context: - logging_setup(args) logging.info("calm daemon started, pid %d" % (os.getpid())) irk.irk("calm daemon started") @@ -900,6 +944,7 @@ def main(): setupdir_default = common_constants.HTDOCS vault_default = common_constants.VAULT logdir_default = '/sourceware/cygwin-staging/logs' + key_default = [common_constants.DEFAULT_GPG_KEY] parser = argparse.ArgumentParser(description='Upset replacement') parser.add_argument('-d', '--daemon', action='store', nargs='?', const=pidfile_default, help="daemonize (PIDFILE defaults to " + pidfile_default + ")", metavar='PIDFILE') @@ -907,7 +952,7 @@ def main(): parser.add_argument('--force', action='count', help="force regeneration of static htdocs content", default=0) parser.add_argument('--homedir', action='store', metavar='DIR', help="maintainer home directory (default: " + homedir_default + ")", default=homedir_default) parser.add_argument('--htdocs', action='store', metavar='DIR', help="htdocs output directory (default: " + htdocs_default + ")", default=htdocs_default) - parser.add_argument('--key', action='append', metavar='KEYID', help="key to use to sign setup.ini", default=[], dest='keys') + parser.add_argument('--key', action='append', metavar='KEYID', help="key to use to sign setup.ini", default=key_default, dest='keys') parser.add_argument('--logdir', action='store', metavar='DIR', help="log directory (default: '" + logdir_default + "')", default=logdir_default) parser.add_argument('--trustedmaint', action='store', metavar='NAMES', help="trusted package maintainers (default: '" + trustedmaint_default + "')", default=trustedmaint_default) parser.add_argument('--pkglist', action='store', metavar='FILE', help="package maintainer list (default: " + pkglist_default + ")", default=pkglist_default) @@ -930,6 +975,18 @@ def main(): if args.reports is None: args.reports = args.daemon + logging_setup(args) + + # find matching keygrips for keys + args.keygrips = [] + for k in args.keys: + details = utils.system('gpg2 --list-keys --with-keygrip --with-colons %s' % k) + for l in details.splitlines(): + if l.startswith('grp'): + grip = l.split(':')[9] + args.keygrips.append(grip) + logging.debug('key ID %s has keygrip %s' % (k, grip)) + state = CalmState() state.args = args @@ -944,7 +1001,6 @@ def main(): if args.daemon: do_daemon(args, state) else: - logging_setup(args) status = do_main(args, state) return status diff --git a/calm/common_constants.py b/calm/common_constants.py index 29b719b..82d7801 100644 --- a/calm/common_constants.py +++ b/calm/common_constants.py @@ -82,11 +82,16 @@ DEFAULT_KEEP_COUNT = 3 DEFAULT_KEEP_COUNT_TEST = 2 DEFAULT_KEEP_DAYS = 0 +# getting gpg to accurately tell you the default key is apparently impossible, +# so hardcode it here +DEFAULT_GPG_KEY = '56405CF6FCC81574682A5D561A698DE9E2E56300' + # different values to be used when we are not running on sourceware.org, but my # test system... if os.uname()[1] == 'tambora': EMAILS = 'debug' ALWAYS_BCC = '' + DEFAULT_GPG_KEY = '29E138393680DBA0' # package compressions PACKAGE_COMPRESSIONS = ['bz2', 'gz', 'lzma', 'xz', 'zst'] diff --git a/calm/utils.py b/calm/utils.py index 26e3655..6fb93b9 100644 --- a/calm/utils.py +++ b/calm/utils.py @@ -124,9 +124,11 @@ def system(args): for l in e.output.decode().splitlines(): logging.warning(l) logging.warning('%s exited %d' % (args.split()[0], e.returncode)) + return e.output.decode() else: for l in output.decode().splitlines(): logging.info(l) + return output.decode() #
[calm - Cygwin server-side packaging maintenance script] branch master, updated. 20230209-98-gf1ccc90
Jon Turney via Cygwin-apps-cvs Tue, 23 Apr 2024 08:21:32 -0700
