PQM

Merge lp:~thumper/pqm/queue-abstraction-2 into lp:pqm

Proposed by Tim Penhey
Status: Merged
Merged at revision: not available
Proposed branch: lp:~thumper/pqm/queue-abstraction-2
Merge into: lp:pqm
Prerequisite: lp:~thumper/pqm/queue-abstraction
Diff against target: 928 lines
To merge this branch: bzr merge lp:~thumper/pqm/queue-abstraction-2
Reviewer Review Type Date Requested Status
Robert Collins Pending
Review via email: mp+518@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

Due to a fubar in the code, I can't mark as needs review right now.

Revision history for this message
Tim Penhey (thumper) wrote :
Download full text (31.3 KiB)

=== modified file 'bin/pqm'
--- bin/pqm 2008-07-17 21:29:54 +0000
+++ bin/pqm 2008-08-05 06:33:49 +0000
@@ -56,151 +56,23 @@
 from pqm import *
 from pqm.PQMConfigParser import ConfigParser
 from pqm.commandline import parse_command_line
+from pqm.core import get_script_queue, PatchQueueManager
 from pqm.errors import PQMCmdFailure, PQMException
 from pqm.lockfile import LockFile
 from pqm.script import Command, read_email

-def dir_from_option(configp, option, default):
- """calculate a working dir path"""
- return os.path.abspath(os.path.expanduser(configp.get_option('DEFAULT',option, os.path.join(queuedir, default))))
-
 def runtla_internal(sender, cmd, *args):
     return apply(popen_noshell, [arch_path, cmd] + list(args))

+
 def runtla(sender, cmd, *args):
     (status, msg, output) = apply(runtla_internal, [sender, cmd] + list(args))
     if not ((status is None) or (status == 0)):
         raise PQMTlaFailure(sender, ["VCS command %s %s failed (%s): %s" % (cmd, args, status, msg)] + output)
     return output

-def do_run_mode(queuedir, logger, logdir, mail_reply, mail_server,
- from_address, fromaddr, configp, options):
- scripts = find_patches(
- queuedir, logger, rev_optionhandler, configp, options)
- (goodscripts, badscripts) = ([], [])
- for script in scripts:
- if not os.path.isfile("%s/stop.patch" % queuedir):
- run_one_script(logger, script, logdir, goodscripts, badscripts,
- mail_reply, mail_server, from_address, fromaddr, options)
-
- if options.print_report:
- for (patchname, logname) in goodscripts:
- print "Patch: " + patchname
- print "Status: success"
- print "Log: " + logname
- print
- for (patchname, logname) in badscripts:
- print "Patch: " + patchname
- print "Status: failure"
- print "Log: " + logname
- print
-
-def run_one_script(logger, script, logdir, goodscripts, badscripts,
- mail_reply, mail_server, from_address, fromaddr, options):
- # FIXME: This is currently extremely hard to test. move it to the library,
- # and test it!
- try:
- try:
- logger.info('trying script ' + script.filename)
- logname = os.path.join(logdir, os.path.basename(script.filename) + '.log')
- (sender, subject, msg, sig) = read_email(logger, open(script.filename))
- if options.verify_sigs:
- sigid,siguid = verify_sig(
- script.getSender(), msg, sig, 0, logger, options.keyring)
- success = False
- output = []
- failedcmd=None
-
- # ugly transitional code
- pqm.logger = logger
- pqm.workdir = workdir
- pqm.runtla = runtla
- pqm.precommit_hook = precommit_hook
- (successes, unrecognized, output) = script.run()
-
- logger.info('successes: %s' % (successes,))
- logger.info('unrecognized: %s' % (unrecognized,))
- success = True
- goodscripts.append((script.filename, logname))
- except PQMCmdFailure, e:
- ba...

lp:~thumper/pqm/queue-abstraction-2 updated
181. By Tim Penhey

Merge trunk.

Revision history for this message
Dan Watkins (oddbloke) wrote :

Hi Tim,

This is shaping up nicely. I just have a few suggestions which might
improve it.

On Tue, 05 Aug 2008 06:34:31 -0000
Tim Penhey <email address hidden> wrote:
> === modified file 'bin/pqm'
> @@ -371,12 +234,16 @@
> pqm.used_transactions[line[0:-1]] = 1
>
> if options.read_mode:
> - do_read_mode(logger, options)
> + do_read_mode(logger, options, manager.mail_reply,
> manager.mail_server,
> + manager.from_address, manager.nice_from_address)
It feels to me as if do_read_mode shouldn't exist in bin/pqm any more.
I'm not entirely sure where it should move to, but perhaps EmailQueue
would be appropriate?

> === added file 'pqm/core.py'
> @@ -0,0 +1,229 @@
> +def get_script_queue(queuedir, logger, branch_spec_handler, configp,
> + options):
> + """Determine the type of queue from the config."""
> + # Tim Penhey, 2008-05-29
> + # Right now there is only one queue type, but this will change
> RSN.
> + return EmailQueue(
> + queuedir, logger, branch_spec_handler, configp, options)
What other sorts of queue do you envisage there being?

> === modified file 'pqm/script.py'
It seems to me that the Commands should be moved to their own module,
as several different scripts may wish to use them...

> === modified file 'pqm/ui/twistd.py'
> class QueueResource(resource.Resource):
QueueResource doesn't seem to have been updated to use the new
abstraction. For example, it directly checks whether 'stop.patch'
exists, when there is a method on Queue it should be using instead.

Hope this helps,

Dan

--
Daniel Watkins (Odd_Bloke)

Revision history for this message
Tim Penhey (thumper) wrote :

On Tuesday 05 August 2008 19:12:19 Daniel Watkins wrote:
> Hi Tim,
>
> This is shaping up nicely. I just have a few suggestions which might
> improve it.

Thanks for looking.

> On Tue, 05 Aug 2008 06:34:31 -0000
> Tim Penhey <email address hidden> wrote:
> > === modified file 'bin/pqm'
> > @@ -371,12 +234,16 @@
> > pqm.used_transactions[line[0:-1]] = 1
> >
> > if options.read_mode:
> > - do_read_mode(logger, options)
> > + do_read_mode(logger, options, manager.mail_reply,
> > manager.mail_server,
> > + manager.from_address, manager.nice_from_address)
> It feels to me as if do_read_mode shouldn't exist in bin/pqm any more.
> I'm not entirely sure where it should move to, but perhaps EmailQueue
> would be appropriate?

Hmm.. I was trying hard not to do too much at once :)

Perhaps moving it to EmailQueue would be the best place for this.

> > === added file 'pqm/core.py'
> > @@ -0,0 +1,229 @@
> > +def get_script_queue(queuedir, logger, branch_spec_handler, configp,
> > + options):
> > + """Determine the type of queue from the config."""
> > + # Tim Penhey, 2008-05-29
> > + # Right now there is only one queue type, but this will change
> > RSN.
> > + return EmailQueue(
> > + queuedir, logger, branch_spec_handler, configp, options)
> What other sorts of queue do you envisage there being?

Well, at least one for Launchpad.

> > === modified file 'pqm/script.py'
> It seems to me that the Commands should be moved to their own module,
> as several different scripts may wish to use them...

Again, I was trying to keep the changes small enough. I'm quite happy for
thinks to be broken up more later, but there were challenges enough with
the mass move that was in this branch.

> > === modified file 'pqm/ui/twistd.py'
> > class QueueResource(resource.Resource):
> QueueResource doesn't seem to have been updated to use the new
> abstraction. For example, it directly checks whether 'stop.patch'
> exists, when there is a method on Queue it should be using instead.

OK, I'll look into this later today.

> Hope this helps,

It does, thanks.

Tim

lp:~thumper/pqm/queue-abstraction-2 updated
182. By Tim Penhey

Update the QueueResource for the twisted UI to use the queue rather than checking the filesystem for stop.patch.

183. By Tim Penhey

Fix the test that was incorrectly using the wrong MergeCommand.

Revision history for this message
Tim Penhey (thumper) wrote :
Download full text (3.2 KiB)

This is the diff from r181 -> r183 which addresses the twisted UI referring to stop.patch, and fixes the broken test.

=== modified file 'pqm/tests/test_pqm.py'
--- pqm/tests/test_pqm.py 2008-08-05 06:31:49 +0000
+++ pqm/tests/test_pqm.py 2008-08-06 03:21:19 +0000
@@ -202,11 +202,11 @@
         self.assertEqual(script.getLines(),
             [("star-merge http://www.example.com/foo/bar "
               "http://www.example.com/bar/baz")])
- self.assertEqual([pqm.MergeCommand(None,
- None,
- None,
- 'http://www.example.com/foo/bar',
- 'http://www.example.com/bar/baz')],
+ self.assertEqual([MergeCommand(None,
+ None,
+ None,
+ 'http://www.example.com/foo/bar',
+ 'http://www.example.com/bar/baz')],
                          script.getCommands())

     def testGPGFields(self):
@@ -231,11 +231,11 @@
             [("star-merge http://www.example.com/argh/blah "
               "http://www.example.com/bing/bong")])
         self.assertEqual(
- [pqm.MergeCommand(None,
- None,
- None,
- 'http://www.example.com/argh/blah',
- 'http://www.example.com/bing/bong')],
+ [MergeCommand(None,
+ None,
+ None,
+ 'http://www.example.com/argh/blah',
+ 'http://www.example.com/bing/bong')],
             script.getCommands())

     def testDate(self):

=== modified file 'pqm/ui/twistd.py'
--- pqm/ui/twistd.py 2008-05-29 02:44:29 +0000
+++ pqm/ui/twistd.py 2008-08-06 03:18:46 +0000
@@ -55,15 +55,10 @@
             return self.getProjectPage(None, request).render(request)

     def getProjectPage(self, selected_project, request):
-
- # Get queuedir
- configp = ConfigParser()
- configp.read(self.queue.filenames)
- handler = pqm.BranchSpecOptionHandler(configp)
- queuedir = pqm.get_queuedir(configp, logging, [])
+ """Render the project page."""

         text = "<h1>PQM Queue: %d scripts</h1>" % len(self.queue.messages)
- if os.path.isfile("%s/stop.patch" % queuedir):
+ if not self.queue.is_processing_requests:
             text += "<h2>PQM is not currently processing additional requests</h2>"
         text += "<p>Current time: %s UTC</p>" % cgi.escape(
             time.strftime("%a, %d %b %Y %H:%M:%S", time.gmtime()))
@@ -157,6 +152,7 @@
     def __init__(self, filenames):
         self.filenames = filenames
         self.messages = []
+ self.is_processing_requests = False

     def refresh(self):
         configp = ConfigParser()
@@ -167,6 +163,7 @@
         script_queue = get_script_queue(
             queuedir, logging, handler, configp, FakeOptions())

+ self.is_processing_requests = script_queue.is_processing_requests(...

Read more...

lp:~thumper/pqm/queue-abstraction-2 updated
184. By Tim Penhey

Merge in trunk and fix conflicts.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/pqm'
2--- bin/pqm 2008-07-17 11:53:21 +0000
3+++ bin/pqm 2009-03-05 05:50:16 +0000
4@@ -56,151 +56,23 @@
5 from pqm import *
6 from pqm.PQMConfigParser import ConfigParser
7 from pqm.commandline import parse_command_line
8+from pqm.core import get_script_queue, PatchQueueManager
9 from pqm.errors import PQMCmdFailure, PQMException
10 from pqm.lockfile import LockFile
11 from pqm.script import Command, read_email
12
13
14-def dir_from_option(configp, option, default):
15- """calculate a working dir path"""
16- return os.path.abspath(os.path.expanduser(configp.get_option('DEFAULT',option, os.path.join(queuedir, default))))
17-
18 def runtla_internal(sender, cmd, *args):
19 return apply(popen_noshell, [arch_path, cmd] + list(args))
20
21+
22 def runtla(sender, cmd, *args):
23 (status, msg, output) = apply(runtla_internal, [sender, cmd] + list(args))
24 if not ((status is None) or (status == 0)):
25 raise PQMTlaFailure(sender, ["VCS command %s %s failed (%s): %s" % (cmd, args, status, msg)] + output)
26 return output
27
28-def do_run_mode(queuedir, logger, logdir, mail_reply, mail_server,
29- from_address, fromaddr, configp, options):
30- scripts = find_patches(
31- queuedir, logger, rev_optionhandler, configp, options)
32- (goodscripts, badscripts) = ([], [])
33- for script in scripts:
34- if not os.path.isfile("%s/stop.patch" % queuedir):
35- run_one_script(logger, script, logdir, goodscripts, badscripts,
36- mail_reply, mail_server, from_address, fromaddr, options)
37-
38- if options.print_report:
39- for (patchname, logname) in goodscripts:
40- print "Patch: " + patchname
41- print "Status: success"
42- print "Log: " + logname
43- print
44- for (patchname, logname) in badscripts:
45- print "Patch: " + patchname
46- print "Status: failure"
47- print "Log: " + logname
48- print
49-
50-def run_one_script(logger, script, logdir, goodscripts, badscripts,
51- mail_reply, mail_server, from_address, fromaddr, options):
52- # FIXME: This is currently extremely hard to test. move it to the library,
53- # and test it!
54- try:
55- success = False
56- try:
57- logger.info('trying script ' + script.filename)
58- logname = os.path.join(logdir, os.path.basename(script.filename) + '.log')
59- (sender, subject, msg, sig) = read_email(logger, open(script.filename))
60- if options.verify_sigs:
61- sigid,siguid = verify_sig(
62- script.getSender(), msg, sig, 0, logger, options.keyring)
63- output = []
64- failedcmd=None
65-
66- # ugly transitional code
67- pqm.logger = logger
68- pqm.workdir = workdir
69- pqm.runtla = runtla
70- pqm.precommit_hook = precommit_hook
71- (successes, unrecognized, output) = script.run()
72-
73- logger.info('successes: %s' % (successes,))
74- logger.info('unrecognized: %s' % (unrecognized,))
75- success = True
76- goodscripts.append((script.filename, logname))
77- except PQMCmdFailure, e:
78- badscripts.append((script.filename, logname))
79- successes = e.goodcmds
80- failedcmd = e.badcmd
81- output = e.output
82- unrecognized=[]
83- except PQMException, e:
84- badscripts.append((script.filename, logname))
85- successes = []
86- failedcmd = []
87- output = [str(e)]
88- unrecognized=[]
89- except Exception, e:
90- # catch all to ensure we get some output in uncaught failures
91- output = [str(e)]
92- raise
93- if mail_reply:
94- send_mail_reply(success, successes, unrecognized,
95- mail_server, from_address, script.getSender(),
96- fromaddr, failedcmd, output, script)
97- else:
98- logger.info('not sending mail reply')
99- finally:
100- # ensure we always unlink the script file.
101- log_list(logname, output)
102- os.unlink(script.filename)
103-
104-def send_mail_reply(success, successes, unrecognized, mail_server, from_address, sender, fromaddr, failedcmd, output, script):
105- if success:
106- retmesg = mail_format_successes(successes, "Command was successful.", unrecognized)
107- if len(successes) > 0:
108- statusmsg='success'
109- else:
110- statusmsg='no valid commands given'
111- else:
112- retmesg = mail_format_successes(successes, "Command passed checks, but was not committed.", unrecognized)
113- retmesg+= "\n%s" % failedcmd
114- retmesg+= '\nCommand failed!'
115- if not script.debug:
116- retmesg += '\nLast 20 lines of log output:'
117- retmesg += ''.join(output[-20:])
118- else:
119- retmesg += '\nAll lines of log output:'
120- retmesg += ''.join(output)
121- statusmsg='failure'
122- server = smtplib.SMTP(mail_server)
123- server.sendmail(from_address, [sender], 'From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s\n' % (fromaddr, sender, statusmsg, retmesg))
124- server.quit()
125-
126-def mail_format_successes(successes, command_msg, unrecognized):
127- retmesg = []
128- for success in successes:
129- retmesg.append('> ' + success)
130- retmesg.append(command_msg)
131- for line in unrecognized:
132- retmesg.append('> ' + line)
133- retmesg.append('Unrecognized command.')
134- return string.join(retmesg, '\n')
135-
136-def log_list(logname, list):
137- f = open(logname, 'w')
138- for l in list:
139- f.write(l)
140- f.close()
141-
142-def run(pqm_subdir, queuedir, logger, logdir, mail_reply, mail_server,
143- from_address, fromaddr, options):
144- lockfile=LockFile(os.path.join(pqm_subdir, 'pqm.lock'), logger,
145- options.no_act, options.cron_mode)
146- lockfile.acquire()
147- try:
148- if options.run_mode:
149- do_run_mode(queuedir, logger, logdir, mail_reply, mail_server,
150- from_address, fromaddr, configp, options)
151- finally:
152- lockfile.release()
153-
154-def do_read_mode(logger, options):
155+def do_read_mode(logger, options, mail_reply, mail_server, from_address, fromaddr):
156 sender = None
157 try:
158 (sender, subject, msg, sig) = read_email(logger)
159@@ -230,12 +102,6 @@
160 arch_impl = None
161 logfile_name = 'pqm.log'
162 default_mail_log_level = logging.ERROR
163-mail_server = 'localhost'
164-queuedir = None
165-workdir = None
166-logdir = None
167-mail_reply = 1
168-from_address = None
169 precommit_hook = []
170
171 (options, args) = parse_command_line(sys.argv[1:])
172@@ -290,17 +156,8 @@
173 Command.arch_path = arch_path
174
175 pqm.gpgv_path = configp.get_option('DEFAULT', 'gpgv_path', 'gpgv')
176-myname = configp.get_option('DEFAULT', 'myname', 'Arch Patch Queue Manager')
177-
178-if configp.has_option('DEFAULT', 'from_address'):
179- from_address = configp.get('DEFAULT', 'from_address')
180-else:
181- logger.error("No from_address specified")
182- sys.exit(1)
183-fromaddr = '%s <%s>' % (myname, from_address)
184-
185-mail_reply=configp.get_boolean_option('DEFAULT', 'mail_reply',1)
186-# The command line parameter overrides the setting in the config file.
187+
188+# The command line paramter overrides the setting in the config file.
189 if options.verify_sigs:
190 options.verify_sigs = configp.get_boolean_option(
191 'DEFAULT', 'verify_sigs', True)
192@@ -310,15 +167,24 @@
193 else:
194 queuedir = get_queuedir(configp, logger, args)
195 queuedir=os.path.abspath(queuedir)
196-pqm_subdir = os.path.join(queuedir, 'pqm')
197-pqm.pqm_subdir = pqm_subdir
198+
199+manager = PatchQueueManager(queuedir, configp, options, logger)
200+
201+if manager.from_address is None:
202+ logger.error("No from_address specified")
203+ sys.exit(1)
204+
205+# Still temporary hack. Tim Penhey 2008-05-29
206+pqm.pqm_subdir = manager.control_dir
207+# ugly transitional code
208+pqm.logger = logger
209+pqm.workdir = manager.work_dir
210+pqm.runtla = runtla
211+pqm.precommit_hook = precommit_hook
212
213 if not configp.has_option('DEFAULT', 'dont_set_home'):
214 os.environ['HOME'] = queuedir
215
216-workdir=dir_from_option(configp, 'workdir', 'workdir')
217-logdir=dir_from_option(configp, 'logdir', 'logs')
218-
219 if not options.keyring:
220 if configp.has_option('DEFAULT', 'keyring'):
221 options.keyring = configp.get('DEFAULT', 'keyring')
222@@ -329,11 +195,8 @@
223 logger.error("Couldn't access keyring %s" % (options.keyring,))
224 sys.exit(1)
225
226-do_mkdir(queuedir, options.no_act)
227+manager.make_directories()
228 os.chdir(queuedir)
229-do_mkdir(workdir, options.no_act)
230-do_mkdir(logdir, options.no_act)
231-do_mkdir(pqm_subdir, options.no_act)
232
233 rev_optionhandler = pqm.BranchSpecOptionHandler(configp, queuedir=queuedir)
234 if len(rev_optionhandler._specs) == 0:
235@@ -348,8 +211,8 @@
236
237 if not options.no_log:
238 if not os.path.isabs(logfile_name):
239- logfile_name = os.path.join(pqm_subdir, logfile_name)
240- logger.debug("Adding log file: %s" % (logfile_name,))
241+ logfile_name = os.path.join(manager.control_dir, logfile_name)
242+ logger.debug("Adding log file: %s" % logfile_name)
243 filehandler = logging.FileHandler(logfile_name)
244 if options.loglevel >= logging.WARN:
245 filehandler.setLevel(logging.INFO)
246@@ -371,12 +234,16 @@
247 pqm.used_transactions[line[0:-1]] = 1
248
249 if options.read_mode:
250- do_read_mode(logger, options)
251+ do_read_mode(logger, options, manager.mail_reply, manager.mail_server,
252+ manager.from_address, manager.nice_from_address)
253
254 assert(options.run_mode)
255
256-run(pqm_subdir, queuedir, logger, logdir, mail_reply, mail_server,
257- from_address, fromaddr, options)
258+script_queue = get_script_queue(
259+ queuedir, logger, rev_optionhandler, configp, options)
260+
261+manager.run(script_queue)
262+
263 logger.info("main thread exiting...")
264 sys.exit(0)
265
266
267=== modified file 'pqm/__init__.py'
268--- pqm/__init__.py 2008-07-17 10:09:34 +0000
269+++ pqm/__init__.py 2009-03-05 05:50:16 +0000
270@@ -27,6 +27,7 @@
271 import string
272 import sys
273
274+from pqm.core import do_mkdir
275 from pqm.errors import PQMException, PQMTlaFailure
276 from pqm.script import Script
277
278@@ -41,6 +42,7 @@
279 logger = logging # default value for simple use.
280 groups = {}
281
282+
283 def get_queuedir(config_parser, logger, args):
284 """Get the queuedir that should be used from the config"""
285 if config_parser.has_option('DEFAULT', 'queuedir'):
286@@ -54,23 +56,6 @@
287 sys.exit(1)
288
289
290-def find_patches(queuedir, logger, branch_spec_handler, configp, options):
291- patches=[]
292- patches_re=re.compile('^patch\.\d+$')
293- for f in os.listdir(queuedir):
294- if patches_re.match(f):
295- fname=os.path.join(queuedir, f)
296- submission_time = os.stat(fname)[stat.ST_MTIME]
297- patches.append((Script(fname, logger, options.verify_sigs,
298- submission_time, branch_spec_handler,
299- configp, options.keyring),
300- f))
301- def sortpatches(a, b):
302- return cmp(a[1], b[1])
303- patches.sort(sortpatches)
304- return [patch[0] for patch in patches]
305-
306-
307 def verify_sig(sender, msg, sig, check_replay, logger, keyring):
308 """Verify the GPG signature on a message."""
309 verifier = GPGSigVerifier([keyring], gpgv=gpgv_path)
310@@ -101,7 +86,6 @@
311 logger.error("Replay attack detected, aborting")
312 raise PQMException(sender, "Replay attack detected, aborting")
313 gpg_key_re = re.compile('^\[GNUPG:\] GOODSIG ([0-9A-F]+) .*<([^>]*)>.*$')
314- sig_from = None
315 for line in output:
316 match = gpg_key_re.match(line)
317 if match:
318@@ -476,11 +460,15 @@
319
320 def __init__(self, configp, queuedir=None):
321 self._configp = configp
322+ # TODO: remove the queuedir from this class. The only reason
323+ # it is here is to have the status file. This should now be
324+ # moved into the PatchQueueManager class.
325+ # Tim Penhey 2008-05-29
326 if self._configp.has_option('DEFAULT', 'queuedir'):
327 self.queuedir = os.path.abspath(os.path.expanduser(
328 self._configp.get('DEFAULT', 'queuedir')))
329- do_mkdir(self.queuedir)
330- do_mkdir(os.path.join(self.queuedir, 'pqm'))
331+ do_mkdir(self.queuedir, logger)
332+ do_mkdir(os.path.join(self.queuedir, 'pqm'), logger)
333 else:
334 self.queuedir = queuedir
335 self._specs = {}
336@@ -614,14 +602,3 @@
337 self.statusfile.write(line)
338 self.statusfile.flush()
339 self.statusfile.truncate()
340-
341-def do_mkdir(name, no_act=False):
342- if os.access(name, os.X_OK):
343- return
344- try:
345- logger.info('Creating directory "%s"' % (name))
346- except:
347- pass
348- if not no_act:
349- os.mkdir(name)
350-
351
352=== added file 'pqm/core.py'
353--- pqm/core.py 1970-01-01 00:00:00 +0000
354+++ pqm/core.py 2009-03-05 05:50:16 +0000
355@@ -0,0 +1,229 @@
356+# -*- mode: python; coding: utf-8 -*-
357+# vim:smartindent cinwords=if,elif,else,for,while,try,except,finally,def,class:ts=4:sts=4:sta:et:ai:shiftwidth=4
358+#
359+# Copyright © 2004, 2008 Canonical Ltd.
360+# Author: Robert Collins <robertc@robertcollins.net>
361+# Author: Tim Penhey <tim@canonical.com>
362+
363+# This program is free software; you can redistribute it and/or modify
364+# it under the terms of the GNU General Public License as published by
365+# the Free Software Foundation; either version 2 of the License, or
366+# (at your option) any later version.
367+
368+# This program is distributed in the hope that it will be useful,
369+# but WITHOUT ANY WARRANTY; without even the implied warranty of
370+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
371+# GNU General Public License for more details.
372+
373+# You should have received a copy of the GNU General Public License
374+# along with this program; if not, write to the Free Software
375+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
376+
377+import os
378+import smtplib
379+
380+from pqm.emailqueue import EmailQueue
381+from pqm.errors import PQMCmdFailure, PQMException
382+from pqm.lockfile import LockFile
383+
384+
385+def get_script_queue(queuedir, logger, branch_spec_handler, configp,
386+ options):
387+ """Determine the type of queue from the config."""
388+ # Tim Penhey, 2008-05-29
389+ # Right now there is only one queue type, but this will change RSN.
390+ return EmailQueue(
391+ queuedir, logger, branch_spec_handler, configp, options)
392+
393+
394+def do_mkdir(name, logger, no_act=False):
395+ if os.access(name, os.X_OK):
396+ return
397+ try:
398+ logger.info('Creating directory "%s"' % (name))
399+ except:
400+ pass
401+ if not no_act:
402+ os.mkdir(name)
403+
404+
405+class PatchQueueManager(object):
406+ """The PatchQueueManager controls the core processing."""
407+
408+ branch_configuration = None
409+
410+ # There are four directories that the manager cares about:
411+ # * queue_dir - the primary directory from which the other three
412+ # default to being under.
413+ # * work_dir - where the merging or commands takes place
414+ # * log_dir - where the script execution log files are kept
415+ # * control_dir - where the lockfile, and pqm log file is kept
416+ queue_dir = None
417+ work_dir = None
418+ log_dir = None
419+ control_dir = None
420+
421+ def __init__(self, queue_dir, config_parser, options, logger):
422+ self.queue_dir = queue_dir
423+ self.config_parser = config_parser
424+ self.options = options
425+ self.logger = logger
426+
427+ self.work_dir = self._dir_from_option('workdir', 'workdir')
428+ self.log_dir = self._dir_from_option('logdir', 'logs')
429+ self.control_dir = os.path.join(self.queue_dir, 'pqm')
430+
431+ self.mail_reply = config_parser.get_boolean_option(
432+ 'DEFAULT', 'mail_reply', True)
433+ self.mail_server = config_parser.get_option(
434+ 'DEFAULT', 'mail_server', 'localhost')
435+ myname = config_parser.get_option(
436+ 'DEFAULT', 'myname', 'Arch Patch Queue Manager')
437+ self.from_address = config_parser.get(
438+ 'DEFAULT', 'from_address', None)
439+ if self.from_address is not None:
440+ self.nice_from_address = '%s <%s>' % (myname, self.from_address)
441+
442+ def _dir_from_option(self, option, default):
443+ """Get the option from the config with a sensible default."""
444+ default_dir = os.path.join(self.queue_dir, default)
445+ configured_dir = self.config_parser.get_option(
446+ 'DEFAULT', option, default_dir)
447+ return os.path.abspath(os.path.expanduser(configured_dir))
448+
449+ def make_directories(self):
450+ """Make sure the directories exist."""
451+ do_mkdir(self.queue_dir, self.logger, self.options.no_act)
452+ do_mkdir(self.work_dir, self.logger, self.options.no_act)
453+ do_mkdir(self.log_dir, self.logger, self.options.no_act)
454+ do_mkdir(self.control_dir, self.logger, self.options.no_act)
455+
456+ def run(self, script_queue):
457+ lockfile = LockFile(
458+ os.path.join(self.control_dir, 'pqm.lock'), self.logger,
459+ self.options.no_act, self.options.cron_mode)
460+ lockfile.acquire()
461+ try:
462+ self.do_run_mode(script_queue)
463+ finally:
464+ lockfile.release()
465+
466+ def do_run_mode(self, script_queue):
467+ """Run through the script queue until empty."""
468+ (goodscripts, badscripts) = ([], [])
469+
470+ while script_queue.next_script() is not None:
471+ if script_queue.is_processing_requests():
472+ self.run_one_script(
473+ script_queue.next_script(), goodscripts, badscripts)
474+ script_queue.pop_script()
475+
476+ if self.options.print_report:
477+ for (patchname, logname) in goodscripts:
478+ print "Patch: " + patchname
479+ print "Status: success"
480+ print "Log: " + logname
481+ print
482+ for (patchname, logname) in badscripts:
483+ print "Patch: " + patchname
484+ print "Status: failure"
485+ print "Log: " + logname
486+ print
487+
488+ def run_one_script(self, script, goodscripts, badscripts):
489+ # FIXME: Test it!
490+ try:
491+ success = False
492+ try:
493+ self.logger.info('trying script ' + script.filename)
494+ logname = os.path.join(self.log_dir, os.path.basename(
495+ script.filename) + '.log')
496+
497+ script.pre_run_hook()
498+
499+ output = []
500+ failedcmd=None
501+
502+ (successes, unrecognized, output) = script.run()
503+
504+ self.logger.info('successes: %s' % (successes,))
505+ self.logger.info('unrecognized: %s' % (unrecognized,))
506+ success = True
507+ goodscripts.append((script.filename, logname))
508+ except PQMCmdFailure, e:
509+ badscripts.append((script.filename, logname))
510+ successes = e.goodcmds
511+ failedcmd = e.badcmd
512+ output = e.output
513+ unrecognized=[]
514+ except PQMException, e:
515+ badscripts.append((script.filename, logname))
516+ successes = []
517+ failedcmd = []
518+ output = [str(e)]
519+ unrecognized=[]
520+ except Exception, e:
521+ # catch all to ensure we get some output in uncaught failures
522+ output = [str(e)]
523+ raise
524+ if self.mail_reply:
525+ self.send_mail_reply(success, successes, unrecognized,
526+ script.getSender(), failedcmd, output,
527+ script)
528+ else:
529+ self.logger.info('not sending mail reply')
530+ finally:
531+ # ensure we always unlink the script file.
532+ log_list(logname, output)
533+ os.unlink(script.filename)
534+
535+ def send_mail_reply(self, success, successes, unrecognized,
536+ to_address, failedcmd, output, script):
537+ if success:
538+ retmesg = mail_format_successes(
539+ successes, "Command was successful.", unrecognized)
540+ if len(successes) > 0:
541+ statusmsg='success'
542+ else:
543+ statusmsg='no valid commands given'
544+ else:
545+ retmesg = mail_format_successes(
546+ successes, "Command passed checks, but was not committed.",
547+ unrecognized)
548+ retmesg += "\n%s" % failedcmd
549+ retmesg += '\nCommand failed!'
550+ if not script.debug:
551+ retmesg += '\nLast 20 lines of log output:'
552+ retmesg += ''.join(output[-20:])
553+ else:
554+ retmesg += '\nAll lines of log output:'
555+ retmesg += ''.join(output)
556+ statusmsg='failure'
557+ server = smtplib.SMTP(self.mail_server)
558+ server.sendmail(
559+ self.from_address,
560+ [to_address],
561+ 'From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s\n' % (
562+ self.nice_from_address, to_address, statusmsg, retmesg))
563+ server.quit()
564+
565+
566+def mail_format_successes(successes, command_msg, unrecognized):
567+ retmesg = []
568+ for success in successes:
569+ retmesg.append('> ' + success)
570+ # Do we really want the command_msg for every success?
571+ retmesg.append(command_msg)
572+ # And I'm almost certain that we don't want these for each one.
573+ for line in unrecognized:
574+ retmesg.append('> ' + line)
575+ retmesg.append('Unrecognized command.')
576+ return '\n'.join(retmesg)
577+
578+
579+def log_list(logname, list):
580+ f = open(logname, 'w')
581+ for l in list:
582+ f.write(l)
583+ f.close()
584+
585
586=== added file 'pqm/emailqueue.py'
587--- pqm/emailqueue.py 1970-01-01 00:00:00 +0000
588+++ pqm/emailqueue.py 2009-03-05 05:50:16 +0000
589@@ -0,0 +1,82 @@
590+# -*- mode: python; coding: utf-8 -*-
591+# vim:smartindent cinwords=if,elif,else,for,while,try,except,finally,def,class:ts=4:sts=4:sta:et:ai:shiftwidth=4
592+#
593+# Copyright © 2004, 2008 Canonical Ltd.
594+# Author: Robert Collins <robertc@robertcollins.net>
595+# Author: Tim Penhey <tim@canonical.com>
596+
597+# This program is free software; you can redistribute it and/or modify
598+# it under the terms of the GNU General Public License as published by
599+# the Free Software Foundation; either version 2 of the License, or
600+# (at your option) any later version.
601+
602+# This program is distributed in the hope that it will be useful,
603+# but WITHOUT ANY WARRANTY; without even the implied warranty of
604+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
605+# GNU General Public License for more details.
606+
607+# You should have received a copy of the GNU General Public License
608+# along with this program; if not, write to the Free Software
609+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
610+
611+import os
612+import re
613+import stat
614+
615+from pqm.queue import Queue
616+from pqm.script import EmailScript
617+
618+
619+def find_patches(queuedir, logger, branch_spec_handler, configp, options):
620+ patches=[]
621+ patches_re=re.compile('^patch\.\d+$')
622+ for f in os.listdir(queuedir):
623+ if patches_re.match(f):
624+ fname=os.path.join(queuedir, f)
625+ submission_time = os.stat(fname)[stat.ST_MTIME]
626+ script = EmailScript(fname, logger, options.verify_sigs,
627+ submission_time, branch_spec_handler,
628+ configp, options.keyring)
629+ patches.append((script, f))
630+ def sortpatches(a, b):
631+ return cmp(a[1], b[1])
632+ patches.sort(sortpatches)
633+ return [patch[0] for patch in patches]
634+
635+
636+class EmailQueue(Queue):
637+ """The email queue gets the scripts from the patch files in the queuedir.
638+
639+ """
640+
641+ def __init__(self, queuedir, logger, branch_spec_handler, configp,
642+ options):
643+ self.queuedir = queuedir
644+ self.configp = configp
645+ self.scripts = find_patches(
646+ queuedir, logger, branch_spec_handler, configp, options)
647+
648+ def is_processing_requests(self):
649+ """Is the queue currently processing requests?"""
650+ return not os.path.isfile("%s/stop.patch" % self.queuedir)
651+
652+ def next_script(self):
653+ """The next script to process.
654+
655+ :return: A `Script` or None if there is nothing to do.
656+ """
657+ if len(self.scripts) > 0:
658+ return self.scripts[0]
659+ else:
660+ return None
661+
662+ def pop_script(self):
663+ """Remove the front queue item."""
664+ self.scripts.pop(0)
665+
666+ def items(self):
667+ """The scripts currently in the queue.
668+
669+ This method is used primarily by the UI to show upcoming work.
670+ """
671+ return self.scripts
672
673=== added file 'pqm/queue.py'
674--- pqm/queue.py 1970-01-01 00:00:00 +0000
675+++ pqm/queue.py 2009-03-05 05:50:16 +0000
676@@ -0,0 +1,49 @@
677+# -*- mode: python; coding: utf-8 -*-
678+# vim:smartindent cinwords=if,elif,else,for,while,try,except,finally,def,class:ts=4:sts=4:sta:et:ai:shiftwidth=4
679+#
680+# Copyright © 2008 Canonical Ltd.
681+# Author: Tim Penhey <tim@canonical.com>
682+
683+# This program is free software; you can redistribute it and/or modify
684+# it under the terms of the GNU General Public License as published by
685+# the Free Software Foundation; either version 2 of the License, or
686+# (at your option) any later version.
687+
688+# This program is distributed in the hope that it will be useful,
689+# but WITHOUT ANY WARRANTY; without even the implied warranty of
690+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
691+# GNU General Public License for more details.
692+
693+# You should have received a copy of the GNU General Public License
694+# along with this program; if not, write to the Free Software
695+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
696+
697+class Queue(object):
698+ """The Queue contains scripts for the Patch Queue Manager to do.
699+
700+ The queue contains instances of the type `Script`.
701+ """
702+
703+ def is_processing_requests(self):
704+ """Is the queue currently processing requests?"""
705+ raise NotImplementedError()
706+
707+ def next_script(self):
708+ """The next script to process.
709+
710+ Does not conceptually pop the script from the queue.
711+
712+ :return: A `Script` or None if there is nothing to do.
713+ """
714+ raise NotImplementedError()
715+
716+ def pop_script(self):
717+ """Remove the front queue item."""
718+ raise NotImplementedError()
719+
720+ def items(self):
721+ """The scripts currently in the queue.
722+
723+ This method is used primarily by the UI to show upcoming work.
724+ """
725+ raise NotImplementedError()
726
727=== modified file 'pqm/script.py'
728--- pqm/script.py 2008-07-17 02:03:01 +0000
729+++ pqm/script.py 2009-03-05 05:50:16 +0000
730@@ -73,7 +73,23 @@
731
732
733 class Script(object):
734- """A command script."""
735+ """A script for the Patch Queue Manager to process.
736+
737+ A script will contain one or more `Command`s.
738+ """
739+
740+ def pre_run_hook(self):
741+ """This hook is called before the script is run."""
742+
743+ def post_run_hook(self):
744+ """This hook is called after the script is run.
745+
746+ This method is called regardless of whether or not run raised
747+ an exception.
748+ """
749+
750+class EmailScript(Script):
751+ """A command script generated from an email."""
752
753 whitespace_re = re.compile('^\s*$')
754 pgp_re = re.compile('^-----BEGIN PGP.*MESSAGE')
755@@ -96,6 +112,12 @@
756 self._configp = configp
757 self._keyring = keyring
758
759+ def pre_run_hook(self):
760+ """This hook is called before the script is run."""
761+ # Make sure that the message has been read, and that the
762+ # signature has been verified if needed.
763+ self._read()
764+
765 def _read(self):
766 """Read in the script details."""
767 # This is cruft from the binary script. It actually does need to read
768@@ -177,8 +199,8 @@
769 if not self.isCommand(line):
770 continue
771 # identify and construct commands
772- star_match = Script.star_re.match(line)
773- debug_match = Script.debug_re.match(line)
774+ star_match = EmailScript.star_re.match(line)
775+ debug_match = EmailScript.debug_re.match(line)
776 any_match = star_match or debug_match
777 if any_match and legacy_lines:
778 result.append(CommandRunner(self,
779
780=== modified file 'pqm/tests/test_pqm.py'
781--- pqm/tests/test_pqm.py 2008-07-17 11:53:21 +0000
782+++ pqm/tests/test_pqm.py 2009-03-05 05:50:16 +0000
783@@ -11,7 +11,9 @@
784 import pqm
785 from pqm.errors import PQMCmdFailure
786 from pqm.PQMConfigParser import ConfigParser
787-from pqm.script import Command, CommandRunner, DebugCommand, MergeCommand
788+from pqm.script import (
789+ Command, CommandRunner, DebugCommand, EmailScript, MergeCommand)
790+
791
792 sample_message = dedent("""\
793 From: John.Citizen@example.com
794@@ -167,7 +169,7 @@
795 self.queue.setUp()
796
797 def testName(self):
798- patch = pqm.Script('foo.script', logging, False, 0, None, None)
799+ patch = EmailScript('foo.script', logging, False, 0, None, None)
800 self.assertEqual(patch.filename, 'foo.script')
801 self.scriptname = 'fpp'
802
803@@ -188,7 +190,7 @@
804 configp = ConfigParser()
805 configp.read([self.queue.configFileName])
806 handler = pqm.BranchSpecOptionHandler(configp)
807- return pqm.Script(self.scriptname, logging, False, 54, handler, configp)
808+ return EmailScript(self.scriptname, logging, False, 54, handler, configp)
809
810 def testFields(self):
811 script = self.getScript(sample_message)
812@@ -201,10 +203,10 @@
813 [("star-merge http://www.example.com/foo/bar "
814 "http://www.example.com/bar/baz")])
815 self.assertEqual([MergeCommand(None,
816- None,
817- None,
818- 'http://www.example.com/foo/bar',
819- 'http://www.example.com/bar/baz')],
820+ None,
821+ None,
822+ 'http://www.example.com/foo/bar',
823+ 'http://www.example.com/bar/baz')],
824 script.getCommands())
825
826 def testGPGFields(self):
827@@ -230,10 +232,10 @@
828 "http://www.example.com/bing/bong")])
829 self.assertEqual(
830 [MergeCommand(None,
831- None,
832- None,
833- 'http://www.example.com/argh/blah',
834- 'http://www.example.com/bing/bong')],
835+ None,
836+ None,
837+ 'http://www.example.com/argh/blah',
838+ 'http://www.example.com/bing/bong')],
839 script.getCommands())
840
841 def testDate(self):
842@@ -298,8 +300,7 @@
843 class TestCommandRunner(unittest.TestCase):
844
845 def test_star_merge_urls(self):
846- from pqm import Script
847- star_match = Script.star_re.match("star-merge file:///url1 file:///url2")
848+ star_match = EmailScript.star_re.match("star-merge file:///url1 file:///url2")
849 self.assertEqual(star_match.group(1), 'file:///url1')
850 self.assertEqual(star_match.group(2), 'file:///url2')
851
852
853=== modified file 'pqm/ui/tests/test_twisted.py'
854--- pqm/ui/tests/test_twisted.py 2008-07-16 15:44:48 +0000
855+++ pqm/ui/tests/test_twisted.py 2009-03-05 05:50:16 +0000
856@@ -6,6 +6,7 @@
857
858 import pqm
859 from pqm.PQMConfigParser import ConfigParser
860+from pqm.core import get_script_queue
861 from pqm.tests.test_pqm import QueueSetup
862
863 class TestTwistedUI(unittest.TestCase):
864@@ -28,8 +29,11 @@
865 configp.read([self.queueSetup.configFileName])
866 handler = pqm.BranchSpecOptionHandler(configp)
867 queuedir = pqm.get_queuedir(configp, logging, [])
868- patches = pqm.find_patches(
869+
870+ script_queue = get_script_queue(
871 queuedir, logging, handler, configp, FakeOptions())
872+
873+ patches = script_queue.items()
874 self.assertEqual(3, len(patches))
875 self.assertEqual(patches[0].filename, self.queueSetup.messageFileName)
876 self.assertEqual(set(['project']),
877
878=== modified file 'pqm/ui/twistd.py'
879--- pqm/ui/twistd.py 2008-04-16 10:23:52 +0000
880+++ pqm/ui/twistd.py 2009-03-05 05:50:16 +0000
881@@ -28,6 +28,7 @@
882 import pqm
883 from pqm.PQMConfigParser import ConfigParser
884 from pqm.commandline import default_pqm_config_files
885+from pqm.core import get_script_queue
886
887 class QueueResource(resource.Resource):
888 """A resource that shows a PQM queue."""
889@@ -54,15 +55,10 @@
890 return self.getProjectPage(None, request).render(request)
891
892 def getProjectPage(self, selected_project, request):
893-
894- # Get queuedir
895- configp = ConfigParser()
896- configp.read(self.queue.filenames)
897- handler = pqm.BranchSpecOptionHandler(configp)
898- queuedir = pqm.get_queuedir(configp, logging, [])
899+ """Render the project page."""
900
901 text = "<h1>PQM Queue: %d scripts</h1>" % len(self.queue.messages)
902- if os.path.isfile("%s/stop.patch" % queuedir):
903+ if not self.queue.is_processing_requests:
904 text += "<h2>PQM is not currently processing additional requests</h2>"
905 text += "<p>Current time: %s UTC</p>" % cgi.escape(
906 time.strftime("%a, %d %b %Y %H:%M:%S", time.gmtime()))
907@@ -156,14 +152,20 @@
908 def __init__(self, filenames):
909 self.filenames = filenames
910 self.messages = []
911+ self.is_processing_requests = False
912
913 def refresh(self):
914 configp = ConfigParser()
915 configp.read(self.filenames)
916 handler = pqm.BranchSpecOptionHandler(configp)
917 queuedir = pqm.get_queuedir(configp, logging, [])
918- self.messages = pqm.find_patches(
919+
920+ script_queue = get_script_queue(
921 queuedir, logging, handler, configp, FakeOptions())
922+
923+ self.is_processing_requests = script_queue.is_processing_requests()
924+ self.messages = script_queue.items()
925+
926 try:
927 [message.getSender() for message in self.messages]
928 except:

Subscribers

People subscribed via source and target branches