2 # vim:set et ts=4 sw=4:
4 """ Handles NEW and BYHAND packages
6 @contact: Debian FTP Master <ftpmaster@debian.org>
7 @copyright: 2001, 2002, 2003, 2004, 2005, 2006 James Troup <james@nocrew.org>
8 @copyright: 2009 Joerg Jaspert <joerg@debian.org>
9 @copyright: 2009 Frank Lichtenheld <djpig@debian.org>
10 @license: GNU General Public License version 2 or later
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 2 of the License, or
15 # (at your option) any later version.
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
26 ################################################################################
28 # 23:12|<aj> I will not hush!
30 # 23:12|<aj> Where there is injustice in the world, I shall be there!
31 # 23:13|<aj> I shall not be silenced!
32 # 23:13|<aj> The world shall know!
33 # 23:13|<aj> The world *must* know!
34 # 23:13|<elmo> oh dear, he's gone back to powerpuff girls... ;-)
35 # 23:13|<aj> yay powerpuff girls!!
36 # 23:13|<aj> buttercup's my favourite, who's yours?
37 # 23:14|<aj> you're backing away from the keyboard right now aren't you?
38 # 23:14|<aj> *AREN'T YOU*?!
39 # 23:15|<aj> I will not be treated like this.
40 # 23:15|<aj> I shall have my revenge.
41 # 23:15|<aj> I SHALL!!!
43 ################################################################################
45 from __future__ import with_statement
56 import apt_pkg, apt_inst
57 import examine_package
59 from daklib.dbconn import *
60 from daklib.queue import *
61 from daklib import daklog
62 from daklib import utils
63 from daklib.regexes import re_no_epoch, re_default_answer, re_isanum, re_package
64 from daklib.dak_exceptions import CantOpenError, AlreadyLockedError, CantGetLockError
65 from daklib.summarystats import SummaryStats
66 from daklib.config import Config
67 from daklib.changesutils import *
76 ################################################################################
77 ################################################################################
78 ################################################################################
80 def recheck(upload, session):
81 # STU: I'm not sure, but I don't thin kthis is necessary any longer: upload.recheck(session)
82 if len(upload.rejects) > 0:
84 if Options["No-Action"] or Options["Automatic"] or Options["Trainee"]:
87 print "REJECT\n%s" % '\n'.join(upload.rejects)
88 prompt = "[R]eject, Skip, Quit ?"
90 while prompt.find(answer) == -1:
91 answer = utils.our_raw_input(prompt)
92 m = re_default_answer.match(prompt)
95 answer = answer[:1].upper()
98 upload.do_reject(manual=0, reject_message='\n'.join(upload.rejects))
99 upload.pkg.remove_known_changes(session=session)
110 ################################################################################
112 class Section_Completer:
113 def __init__ (self, session):
116 for s, in session.query(Section.section):
117 self.sections.append(s)
119 def complete(self, text, state):
123 for word in self.sections:
125 self.matches.append(word)
127 return self.matches[state]
131 ############################################################
133 class Priority_Completer:
134 def __init__ (self, session):
137 for p, in session.query(Priority.priority):
138 self.priorities.append(p)
140 def complete(self, text, state):
144 for word in self.priorities:
146 self.matches.append(word)
148 return self.matches[state]
152 ################################################################################
154 def print_new (new, upload, indexed, file=sys.stdout):
158 for pkg in new.keys():
160 section = new[pkg]["section"]
161 priority = new[pkg]["priority"]
162 if new[pkg]["section id"] == -1:
165 if new[pkg]["priority id"] == -1:
169 line = "(%s): %-20s %-20s %-20s" % (index, pkg, priority, section)
171 line = "%-20s %-20s %-20s" % (pkg, priority, section)
172 line = line.strip()+'\n'
174 notes = get_new_comments(upload.pkg.changes.get("source"))
176 print "\nAuthor: %s\nVersion: %s\nTimestamp: %s\n\n%s" \
177 % (note.author, note.version, note.notedate, note.comment)
179 return broken, len(notes) > 0
181 ################################################################################
183 def index_range (index):
187 return "1-%s" % (index)
189 ################################################################################
190 ################################################################################
192 def edit_new (new, upload):
193 # Write the current data to a temporary file
194 (fd, temp_filename) = utils.temp_filename()
195 temp_file = os.fdopen(fd, 'w')
196 print_new (new, upload, indexed=0, file=temp_file)
198 # Spawn an editor on that file
199 editor = os.environ.get("EDITOR","vi")
200 result = os.system("%s %s" % (editor, temp_filename))
202 utils.fubar ("%s invocation failed for %s." % (editor, temp_filename), result)
203 # Read the edited data back in
204 temp_file = utils.open_file(temp_filename)
205 lines = temp_file.readlines()
207 os.unlink(temp_filename)
214 # Pad the list if necessary
215 s[len(s):3] = [None] * (3-len(s))
216 (pkg, priority, section) = s[:3]
217 if not new.has_key(pkg):
218 utils.warn("Ignoring unknown package '%s'" % (pkg))
220 # Strip off any invalid markers, print_new will readd them.
221 if section.endswith("[!]"):
222 section = section[:-3]
223 if priority.endswith("[!]"):
224 priority = priority[:-3]
225 for f in new[pkg]["files"]:
226 upload.pkg.files[f]["section"] = section
227 upload.pkg.files[f]["priority"] = priority
228 new[pkg]["section"] = section
229 new[pkg]["priority"] = priority
231 ################################################################################
233 def edit_index (new, upload, index):
234 priority = new[index]["priority"]
235 section = new[index]["section"]
236 ftype = new[index]["type"]
239 print "\t".join([index, priority, section])
243 prompt = "[B]oth, Priority, Section, Done ? "
245 prompt = "[S]ection, Done ? "
246 edit_priority = edit_section = 0
248 while prompt.find(answer) == -1:
249 answer = utils.our_raw_input(prompt)
250 m = re_default_answer.match(prompt)
253 answer = answer[:1].upper()
260 edit_priority = edit_section = 1
266 readline.set_completer(Priorities.complete)
268 while not got_priority:
269 new_priority = utils.our_raw_input("New priority: ").strip()
270 if new_priority not in Priorities.priorities:
271 print "E: '%s' is not a valid priority, try again." % (new_priority)
274 priority = new_priority
278 readline.set_completer(Sections.complete)
280 while not got_section:
281 new_section = utils.our_raw_input("New section: ").strip()
282 if new_section not in Sections.sections:
283 print "E: '%s' is not a valid section, try again." % (new_section)
286 section = new_section
288 # Reset the readline completer
289 readline.set_completer(None)
291 for f in new[index]["files"]:
292 upload.pkg.files[f]["section"] = section
293 upload.pkg.files[f]["priority"] = priority
294 new[index]["priority"] = priority
295 new[index]["section"] = section
298 ################################################################################
300 def edit_overrides (new, upload, session):
304 print_new (new, upload, indexed=1)
311 prompt = "(%s) edit override <n>, Editor, Done ? " % (index_range(index))
314 while not got_answer:
315 answer = utils.our_raw_input(prompt)
316 if not answer.isdigit():
317 answer = answer[:1].upper()
318 if answer == "E" or answer == "D":
320 elif re_isanum.match (answer):
322 if (answer < 1) or (answer > index):
323 print "%s is not a valid index (%s). Please retry." % (answer, index_range(index))
328 edit_new(new, upload)
332 edit_index (new, upload, new_index[answer])
337 ################################################################################
339 def check_pkg (upload):
340 save_stdout = sys.stdout
342 sys.stdout = os.popen("less -R -", 'w', 0)
343 changes = utils.parse_changes (upload.pkg.changes_file)
344 print examine_package.display_changes(changes['distribution'], upload.pkg.changes_file)
345 files = upload.pkg.files
346 for f in files.keys():
347 if files[f].has_key("new"):
348 ftype = files[f]["type"]
350 print examine_package.check_deb(changes['distribution'], f)
352 print examine_package.check_dsc(changes['distribution'], f)
353 print examine_package.output_package_relations()
355 if e.errno == errno.EPIPE:
356 utils.warn("[examine_package] Caught EPIPE; skipping.")
358 sys.stdout = save_stdout
360 except KeyboardInterrupt:
361 utils.warn("[examine_package] Caught C-c; skipping.")
362 sys.stdout = save_stdout
364 ################################################################################
366 ## FIXME: horribly Debian specific
368 def do_bxa_notification(upload):
369 files = upload.pkg.files
371 for f in files.keys():
372 if files[f]["type"] == "deb":
373 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(f)))
375 summary += "Package: %s\n" % (control.Find("Package"))
376 summary += "Description: %s\n" % (control.Find("Description"))
377 upload.Subst["__BINARY_DESCRIPTIONS__"] = summary
378 bxa_mail = utils.TemplateSubst(upload.Subst,Config()["Dir::Templates"]+"/process-new.bxa_notification")
379 utils.send_mail(bxa_mail)
381 ################################################################################
383 def add_overrides (new, upload, session):
384 changes = upload.pkg.changes
385 files = upload.pkg.files
386 srcpkg = changes.get("source")
388 for suite in changes["suite"].keys():
389 suite_id = get_suite(suite).suite_id
390 for pkg in new.keys():
391 component_id = get_component(new[pkg]["component"]).component_id
392 type_id = get_override_type(new[pkg]["type"]).overridetype_id
393 priority_id = new[pkg]["priority id"]
394 section_id = new[pkg]["section id"]
395 Logger.log(["%s overrides" % (srcpkg), suite, new[pkg]["component"], new[pkg]["type"], new[pkg]["priority"], new[pkg]["section"]])
396 session.execute("INSERT INTO override (suite, component, type, package, priority, section, maintainer) VALUES (:sid, :cid, :tid, :pkg, :pid, :sectid, '')",
397 { 'sid': suite_id, 'cid': component_id, 'tid':type_id, 'pkg': pkg, 'pid': priority_id, 'sectid': section_id})
398 for f in new[pkg]["files"]:
399 if files[f].has_key("new"):
405 if Config().FindB("Dinstall::BXANotify"):
406 do_bxa_notification(upload)
408 ################################################################################
410 def do_new(upload, session):
412 files = upload.pkg.files
413 upload.check_files(not Options["No-Action"])
414 changes = upload.pkg.changes
417 # Check for a valid distribution
418 upload.check_distributions()
420 # Make a copy of distribution we can happily trample on
421 changes["suite"] = copy.copy(changes["distribution"])
423 # Try to get an included dsc
425 (status, _) = upload.load_dsc()
429 # The main NEW processing loop
433 # Find out what's new
434 new, byhand = determine_new(upload.pkg.changes_file, changes, files, dsc=dsc, session=session, new=new)
440 if Options["No-Action"] or Options["Automatic"]:
443 (broken, note) = print_new(new, upload, indexed=0)
446 if not broken and not note:
447 prompt = "Add overrides, "
449 print "W: [!] marked entries must be fixed before package can be processed."
451 print "W: note must be removed before package can be processed."
452 prompt += "RemOve all notes, Remove note, "
454 prompt += "Edit overrides, Check, Manual reject, Note edit, Prod, [S]kip, Quit ?"
456 while prompt.find(answer) == -1:
457 answer = utils.our_raw_input(prompt)
458 m = re_default_answer.search(prompt)
461 answer = answer[:1].upper()
463 if answer in ( 'A', 'E', 'M', 'O', 'R' ) and Options["Trainee"]:
464 utils.warn("Trainees can't do that")
467 if answer == 'A' and not Options["Trainee"]:
470 done = add_overrides (new, upload, session)
471 new_accept(upload, Options["No-Action"], session)
472 Logger.log(["NEW ACCEPT: %s" % (upload.pkg.changes_file)])
473 except CantGetLockError:
474 print "Hello? Operator! Give me the number for 911!"
475 print "Dinstall in the locked area, cant process packages, come back later"
478 elif answer == 'E' and not Options["Trainee"]:
479 new = edit_overrides (new, upload, session)
480 elif answer == 'M' and not Options["Trainee"]:
481 aborted = upload.do_reject(manual=1,
482 reject_message=Options["Manual-Reject"],
483 notes=get_new_comments(changes.get("source", ""), session=session))
485 upload.pkg.remove_known_changes(session=session)
487 Logger.log(["NEW REJECT: %s" % (upload.pkg.changes_file)])
490 edit_note(get_new_comments(changes.get("source", ""), session=session),
491 upload, session, bool(Options["Trainee"]))
492 elif answer == 'P' and not Options["Trainee"]:
493 prod_maintainer(get_new_comments(changes.get("source", ""), session=session),
495 Logger.log(["NEW PROD: %s" % (upload.pkg.changes_file)])
496 elif answer == 'R' and not Options["Trainee"]:
497 confirm = utils.our_raw_input("Really clear note (y/N)? ").lower()
499 for c in get_new_comments(changes.get("source", ""), changes.get("version", ""), session=session):
502 elif answer == 'O' and not Options["Trainee"]:
503 confirm = utils.our_raw_input("Really clear all notes (y/N)? ").lower()
505 for c in get_new_comments(changes.get("source", ""), session=session):
515 ################################################################################
516 ################################################################################
517 ################################################################################
519 def usage (exit_code=0):
520 print """Usage: dak process-new [OPTION]... [CHANGES]...
521 -a, --automatic automatic run
522 -b, --no-binaries do not sort binary-NEW packages first
523 -c, --comments show NEW comments
524 -h, --help show this help and exit.
525 -m, --manual-reject=MSG manual reject with `msg'
526 -n, --no-action don't do anything
527 -t, --trainee FTP Trainee mode
528 -V, --version display the version number and exit"""
531 ################################################################################
533 def do_byhand(upload, session):
536 files = upload.pkg.files
540 for f in files.keys():
541 if files[f]["section"] == "byhand":
542 if os.path.exists(f):
543 print "W: %s still present; please process byhand components and try again." % (f)
549 if Options["No-Action"]:
552 if Options["Automatic"] and not Options["No-Action"]:
554 prompt = "[A]ccept, Manual reject, Skip, Quit ?"
556 prompt = "Manual reject, [S]kip, Quit ?"
558 while prompt.find(answer) == -1:
559 answer = utils.our_raw_input(prompt)
560 m = re_default_answer.search(prompt)
563 answer = answer[:1].upper()
566 dbchg = get_dbchange(upload.pkg.changes_file, session)
568 print "Warning: cannot find changes file in database; can't process BYHAND"
574 # Find the file entry in the database
576 for f in dbchg.files:
583 print "Warning: Couldn't find BYHAND item %s in the database to mark it processed" % b
586 Logger.log(["BYHAND ACCEPT: %s" % (upload.pkg.changes_file)])
587 except CantGetLockError:
588 print "Hello? Operator! Give me the number for 911!"
589 print "Dinstall in the locked area, cant process packages, come back later"
591 aborted = upload.do_reject(manual=1,
592 reject_message=Options["Manual-Reject"],
593 notes=get_new_comments(changes.get("source", ""), session=session))
595 upload.pkg.remove_known_changes(session=session)
597 Logger.log(["BYHAND REJECT: %s" % (upload.pkg.changes_file)])
605 ################################################################################
607 def check_daily_lock():
609 Raises CantGetLockError if the dinstall daily.lock exists.
614 os.open(cnf["Process-New::DinstallLockFile"],
615 os.O_RDONLY | os.O_CREAT | os.O_EXCL)
617 if e.errno == errno.EEXIST or e.errno == errno.EACCES:
618 raise CantGetLockError
620 os.unlink(cnf["Process-New::DinstallLockFile"])
623 @contextlib.contextmanager
624 def lock_package(package):
626 Lock C{package} so that noone else jumps in processing it.
628 @type package: string
629 @param package: source package name to lock
632 path = os.path.join(Config()["Process-New::LockDir"], package)
634 fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDONLY)
636 if e.errno == errno.EEXIST or e.errno == errno.EACCES:
637 user = pwd.getpwuid(os.stat(path)[stat.ST_UID])[4].split(',')[0].replace('.', '')
638 raise AlreadyLockedError, user
645 class clean_holding(object):
646 def __init__(self,pkg):
652 def __exit__(self, type, value, traceback):
655 for f in self.pkg.files.keys():
656 if os.path.exists(os.path.join(h.holding_dir, f)):
657 os.unlink(os.path.join(h.holding_dir, f))
660 def do_pkg(changes_full_path, session):
661 changes_dir = os.path.dirname(changes_full_path)
662 changes_file = os.path.basename(changes_full_path)
665 u.pkg.changes_file = changes_file
666 (u.pkg.changes["fingerprint"], rejects) = utils.check_signature(changes_file)
667 u.load_changes(changes_file)
668 u.pkg.directory = changes_dir
671 origchanges = os.path.abspath(u.pkg.changes_file)
673 # Try to get an included dsc
675 (status, _) = u.load_dsc()
680 bcc = "X-DAK: dak process-new"
681 if cnf.has_key("Dinstall::Bcc"):
682 u.Subst["__BCC__"] = bcc + "\nBcc: %s" % (cnf["Dinstall::Bcc"])
684 u.Subst["__BCC__"] = bcc
687 u.check_distributions()
688 for deb_filename, f in files.items():
689 if deb_filename.endswith(".udeb") or deb_filename.endswith(".deb"):
690 u.binary_file_checks(deb_filename, session)
691 u.check_binary_against_db(deb_filename, session)
693 u.source_file_checks(deb_filename, session)
694 u.check_source_against_db(deb_filename, session)
696 u.pkg.changes["suite"] = copy.copy(u.pkg.changes["distribution"])
699 with lock_package(u.pkg.changes["source"]):
700 with clean_holding(u.pkg):
701 if not recheck(u, session):
704 new, byhand = determine_new(u.pkg.changes_file, u.pkg.changes, files, dsc=dsc, session=session)
706 do_byhand(u, session)
712 new_accept(u, Options["No-Action"], session)
713 except CantGetLockError:
714 print "Hello? Operator! Give me the number for 911!"
715 print "Dinstall in the locked area, cant process packages, come back later"
717 except AlreadyLockedError, e:
718 print "Seems to be locked by %s already, skipping..." % (e)
720 def show_new_comments(changes_files, session):
722 query = """SELECT package, version, comment, author
724 WHERE package IN ('"""
726 for changes in changes_files:
727 sources.add(os.path.basename(changes).split("_")[0])
729 query += "%s') ORDER BY package, version" % "', '".join(sources)
730 r = session.execute(query)
733 print "%s_%s\n%s\n(%s)\n\n\n" % (i[0], i[1], i[2], i[3])
737 ################################################################################
740 accept_count = SummaryStats().accept_count
741 accept_bytes = SummaryStats().accept_bytes
747 sys.stderr.write("Accepted %d package %s, %s.\n" % (accept_count, sets, utils.size_type(int(accept_bytes))))
748 Logger.log(["total",accept_count,accept_bytes])
750 if not Options["No-Action"] and not Options["Trainee"]:
753 ################################################################################
756 global Options, Logger, Sections, Priorities
759 session = DBConn().session()
761 Arguments = [('a',"automatic","Process-New::Options::Automatic"),
762 ('b',"no-binaries","Process-New::Options::No-Binaries"),
763 ('c',"comments","Process-New::Options::Comments"),
764 ('h',"help","Process-New::Options::Help"),
765 ('m',"manual-reject","Process-New::Options::Manual-Reject", "HasArg"),
766 ('t',"trainee","Process-New::Options::Trainee"),
767 ('n',"no-action","Process-New::Options::No-Action")]
769 for i in ["automatic", "no-binaries", "comments", "help", "manual-reject", "no-action", "version", "trainee"]:
770 if not cnf.has_key("Process-New::Options::%s" % (i)):
771 cnf["Process-New::Options::%s" % (i)] = ""
773 changes_files = apt_pkg.ParseCommandLine(cnf.Cnf,Arguments,sys.argv)
774 if len(changes_files) == 0:
775 new_queue = get_policy_queue('new', session );
776 changes_paths = [ os.path.join(new_queue.path, j) for j in utils.get_changes_files(new_queue.path) ]
778 changes_paths = [ os.path.abspath(j) for j in changes_files ]
780 Options = cnf.SubTree("Process-New::Options")
785 if not Options["No-Action"]:
787 Logger = daklog.Logger(cnf, "process-new")
788 except CantOpenError, e:
789 Options["Trainee"] = "True"
791 Sections = Section_Completer(session)
792 Priorities = Priority_Completer(session)
793 readline.parse_and_bind("tab: complete")
795 if len(changes_paths) > 1:
796 sys.stderr.write("Sorting changes...\n")
797 changes_files = sort_changes(changes_paths, session, Options["No-Binaries"])
799 if Options["Comments"]:
800 show_new_comments(changes_files, session)
802 for changes_file in changes_files:
803 changes_file = utils.validate_changes_file_arg(changes_file, 0)
806 print "\n" + os.path.basename(changes_file)
808 do_pkg (changes_file, session)
812 ################################################################################
814 if __name__ == '__main__':