[Zope-CVS] CVS: CVSROOT - postcommit_actions:1.151
Ken Manheimer
cvs-admin at zope.org
Thu Jun 17 17:43:51 EDT 2004
Update of /cvs-repository/CVSROOT
In directory cvs.zope.org:/tmp/cvs-serv30424
Modified Files:
postcommit_actions
Log Message:
Track different LDAP server IP number - and convert to new
postcommit-actions packaging.
=== CVSROOT/postcommit_actions 1.150 => 1.151 ===
--- CVSROOT/postcommit_actions:1.150 Tue Dec 9 13:53:06 2003
+++ CVSROOT/postcommit_actions Thu Jun 17 17:40:12 2004
@@ -1,621 +1,25 @@
-#!/usr/bin/env python
+#!/bin/env python
-"""Apply checkin actions dictated by traffic_table.py, as checkins occur.
+"""Dispatch checkin notifications and manage symlinks, after checkins happen.
-Special provisions to fork into the background, disconnected from tty, so
-the user and (more critically) CVS locks aren't held just pending completion
-of notification and other potentially meandering actions.
+Meant to be used on 'loginfo' CVS bookkeeping hook, eg:
-Options:
+ DEFAULT ${CVSROOT}/CVSROOT/postcommit_actions %{sVv}
- --verbose - operate in verbose mode, with output to OUTPUT_LOG in the
- detached copy, and to stdout before the detach (and forever if
- --wait specified)
+Notifications are routed according to traffic_table.py in the bookkeeping
+directory, and symlink management is according to checkin of a file named
+'repolinks'. The actual work happens in a separate, disconnected process to
+avoid CVS lock self-contention and also avoid delaying people's checkin
+activities.
- --dryrun - do not actually do any of the activities
+See RepoUtils.postcommit and .adjustlinks for more details (generally
+installed in the python site-packages directory)."""
- --wait - do not fork into the background (and leave output in stdout/stderr)
- This is for internal use by the script when forking.
+from RepoUtils.postcommit import cvs_postcommit
+import sys
- --msgfile MSGFILENM - internal - get log message from MSGFILENM (and delete)
-
-We expect to be invoked from loginfo with %{CVSROOT} %{sVv}: CVS expands
-the loginfo %{sVv} into a single command-line token (bizarrely enough)
-containing multiple, space-delimited parts:
-
- - The first part is the path of repository directory
-
- - Then comes a comma-concatenated string for each file being checked in,
- consisting of:
-
- - The file name,
- - The old version number, then
- - the new one, like so:
-
- filename,1.52,1.53
-
-\(See the CVS loginfo docs for more format-string details.)
-
-The actions are configured in traffic_table.py. The only remaining relevant
-one is:
-
- - email checkin messages to designated recipients
-
-\(Versions of the script that supported automatic mirroring was removed
-beginning at version 1.123.)"""
-
-import sys, os, tempfile, time, pwd
-SCRIPT_DIR = os.path.abspath(os.path.split(sys.argv[0])[0])
-os.chdir(SCRIPT_DIR) # Valid dir, having other local scripts.
-import string
-import getopt
-import smtplib
-import socket, whrandom, getpass
-
-CVSROOT = os.path.split(SCRIPT_DIR)[0]
-CVSROOT_ABS = os.path.abspath('..')
-USE_LDAP = 1
-LDAP_PORT = 389
-LDAP_HOST = "10.0.11.6"
-OFFICIAL_SENDER = "cvs-admin at zope.org"
-
-# Ceiling on number of lines per file report (diff or "added"), at which point
-# we go to excerpts from the beginning and end:
-FILE_LINES_LIMIT = 2000
-# Number of lines in beginning and end excerpts when FILE_LINES_LIMIT is hit:
-FILE_EXCERPTS_LINES = 200
-
-CUSTOM_TRAFFIC_TABLE = "%s/custom_traffic_table.py" % CVSROOT
-
-import re
-
-import traffic_table
-import adjustlinks
-
-# Hook up with an outboard file.
-if os.path.exists(CUSTOM_TRAFFIC_TABLE):
- execfile(CUSTOM_TRAFFIC_TABLE)
-
-# OUTPUT_LOG must be writable by anyone whose checkins will invoke this
-# script in order for their logging to happen.
-OUTPUT_LOG = "%s/cvs-postcommit_actions.log" % CVSROOT
-
-# Set CVSMASTER to the email address of someone managing your CVS mirroring.
-# Notices about any caught malfunctions will be sent to this address.
-CVSMASTER = "cvs-admin at zope.org"
-
-# You have to plug in the recipients email address via '%' string formatting.
-MAIL_CMD = "/usr/lib/sendmail -t -f %s"
-
-WAIT = 0 # Fork unless --wait is requested.
-DRYRUN = 0 # '--dryrun' option
-VERBOSE = 0 # '--verbose' option default value
-SCRIPT = "postcommit_actions" # Will b set to argv[0], when obtained.
-
-sVv_re = re.compile("(.*),([^,]+),([^,]+)")
-
-def main(args):
- """Grok the args and the traffic_table and process accordingly."""
-
- global SCRIPT, DRYRUN, VERBOSE, WAIT
-
- orig_args = args[:]
-
- SCRIPT = args[0]
- try:
- opts, args = getopt.getopt(args[1:], "", ["verbose", "dryrun",
- "wait", "msgfile=",
- ])
- except getopt.error, err:
- complain("%s\n", err)
- raise SystemExit, 1
-
- msgfilenm = ""
-
- for opt, val in opts:
- if opt == "--verbose":
- VERBOSE = 1
- complain("%s: VERBOSE\n", SCRIPT)
- elif opt == "--dryrun":
- DRYRUN = 1
- complain("%s: DRYRUN\n", SCRIPT)
- elif opt == "--wait":
- WAIT = 1
- complain("%s: WAIT/Foreground\n", SCRIPT)
- elif opt == "--msgfile":
- # This is internal, for the script to pass itself a file.
- msgfilenm = val
- else:
- complain("Unrecognized option '%s'\n", opt)
- raise SystemExit, 1
- if VERBOSE:
- complain("Initial args: %s '%s'\n",
- string.join(orig_args[:-1], " "),
- orig_args[-1])
- complain("Working dir: %s\n" % safe_getcwd('/tmp'))
-
- if len(args) != 1:
- usage(); raise SystemExit, 1
-
- doing_directory = 0
- no_files = 1
-
- # The %{sVv} args are passed in as a single token - have to split them.
- split_args = string.split(args[0])
- repo = split_args[0]
-
- if len(split_args) == 0:
- subjs = ["<no files>", "", ""]
- elif len(split_args) == 1:
- subjs = split_args + ["", ""]
- elif split_args[1] == "-":
- subjs = split_args
- doing_directory = 1
- else:
- no_files = 0
- subjs = map(grok_file, split_args[1:])
-
- if VERBOSE:
- complain("CWD: %s, Repo: %s, ",
- safe_getcwd('/tmp'), repo)
- complain("Subjects: %s\n", subjs)
-
- if not WAIT:
- detach(orig_args)
-
- entries = find_entries(repo)
-
- for entry in entries:
- wasVerbose = VERBOSE
- VERBOSE = entry.get('verbose', VERBOSE)
- selector_path = entry['path']
- addrs = entry.get('addrs')
- specials = entry.get('specials')
-
- if addrs:
- do_mail(repo, addrs, subjs, msgfilenm=msgfilenm,
- doing_directory=doing_directory, no_files=no_files)
-
- if specials:
- subj_names = map(lambda x: x[0], subjs)
- for trigger, action in specials:
- if trigger in subj_names:
- do_special(trigger, action, addrs)
-
- VERBOSE = wasVerbose
-
- if VERBOSE:
- complain("** Done **\n\n")
-
- if failures:
- handle_failures(orig_args)
-
- if msgfilenm:
- os.unlink(msgfilenm)
-
-def detach(args):
- """Fork us into the background, with stdout and stderr to OUTPUT_LOG.
-
- We have to disconnect std io (in, out, and err) and run the program in
- the background - which we do using some shell wizardry."""
-
- doctored_args = [args[0]]
- doctored_args.append("--wait")
- tempfile.mktemp() # Throw one away, to get initial template.
- tempfile.tempdir = "/tmp"
-## template = (tempfile.template or
-## "@%s.%d" % (os.getpid(), whrandom.randint(1000000, 9999999)))
-## tempfile.template = "cvs-log" + template
- msgfile = open(tempfile.mktemp(suffix='.cvslog'), 'w')
- msgfile.write(sys.stdin.read())
- msgfile.close()
- msgfilenm = msgfile.name
- doctored_args.append("--msgfile")
- doctored_args.append(msgfilenm)
- for i in args[1:]:
- doctored_args.append('"%s"' % i)
- cmd = (("( exec 1>>%s 2>&1; "
- + ("export CVSROOT=%s; " % CVSROOT)
- + string.join(doctored_args, " ") + " &)")
- % OUTPUT_LOG)
-
- if VERBOSE:
- complain("Re-executing detached in %s, cmd:\n\t%s\n",
- safe_getcwd('/tmp'), cmd)
-
- os.system(cmd)
- loosen_file(OUTPUT_LOG)
-
- os._exit(0)
-
-def find_entries(repo=None):
- """Return dictionary of traffic_table entries qualified by repo regexp.
-
- Iff no entries match, we return the catchall entry - the (last) one with
- path == None."""
-
- entries = []
- catchall = None
- linkmgr = adjustlinks.LinkManager()
- gotaddrs = []
-
- containers = linkmgr.all_containers(repo)
- for it in traffic_table.get_table():
- if it['path'] == None:
- # Retain the catchall entry in case no regular ones qualify.
- catchall = it
- else:
- # Obtain qualifying candidates:
- for candidate in containers:
-
- if (re.match(it['path'], candidate)
- and it.get('addrs') not in gotaddrs):
- entries.append(it)
- gotaddrs.append(it.get('addrs'))
-
- if entries:
- if VERBOSE > 1:
- complain("find_entries: repo: %s, containers: %s\n entries: %s\n",
- repo, containers, entries)
- elif catchall:
- entries.append(catchall)
- if VERBOSE > 1:
- complain("No matches, so using catchall:\n %s\n", entries)
- elif VERBOSE > 1:
- complain("No matches, no catchall - no actions\n")
-
- for e in entries:
- if e.has_key('addrs') and (type(e['addrs']) == type("")):
- # Be lenient - listify string args.
- e['addrs'] = [e['addrs']]
-
- return entries
-
-def do_mail(repo, addrs, subjs,
- msgfilenm, doing_directory=0, no_files=0):
- """Send notice about checkin to addresses dictated by traffic table.
-
- We include a diff."""
- if VERBOSE:
- complain("Notice to %s\n for %s / %s\n", addrs, repo, subjs)
- # The message contents are on stdin, just _yearning_ to be sent...-)
- subject = "CVS: %s " % repo
-
- diff_msg = ''
-
- if doing_directory or no_files:
- subject = subject + string.join(subjs, " ")
- if subjs[2] == 'New':
- new_msg = ('=== Added directory %s ===\n' % repo)
- diff_msg = diff_msg + new_msg + '\n'
- else:
- subject = subject + "-"
- for fn, old, new in subjs:
- subject = subject + " %s:%s" % (fn, new)
- if new == 'NONE':
- new_msg = ('=== Removed File %s/%s ===\n'
- % (repo, fn))
- else:
- try:
- new_msg = "\n" + create_diff(repo, fn, old, new)
- except IOError:
- if DRYRUN:
- text = "[Contrived diff]"
- else:
- raise
-
-
- diff_msg = diff_msg + new_msg + '\n'
-
- try:
- # Prepend the Subject and From lines, and append the diff:
- mf = open(msgfilenm, 'r')
- text = mf.read() + diff_msg
- mf.close()
- except IOError:
- if DRYRUN and WAIT:
- text = "[Contrived content]\n" + diff_msg
- else:
- raise
-
- send_mail(addrs, text, subject)
-
-def create_diff(repo, file, old, new):
- """ Create a diff comparing old and new versions """
- if old == 'NONE': # A new file was added
- # We have to change to a neutral dir, or cvs will complain about
- # doing checkouts into the repository - even though the checkout
- # is to stdout, sigh.
- origdir = os.getcwd()
- try:
- os.chdir('/tmp')
-
- revclause = ''
- if new and new != 'NONE':
- revclause = '-r ' + new
-
- # "Checkout" to stdout, so we can collect the lines to return.
- co_stdout_cmd = 'cvs -fn co -p %s %s/%s' % (revclause, repo, file)
- handle = os.popen(co_stdout_cmd)
- lines = handle.readlines()
- handle.close()
- header = ("=== Added File %s/%s ==="
- % (repo, file))
-
- finally:
- os.chdir(origdir)
-
- else: # A "normal" update happened
- diff_cmd = ('cvs -d %s -f rdiff -r %s -r %s -kk -u %s/%s'
- % (CVSROOT_ABS, old, new, repo, file))
- file_handle = os.popen(diff_cmd)
- lines = file_handle.readlines()[2:]
- file_handle.close()
- header = ("=== %s/%s %s => %s ==="
- % (str(repo), str(file), old, new))
-
- # Provide for initial checkins on branch - in which case, the ,v files
- # exist only in the Attic.
- if (old == 'NONE') and (len(new.split('.')) > 2):
- template = "%s/%s/Attic/%s,v"
- else:
- template = "%s/%s/%s,v"
- commav = template % (CVSROOT_ABS, repo, file)
- if os.path.exists(commav):
- isbinary = expands_as_binary(commav)
- else:
- complain("Couldn't check binary-ness of missing comma-v"
- " for %s%s v %s, should be at:\n %s"
- % (((old == 'NONE') and "new ") or '',
- file, new, commav))
- # Let the diff go ahead:
- isbinary = None
-
- if isbinary:
- return "%s\n <Binary-ish file>" % header
- else:
- total = len(lines)
- if total >= FILE_LINES_LIMIT:
- omitted = total - (2 * FILE_EXCERPTS_LINES)
- # Limit exceeded, show only exercpts from beginning and end.
- lines = (lines[:FILE_EXCERPTS_LINES]
- + ['\n',
- ('[-=- -=- -=- %s lines omitted -=- -=- -=-]\n'
- % (total - (2 * FILE_EXCERPTS_LINES))),
- '\n']
- + lines[-FILE_EXCERPTS_LINES:])
- header = header + " (%s/%s lines abridged)" % (omitted, total)
- lines.insert(0, header + "\n")
- return string.join(lines, '')
-
-def do_special(trigger, action, addrs):
- """Do special action - a script, to be invoked from the CVSROOT dir.
-
- Run it with the version info and send the result to indicated addrs."""
-
- action_cmd = "%s/CVSROOT/%s %s" % (CVSROOT, action, trigger)
- file_handle = os.popen(action_cmd)
-
- output = file_handle.read()
- result = file_handle.close()
-
- if result:
- note_failure("*** Special command failed with error %s:\n"
- "** %s **\n%s\n",
- result, action_cmd, output)
- else:
- subject = "CVS: %s run for %s changes" % (action, trigger)
- send_mail(addrs, output, subject)
- complain("%s\n%s\n", subject, output)
-
-def send_mail(addrs, text, subject):
- user = getuser()
- fullname = email = ''
- if user:
- fullname, email = get_user_info(user)
- if '.' in fullname or ',' in fullname:
- fullname = '"' + fullname + '"'
- else:
- user = "*unknown*"
- if not fullname:
- fullname = user or "The Unidentified User"
- if not email:
- email = OFFICIAL_SENDER
-
- cmd_info = {'verbose1': (VERBOSE and "set -x; ") or "",
- 'dryrun': (DRYRUN and "echo Would do: ") or "",
- 'mailcmd': MAIL_CMD % email,
- 'verbose2': (VERBOSE and "-v") or ""}
- cmd = ("%(verbose1)s%(dryrun)s%(mailcmd)s %(verbose2)s" % cmd_info)
-
- if VERBOSE:
- complain("%sDoing mail cmd for user %s:\n\t%s\n",
- ((DRYRUN and "NOT ") or ""), user, cmd)
-
- envelope_info = {'subject': subject,
- 'to': string.join(addrs, ", "),
- 'from': "%s <%s>" % (fullname, email),
- 'sender': OFFICIAL_SENDER,
- 'user': user}
- header = ("Subject: %(subject)s\n"
- "To: %(to)s\n"
- "From: %(from)s\n"
- "Sender: %(sender)s\n"
- % envelope_info)
-
- notice = header + '\n' + text
-
- if not DRYRUN:
- cmd_in, cmd_out = os.popen2(cmd, 'rw')
- cmd_in.write(notice)
- cmd_in.close()
- output = cmd_out.read()
- result = cmd_out.close()
- else:
- result = None
-
- if VERBOSE:
- complain(string.join(map(lambda x: '= ' + x,
- string.split(header, '\n')),
- '\n'))
- if result:
- note_failure("*** Mail cmd yielded unexpected result %s:\n%s\n",
- result, output)
-
-def loosen_file(fname):
- """Relax permissions on (newly created) file so others can use it too."""
- try:
- os.chmod(fname, 0777)
- except os.error, err:
- pass
-
-def grok_file(s):
- """Separate "file,old-version,new-version"."""
- m = sVv_re.match(s)
- if not m:
- raise ValueError, "'%s' not in file,old-vers,new-vers format" % s
- return m.groups()
-
-failures = 0
-def note_failure(msg, *args):
- """Register a failure for handle_failures to announce at the end."""
- global failures
- failures = 1
- apply(complain, (msg,) + args)
-
-def getuser():
- """Try to get the user's login name."""
- try: return getpass.getuser()
- except: return None
-
-def get_user_info(user):
- """Obtain some aproximation to user's (fullname, email).
-
- We prefer to get the info out of ldap.
-
- Failing that, we use whatever we can get from the password file.
-
- Failing that, we return an empty fullname and the value of OFFICIAL_SENDER.
-
- Failling all, we return ('', '')."""
- if not user:
- return ('', '')
- # Fallback values:
- email = OFFICIAL_SENDER
- try:
- fullname = pwd.getpwnam(user)[4]
- except KeyError:
- fullname = ''
-
- if USE_LDAP:
- try:
- import ldap; ldap_mod = ldap
- except ImportError:
- try:
- import _ldap; ldap_mod = _ldap
- except ImportError:
- print "Failed to get any LDAP module, punting on ldap."
- return (fullname, email)
- try:
- c = ldap_mod.open(LDAP_HOST, LDAP_PORT)
- c.simple_bind_s('', '')
- # ('sn' for "surname")
- record = c.search_s('ou=people,dc=zope,dc=org',
- ldap_mod.SCOPE_SUBTREE,
- 'cn=%s' % user, ['mail', 'sn', 'givenName'])
- if record:
- d = record[0][1]
- email = d.get('mail', [email])[0]
- first = d.get('givenName', [''])[0]
- last = d.get('sn', [''])[0]
- if first or last:
- if first: first = first + " "
- fullname = "%s%s" % (first, last)
- except ldap_mod.LDAPError:
- pass
-
- return (fullname, email)
-
-def handle_failures(argstring):
- """On serious failures, send the complaints log to CVSMASTER."""
-
- if os.environ.has_key('HOSTNAME'):
- host = os.environ['HOSTNAME']
- else:
- host = socket.gethostbyaddr(socket.gethostname())[0]
- if not host:
- host = "nohost.zope.com"
-
- user = getuser() or "unidentified"
-
- if os.path.isfile(OUTPUT_LOG):
- log_file_expr = ("\n\tSee log file for application errors:\n\t\t%s"
- % OUTPUT_LOG)
- else:
- log_file_expr = ""
-
- complain("Sending complaints log to CVSMASTER %s\n", CVSMASTER)
- complain("Time stamp: %s\n", time.ctime(time.time()))
- complain("Fatal failures in %s:%s"
- "\n\tCheckin by: %s@%s\n\tInvocation: %s\n\n",
- SCRIPT, log_file_expr, user, host, argstring)
-
- cmd = ('%s %s -s "CVS errors in %s for %s" %s'
- % (MAIL_CMD, ((VERBOSE and "-v") or ""), SCRIPT, user, CVSMASTER))
- f = os.popen(cmd, 'w')
- f.write("Serious errors encountered during CVS postcommit actions,\n")
- f.write("the log is below with orienting details at the bottom.\n\n")
- f.write(string.join(complaints, ""))
- return f.close()
-
-def usage():
- complain("Usage: %s [options] ", SCRIPT)
- complain('"repopath file,oldv,newv [f2,o2,n2 ...]"\n')
- complain("(Note that repo-path and files must be"
- " quoted so they are a single token.)\n")
-
-def get_command(cmd, path):
- """Get a valid exe for cmd on path."""
- for d in path:
- maybe = os.path.join(d, cmd)
- if os.path.isfile(maybe):
- return maybe
- note_failure("No usable %s executable found.\n", `cmd`)
-
-EXPANDS_RE = re.compile("expand.*@b@")
-def expands_as_binary(repository_path):
- """Return true if the repository file is marked for binary keyword
- expansion."""
-
- f = open(repository_path, 'r')
- while 1:
- l = f.readline().strip()
- if not l:
- # Break out on first blank line or eof:
- return 0
- if EXPANDS_RE.match(l):
- return 1
-
-complaints = []
-def complain(msg, *args):
- global complaints
-
- import time
- t = time.strftime("%Y-%m-%d %T - ", time.localtime(time.time()))
-
- complaints.append(t + (msg % args))
- sys.stderr.write(msg % args)
- sys.stderr.flush()
-
-def safe_getcwd(fallback):
- try:
- return os.getcwd()
- except:
- os.chdir(fallback)
- try:
- return os.getcwd()
- except:
- return "<unknown dir>"
-
-if __name__ == "__main__":
- main(sys.argv)
+cvs_postcommit(sys.argv,
+ cvsmaster="cvs-admin at zope.org",
+ ldap_host="10.0.24.10",
+ ldap_search_base="ou=people,dc=zope,dc=org",
+ ldap_filter_list_template="cn=%s")
More information about the Zope-CVS
mailing list