~ubuntu-branches/debian/sid/subversion/sid

« back to all changes in this revision

Viewing changes to tools/server-side/svnpubsub/irkerbridge.py

  • Committer: Package Import Robot
  • Author(s): James McCoy, Peter Samuelson, James McCoy
  • Date: 2014-01-12 19:48:33 UTC
  • mfrom: (0.2.10)
  • Revision ID: package-import@ubuntu.com-20140112194833-w3axfwksn296jn5x
Tags: 1.8.5-1
[ Peter Samuelson ]
* New upstream release.  (Closes: #725787) Rediff patches:
  - Remove apr-abi1 (applied upstream), rename apr-abi2 to apr-abi
  - Remove loosen-sqlite-version-check (shouldn't be needed)
  - Remove java-osgi-metadata (applied upstream)
  - svnmucc prompts for a changelog if none is provided. (Closes: #507430)
  - Remove fix-bdb-version-detection, upstream uses "apu-config --dbm-libs"
  - Remove ruby-test-wc (applied upstream)
  - Fix “svn diff -r N file” when file has svn:mime-type set.
    (Closes: #734163)
  - Support specifying an encoding for mod_dav_svn's environment in which
    hooks are run.  (Closes: #601544)
  - Fix ordering of “svnadmin dump” paths with certain APR versions.
    (Closes: #687291)
  - Provide a better error message when authentication fails with an
    svn+ssh:// URL.  (Closes: #273874)
  - Updated Polish translations.  (Closes: #690815)

[ James McCoy ]
* Remove all traces of libneon, replaced by libserf.
* patches/sqlite_3.8.x_workaround: Upstream fix for wc-queries-test test
  failurse.
* Run configure with --with-apache-libexecdir, which allows removing part of
  patches/rpath.
* Re-enable auth-test as upstream has fixed the problem of picking up
  libraries from the environment rather than the build tree.
  (Closes: #654172)
* Point LD_LIBRARY_PATH at the built auth libraries when running the svn
  command during the build.  (Closes: #678224)
* Add a NEWS entry describing how to configure mod_dav_svn to understand
  UTF-8.  (Closes: #566148)
* Remove ancient transitional package, libsvn-ruby.
* Enable compatibility with Sqlite3 versions back to Wheezy.
* Enable hardening flags.  (Closes: #734918)
* patches/build-fixes: Enable verbose build logs.
* Build against the default ruby version.  (Closes: #722393)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
#
 
3
#
 
4
# Licensed to the Apache Software Foundation (ASF) under one or more
 
5
# contributor license agreements.  See the NOTICE file distributed with
 
6
# this work for additional information regarding copyright ownership.
 
7
# The ASF licenses this file to You under the Apache License, Version 2.0
 
8
# (the "License"); you may not use this file except in compliance with
 
9
# the License.  You may obtain a copy of the License at
 
10
#
 
11
#     http://www.apache.org/licenses/LICENSE-2.0
 
12
#
 
13
# Unless required by applicable law or agreed to in writing, software
 
14
# distributed under the License is distributed on an "AS IS" BASIS,
 
15
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
16
# See the License for the specific language governing permissions and
 
17
# limitations under the License.
 
18
#
 
19
 
 
20
# IrkerBridge - Bridge an SvnPubSub stream to Irker.
 
21
 
 
22
# Example:
 
23
#  irkerbridge.py --daemon --pidfile pid --logfile log config
 
24
#
 
25
# For detailed option help use:
 
26
#  irkerbridge.py --help
 
27
 
 
28
# It expects a config file that has the following parameters:
 
29
# streams=url
 
30
#   Space separated list of URLs to streams.
 
31
#   This option should only be in the DEFAULT section, is ignored in
 
32
#   all other sections.
 
33
# irker=hostname:port
 
34
#   The hostname/port combination of the irker daemon.  If port is
 
35
#   omitted it defaults to 6659.  Irker is connected to over UDP.
 
36
# match=What to use to decide if the commit should be sent to irker.
 
37
#   It consists of the repository UUID followed by a slash and a glob pattern.
 
38
#   The UUID may be replaced by a * to match all UUIDs. The glob pattern will
 
39
#   be matched against all of the dirs_changed.  Both the UUID and the glob
 
40
#   pattern must match to send the message to irker.
 
41
# to=url
 
42
#   Space separated list of URLs (any URL that Irker will accept) to
 
43
#   send the resulting message to.  At current Irker only supports IRC.
 
44
# template=string
 
45
#   A string to use to format the output.  The string is a Python
 
46
#   string Template.  The following variables are available:
 
47
#   $committer, $id, $date, $repository, $log, $log_firstline,
 
48
#   $log_firstparagraph, $dirs_changed, $dirs_count, $dirs_count_s,
 
49
#   $subdirs_count, $subdirs_count_s, $dirs_root
 
50
#   Most of them should be self explanatory.  $dirs_count is the number of
 
51
#   entries in $dirs_changed, $dirs_count_s is a friendly string version,
 
52
#   $dirs_root is the common root of all the $dirs_changed, $subdirs_count
 
53
#   is the number of subdirs under the $dirs_root that changed,
 
54
#   $subdirs_root_s is a friendly string version. $log_firstparagraph cuts
 
55
#   the log message at the first blank line and replaces newlines with spaces.
 
56
#
 
57
# Within the config file you have sections.  Any configuration option
 
58
# missing from a given section is found in the [DEFAULT] section.
 
59
#
 
60
# Section names are arbitrary names that mean nothing to the bridge.  Each
 
61
# section other than the [DEFAULT] section consists of a configuration that
 
62
# may match and send a message to irker to deliver.  All matching sections
 
63
# will generate a message.
 
64
#
 
65
# Interpolation of values within the config file is allowed by including
 
66
# %(name)s within a value.  For example I can reference the UUID of a repo
 
67
# repeatedly by doing:
 
68
# [DEFAULT]
 
69
# ASF_REPO=13f79535-47bb-0310-9956-ffa450edef68
 
70
#
 
71
# [#commits]
 
72
# match=%(ASF_REPO)s/
 
73
#
 
74
# You can HUP the process to reload the config file without restarting the
 
75
# process.  However, you cannot change the streams it is listening to without
 
76
# restarting the process.
 
77
#
 
78
# TODO: Logging in a better way.
 
79
 
 
80
# Messages longer than this will be truncated and ... added to the end such
 
81
# that the resulting message is no longer than this:
 
82
MAX_PRIVMSG = 400
 
83
 
 
84
import os
 
85
import sys
 
86
import posixpath
 
87
import socket
 
88
import json
 
89
import urlparse
 
90
import optparse
 
91
import ConfigParser
 
92
import traceback
 
93
import signal
 
94
import re
 
95
import fnmatch
 
96
from string import Template
 
97
 
 
98
# Packages that come with svnpubsub
 
99
import svnpubsub.client
 
100
import daemonize
 
101
 
 
102
class Daemon(daemonize.Daemon):
 
103
  def __init__(self, logfile, pidfile, bdec):
 
104
    daemonize.Daemon.__init__(self, logfile, pidfile)
 
105
 
 
106
    self.bdec = bdec
 
107
 
 
108
  def setup(self):
 
109
    # There is no setup which the parent needs to wait for.
 
110
    pass
 
111
 
 
112
  def run(self):
 
113
    print 'irkerbridge started, pid=%d' % (os.getpid())
 
114
 
 
115
    mc = svnpubsub.client.MultiClient(self.bdec.urls,
 
116
                                      self.bdec.commit,
 
117
                                      self.bdec.event)
 
118
    mc.run_forever()
 
119
 
 
120
 
 
121
class BigDoEverythingClass(object):
 
122
  def __init__(self, config, options):
 
123
    self.config = config
 
124
    self.options = options
 
125
    self.urls = config.get_value('streams').split()
 
126
 
 
127
  def locate_matching_configs(self, commit):
 
128
    result = [ ]
 
129
    for section in self.config.sections():
 
130
      match = self.config.get(section, "match").split('/', 1)
 
131
      if len(match) < 2:
 
132
        # No slash so assume all paths
 
133
        match.append('*')
 
134
      match_uuid, match_path = match
 
135
      if commit.repository == match_uuid or match_uuid == "*":
 
136
        for path in commit.changed:
 
137
          if fnmatch.fnmatch(path, match_path):
 
138
            result.append(section)
 
139
            break
 
140
    return result
 
141
 
 
142
  def _generate_dirs_changed(self, commit):
 
143
    if hasattr(commit, 'dirs_changed') or not hasattr(commit, 'changed'):
 
144
      return
 
145
 
 
146
    dirs_changed = set()
 
147
    for p in commit.changed:
 
148
      if p[-1] == '/' and commit.changed[p]['flags'][1] == 'U':
 
149
        # directory with property changes add the directory itself.
 
150
        dirs_changed.add(p)
 
151
      else:
 
152
        # everything else add the parent of the path
 
153
        # directories have a trailing slash so if it's present remove
 
154
        # it before finding the parent.  The result will be a directory
 
155
        # so it needs a trailing slash
 
156
        dirs_changed.add(posixpath.dirname(p.rstrip('/')) + '/')
 
157
 
 
158
    commit.dirs_changed = dirs_changed
 
159
    return
 
160
 
 
161
  def fill_in_extra_args(self, commit):
 
162
    # Set any empty members to the string "<null>"
 
163
    v = vars(commit)
 
164
    for k in v.keys():
 
165
      if not v[k]:
 
166
        v[k] = '<null>'
 
167
 
 
168
    self._generate_dirs_changed(commit)
 
169
    # Add entries to the commit object that are useful for
 
170
    # formatting.
 
171
    commit.log_firstline = commit.log.split("\n",1)[0]
 
172
    commit.log_firstparagraph = re.split("\r?\n\r?\n",commit.log,1)[0]
 
173
    commit.log_firstparagraph = re.sub("\r?\n"," ",commit.log_firstparagraph)
 
174
    if commit.dirs_changed:
 
175
      commit.dirs_root = posixpath.commonprefix(commit.dirs_changed)
 
176
      if commit.dirs_root == '':
 
177
        commit.dirs_root = '/'
 
178
      commit.dirs_count = len(commit.dirs_changed)
 
179
      if commit.dirs_count > 1:
 
180
        commit.dirs_count_s = " (%d dirs)" %(commit.dirs_count)
 
181
      else:
 
182
        commit.dirs_count_s = ""
 
183
 
 
184
      commit.subdirs_count = commit.dirs_count
 
185
      if commit.dirs_root in commit.dirs_changed:
 
186
        commit.subdirs_count -= 1
 
187
      if commit.subdirs_count >= 1:
 
188
        commit.subdirs_count_s = " + %d subdirs" % (commit.subdirs_count)
 
189
      else:
 
190
        commit.subdirs_count_s = ""
 
191
 
 
192
  def _send(self, irker, msg):
 
193
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 
194
    irker_list = irker.split(':')
 
195
    if len(irker_list) < 2:
 
196
      irker_list.append(6659)
 
197
    json_msg = json.dumps(msg)
 
198
    sock.sendto(json_msg, (irker_list[0],int(irker_list[1])))
 
199
    if self.options.verbose:
 
200
      print "SENT: %s to %s" % (json_msg, irker)
 
201
 
 
202
  def join_all(self):
 
203
    # Like self.commit(), but ignores self.config.get(section, "template").
 
204
    for section in self.config.sections():
 
205
      irker = self.config.get(section, "irker")
 
206
      to_list = self.config.get(section, "to").split()
 
207
      if not irker or not to_list:
 
208
        continue
 
209
      for to in to_list:
 
210
        msg = {'to': to, 'privmsg': ''}
 
211
        self._send(irker, msg)
 
212
 
 
213
  def commit(self, url, commit):
 
214
    if self.options.verbose:
 
215
      print "RECV: from %s" % url
 
216
      print json.dumps(vars(commit), indent=2)
 
217
 
 
218
    try:
 
219
      config_sections = self.locate_matching_configs(commit)
 
220
      if len(config_sections) > 0:
 
221
        self.fill_in_extra_args(commit)
 
222
        for section in config_sections:
 
223
          irker = self.config.get(section, "irker")
 
224
          to_list = self.config.get(section, "to").split()
 
225
          template = self.config.get(section, "template")
 
226
          if not irker or not to_list or not template:
 
227
            continue
 
228
          privmsg = Template(template).safe_substitute(vars(commit))
 
229
          if len(privmsg) > MAX_PRIVMSG:
 
230
            privmsg = privmsg[:MAX_PRIVMSG-3] + '...'
 
231
          for to in to_list:
 
232
            msg = {'to': to, 'privmsg': privmsg}
 
233
            self._send(irker, msg)
 
234
 
 
235
    except:
 
236
      print "Unexpected error:"
 
237
      traceback.print_exc()
 
238
      sys.stdout.flush()
 
239
      raise
 
240
 
 
241
  def event(self, url, event_name, event_arg):
 
242
    if self.options.verbose or event_name != "ping":
 
243
      print 'EVENT: %s from %s' % (event_name, url)
 
244
      sys.stdout.flush()
 
245
 
 
246
 
 
247
 
 
248
class ReloadableConfig(ConfigParser.SafeConfigParser):
 
249
  def __init__(self, fname):
 
250
    ConfigParser.SafeConfigParser.__init__(self)
 
251
 
 
252
    self.fname = fname
 
253
    self.read(fname)
 
254
 
 
255
    signal.signal(signal.SIGHUP, self.hangup)
 
256
 
 
257
  def hangup(self, signalnum, frame):
 
258
    self.reload()
 
259
 
 
260
  def reload(self):
 
261
    print "RELOAD: config file: %s" % self.fname
 
262
    sys.stdout.flush()
 
263
 
 
264
    # Delete everything. Just re-reading would overlay, and would not
 
265
    # remove sections/options. Note that [DEFAULT] will not be removed.
 
266
    for section in self.sections():
 
267
      self.remove_section(section)
 
268
 
 
269
    # Get rid of [DEFAULT]
 
270
    self.remove_section(ConfigParser.DEFAULTSECT)
 
271
 
 
272
    # Now re-read the configuration file.
 
273
    self.read(self.fname)
 
274
 
 
275
  def get_value(self, which):
 
276
    return self.get(ConfigParser.DEFAULTSECT, which)
 
277
 
 
278
 
 
279
def main(args):
 
280
  parser = optparse.OptionParser(
 
281
      description='An SvnPubSub client that bridges the data to irker.',
 
282
      usage='Usage: %prog [options] CONFIG_FILE',
 
283
      )
 
284
  parser.add_option('--logfile',
 
285
      help='filename for logging')
 
286
  parser.add_option('--verbose', action='store_true',
 
287
      help="enable verbose logging")
 
288
  parser.add_option('--pidfile',
 
289
      help="the process' PID will be written to this file")
 
290
  parser.add_option('--daemon', action='store_true',
 
291
      help='run as a background daemon')
 
292
 
 
293
  options, extra = parser.parse_args(args)
 
294
 
 
295
  if len(extra) != 1:
 
296
    parser.error('CONFIG_FILE is requried')
 
297
  config_file = os.path.abspath(extra[0])
 
298
 
 
299
  logfile, pidfile = None, None
 
300
  if options.daemon:
 
301
    if options.logfile:
 
302
      logfile = os.path.abspath(options.logfile)
 
303
    else:
 
304
      parser.error('LOGFILE is required when running as a daemon')
 
305
 
 
306
    if options.pidfile:
 
307
      pidfile = os.path.abspath(options.pidfile)
 
308
    else:
 
309
      parser.error('PIDFILE is required when running as a daemon')
 
310
 
 
311
 
 
312
  config = ReloadableConfig(config_file)
 
313
  bdec = BigDoEverythingClass(config, options)
 
314
 
 
315
  d = Daemon(logfile, pidfile, bdec)
 
316
  if options.daemon:
 
317
    d.daemonize_exit()
 
318
  else:
 
319
    d.foreground()
 
320
 
 
321
if __name__ == "__main__":
 
322
  main(sys.argv[1:])