Merge lp://staging/~thumper/pqm/queue-abstraction-2 into lp://staging/pqm
- queue-abstraction-2
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp://staging/~thumper/pqm/queue-abstraction-2 |
Merge into: | lp://staging/pqm |
Prerequisite: | lp://staging/~thumper/pqm/queue-abstraction |
Diff against target: | 928 lines |
To merge this branch: | bzr merge lp://staging/~thumper/pqm/queue-abstraction-2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robert Collins | Pending | ||
Review via email: mp+518@code.staging.launchpad.net |
Commit message
Description of the change
Tim Penhey (thumper) wrote : | # |
Tim Penhey (thumper) wrote : | # |
=== 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_
- """calculate a working dir path"""
- return os.path.
-
def runtla_
return apply(popen_
+
def runtla(sender, cmd, *args):
(status, msg, output) = apply(runtla_
if not ((status is None) or (status == 0)):
raise PQMTlaFailure(
return output
-def do_run_
- from_address, fromaddr, configp, options):
- scripts = find_patches(
- queuedir, logger, rev_optionhandler, configp, options)
- (goodscripts, badscripts) = ([], [])
- for script in scripts:
- if not os.path.
- run_one_
- mail_reply, mail_server, from_address, fromaddr, options)
-
- if options.
- 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_
- 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.
- (sender, subject, msg, sig) = read_email(logger, open(script.
- if options.
- 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.
- logger.
- success = True
- goodscripts.
- except PQMCmdFailure, e:
- ba...
- 181. By Tim Penhey
-
Merge trunk.
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_
>
> if options.read_mode:
> - do_read_
> + do_read_
> manager.
> + manager.
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_
> + 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_
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(
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)
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_
> >
> > if options.read_mode:
> > - do_read_
> > + do_read_
> > manager.
> > + manager.
> 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_
> > + 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_
> 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(
> 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
- 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.
Tim Penhey (thumper) wrote : | # |
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/
--- pqm/tests/
+++ pqm/tests/
@@ -202,11 +202,11 @@
"http://
- self.assertEqua
- None,
- None,
- 'http://
- 'http://
+ self.assertEqua
+ None,
+ None,
+ 'http://
+ 'http://
def testGPGFields(
@@ -231,11 +231,11 @@
"http://
- [pqm.MergeComma
- None,
- None,
- 'http://
- 'http://
+ [MergeCommand(None,
+ None,
+ None,
+ 'http://
+ 'http://
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.getProject
def getProjectPage(
-
- # Get queuedir
- configp = ConfigParser()
- configp.
- handler = pqm.BranchSpecO
- queuedir = pqm.get_
+ """Render the project page."""
text = "<h1>PQM Queue: %d scripts</h1>" % len(self.
- if os.path.
+ if not self.queue.
text += "<h2>PQM is not currently processing additional requests</h2>"
text += "<p>Current time: %s UTC</p>" % cgi.escape(
@@ -157,6 +152,7 @@
def __init__(self, filenames):
+ self.is_
def refresh(self):
configp = ConfigParser()
@@ -167,6 +163,7 @@
+ self.is_
- 184. By Tim Penhey
-
Merge in trunk and fix conflicts.
Preview Diff
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 | |
44 | - for (patchname, logname) in badscripts: |
45 | - print "Patch: " + patchname |
46 | - print "Status: failure" |
47 | - print "Log: " + logname |
48 | |
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 | |
482 | + for (patchname, logname) in badscripts: |
483 | + print "Patch: " + patchname |
484 | + print "Status: failure" |
485 | + print "Log: " + logname |
486 | |
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: |
Due to a fubar in the code, I can't mark as needs review right now.