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])
336 ################################################################################
338 def edit_note(note, upload, session):
339 # Write the current data to a temporary file
340 (fd, temp_filename) = utils.temp_filename()
341 editor = os.environ.get("EDITOR","vi")
344 os.system("%s %s" % (editor, temp_filename))
345 temp_file = utils.open_file(temp_filename)
346 newnote = temp_file.read().rstrip()
349 print utils.prefix_multi_line_string(newnote," ")
350 prompt = "[D]one, Edit, Abandon, Quit ?"
352 while prompt.find(answer) == -1:
353 answer = utils.our_raw_input(prompt)
354 m = re_default_answer.search(prompt)
357 answer = answer[:1].upper()
358 os.unlink(temp_filename)
365 comment = NewComment()
366 comment.package = upload.pkg.changes["source"]
367 comment.version = upload.pkg.changes["version"]
368 comment.comment = newnote
369 comment.author = utils.whoami()
370 comment.trainee = bool(Options["Trainee"])
374 ################################################################################
376 def check_pkg (upload):
378 less_fd = os.popen("less -R -", 'w', 0)
379 stdout_fd = sys.stdout
382 changes = utils.parse_changes (upload.pkg.changes_file)
383 print examine_package.display_changes(changes['distribution'], upload.pkg.changes_file)
384 files = upload.pkg.files
385 for f in files.keys():
386 if files[f].has_key("new"):
387 ftype = files[f]["type"]
389 print examine_package.check_deb(changes['distribution'], f)
391 print examine_package.check_dsc(changes['distribution'], f)
393 print examine_package.output_package_relations()
394 sys.stdout = stdout_fd
396 if e.errno == errno.EPIPE:
397 utils.warn("[examine_package] Caught EPIPE; skipping.")
401 except KeyboardInterrupt:
402 utils.warn("[examine_package] Caught C-c; skipping.")
405 ################################################################################
407 ## FIXME: horribly Debian specific
409 def do_bxa_notification(upload):
410 files = upload.pkg.files
412 for f in files.keys():
413 if files[f]["type"] == "deb":
414 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(f)))
416 summary += "Package: %s\n" % (control.Find("Package"))
417 summary += "Description: %s\n" % (control.Find("Description"))
418 upload.Subst["__BINARY_DESCRIPTIONS__"] = summary
419 bxa_mail = utils.TemplateSubst(upload.Subst,Config()["Dir::Templates"]+"/process-new.bxa_notification")
420 utils.send_mail(bxa_mail)
422 ################################################################################
424 def add_overrides (new, upload, session):
425 changes = upload.pkg.changes
426 files = upload.pkg.files
427 srcpkg = changes.get("source")
429 for suite in changes["suite"].keys():
430 suite_id = get_suite(suite).suite_id
431 for pkg in new.keys():
432 component_id = get_component(new[pkg]["component"]).component_id
433 type_id = get_override_type(new[pkg]["type"]).overridetype_id
434 priority_id = new[pkg]["priority id"]
435 section_id = new[pkg]["section id"]
436 Logger.log(["%s overrides" % (srcpkg), suite, new[pkg]["component"], new[pkg]["type"], new[pkg]["priority"], new[pkg]["section"]])
437 session.execute("INSERT INTO override (suite, component, type, package, priority, section, maintainer) VALUES (:sid, :cid, :tid, :pkg, :pid, :sectid, '')",
438 { 'sid': suite_id, 'cid': component_id, 'tid':type_id, 'pkg': pkg, 'pid': priority_id, 'sectid': section_id})
439 for f in new[pkg]["files"]:
440 if files[f].has_key("new"):
446 if Config().FindB("Dinstall::BXANotify"):
447 do_bxa_notification(upload)
449 ################################################################################
451 def prod_maintainer (notes, upload):
453 # Here we prepare an editor and get them ready to prod...
454 (fd, temp_filename) = utils.temp_filename()
455 temp_file = os.fdopen(fd, 'w')
457 temp_file.write(note.comment)
459 editor = os.environ.get("EDITOR","vi")
462 os.system("%s %s" % (editor, temp_filename))
463 temp_fh = utils.open_file(temp_filename)
464 prod_message = "".join(temp_fh.readlines())
466 print "Prod message:"
467 print utils.prefix_multi_line_string(prod_message," ",include_blank_lines=1)
468 prompt = "[P]rod, Edit, Abandon, Quit ?"
470 while prompt.find(answer) == -1:
471 answer = utils.our_raw_input(prompt)
472 m = re_default_answer.search(prompt)
475 answer = answer[:1].upper()
476 os.unlink(temp_filename)
482 # Otherwise, do the proding...
483 user_email_address = utils.whoami() + " <%s>" % (
484 cnf["Dinstall::MyAdminAddress"])
488 Subst["__FROM_ADDRESS__"] = user_email_address
489 Subst["__PROD_MESSAGE__"] = prod_message
490 Subst["__CC__"] = "Cc: " + cnf["Dinstall::MyEmailAddress"]
492 prod_mail_message = utils.TemplateSubst(
493 Subst,cnf["Dir::Templates"]+"/process-new.prod")
496 utils.send_mail(prod_mail_message)
498 print "Sent proding message"
500 ################################################################################
502 def do_new(upload, session):
504 files = upload.pkg.files
505 upload.check_files(not Options["No-Action"])
506 changes = upload.pkg.changes
509 # Check for a valid distribution
510 upload.check_distributions()
512 # Make a copy of distribution we can happily trample on
513 changes["suite"] = copy.copy(changes["distribution"])
515 # The main NEW processing loop
518 # Find out what's new
519 new = determine_new(changes, files)
525 if Options["No-Action"] or Options["Automatic"]:
528 (broken, note) = print_new(new, upload, indexed=0)
531 if not broken and not note:
532 prompt = "Add overrides, "
534 print "W: [!] marked entries must be fixed before package can be processed."
536 print "W: note must be removed before package can be processed."
537 prompt += "RemOve all notes, Remove note, "
539 prompt += "Edit overrides, Check, Manual reject, Note edit, Prod, [S]kip, Quit ?"
541 while prompt.find(answer) == -1:
542 answer = utils.our_raw_input(prompt)
543 m = re_default_answer.search(prompt)
546 answer = answer[:1].upper()
548 if answer in ( 'A', 'E', 'M', 'O', 'R' ) and Options["Trainee"]:
549 utils.warn("Trainees can't do that")
552 if answer == 'A' and not Options["Trainee"]:
555 done = add_overrides (new, upload, session)
556 new_accept(upload, Options["No-Action"], session)
557 Logger.log(["NEW ACCEPT: %s" % (upload.pkg.changes_file)])
558 except CantGetLockError:
559 print "Hello? Operator! Give me the number for 911!"
560 print "Dinstall in the locked area, cant process packages, come back later"
563 elif answer == 'E' and not Options["Trainee"]:
564 new = edit_overrides (new, upload, session)
565 elif answer == 'M' and not Options["Trainee"]:
566 aborted = upload.do_reject(manual=1,
567 reject_message=Options["Manual-Reject"],
568 notes=get_new_comments(changes.get("source", ""), session=session))
570 upload.pkg.remove_known_changes(session=session)
572 Logger.log(["NEW REJECT: %s" % (upload.pkg.changes_file)])
575 edit_note(get_new_comments(changes.get("source", ""), session=session),
577 elif answer == 'P' and not Options["Trainee"]:
578 prod_maintainer(get_new_comments(changes.get("source", ""), session=session),
580 Logger.log(["NEW PROD: %s" % (upload.pkg.changes_file)])
581 elif answer == 'R' and not Options["Trainee"]:
582 confirm = utils.our_raw_input("Really clear note (y/N)? ").lower()
584 for c in get_new_comments(changes.get("source", ""), changes.get("version", ""), session=session):
587 elif answer == 'O' and not Options["Trainee"]:
588 confirm = utils.our_raw_input("Really clear all notes (y/N)? ").lower()
590 for c in get_new_comments(changes.get("source", ""), session=session):
600 ################################################################################
601 ################################################################################
602 ################################################################################
604 def usage (exit_code=0):
605 print """Usage: dak process-new [OPTION]... [CHANGES]...
606 -a, --automatic automatic run
607 -h, --help show this help and exit.
608 -m, --manual-reject=MSG manual reject with `msg'
609 -n, --no-action don't do anything
610 -t, --trainee FTP Trainee mode
611 -V, --version display the version number and exit"""
614 ################################################################################
616 def do_byhand(upload, session):
619 files = upload.pkg.files
623 for f in files.keys():
624 if files[f]["type"] == "byhand":
625 if os.path.exists(f):
626 print "W: %s still present; please process byhand components and try again." % (f)
632 if Options["No-Action"]:
635 if Options["Automatic"] and not Options["No-Action"]:
637 prompt = "[A]ccept, Manual reject, Skip, Quit ?"
639 prompt = "Manual reject, [S]kip, Quit ?"
641 while prompt.find(answer) == -1:
642 answer = utils.our_raw_input(prompt)
643 m = re_default_answer.search(prompt)
646 answer = answer[:1].upper()
654 Logger.log(["BYHAND ACCEPT: %s" % (upload.pkg.changes_file)])
655 except CantGetLockError:
656 print "Hello? Operator! Give me the number for 911!"
657 print "Dinstall in the locked area, cant process packages, come back later"
659 Logger.log(["BYHAND REJECT: %s" % (upload.pkg.changes_file)])
660 upload.do_reject(manual=1, reject_message=Options["Manual-Reject"])
661 upload.pkg.remove_known_changes(session=session)
670 ################################################################################
672 def check_daily_lock():
674 Raises CantGetLockError if the dinstall daily.lock exists.
679 os.open(cnf["Process-New::DinstallLockFile"],
680 os.O_RDONLY | os.O_CREAT | os.O_EXCL)
682 if e.errno == errno.EEXIST or e.errno == errno.EACCES:
683 raise CantGetLockError
685 os.unlink(cnf["Process-New::DinstallLockFile"])
688 @contextlib.contextmanager
689 def lock_package(package):
691 Lock C{package} so that noone else jumps in processing it.
693 @type package: string
694 @param package: source package name to lock
697 path = os.path.join(Config()["Process-New::LockDir"], package)
699 fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDONLY)
701 if e.errno == errno.EEXIST or e.errno == errno.EACCES:
702 user = pwd.getpwuid(os.stat(path)[stat.ST_UID])[4].split(',')[0].replace('.', '')
703 raise AlreadyLockedError, user
710 class clean_holding(object):
711 def __init__(self,pkg):
717 def __exit__(self, type, value, traceback):
720 for f in self.pkg.files.keys():
721 if os.path.exists(os.path.join(h.holding_dir, f)):
722 os.unlink(os.path.join(h.holding_dir, f))
725 def do_pkg(changes_file, session):
726 new_queue = get_policy_queue('new', session );
728 u.pkg.changes_file = changes_file
729 (u.pkg.changes["fingerprint"], rejects) = utils.check_signature(changes_file)
730 u.load_changes(changes_file)
731 u.pkg.directory = new_queue.path
734 origchanges = os.path.abspath(u.pkg.changes_file)
737 bcc = "X-DAK: dak process-new"
738 if cnf.has_key("Dinstall::Bcc"):
739 u.Subst["__BCC__"] = bcc + "\nBcc: %s" % (cnf["Dinstall::Bcc"])
741 u.Subst["__BCC__"] = bcc
744 for deb_filename, f in files.items():
745 if deb_filename.endswith(".udeb") or deb_filename.endswith(".deb"):
746 u.binary_file_checks(deb_filename, session)
747 u.check_binary_against_db(deb_filename, session)
749 u.source_file_checks(deb_filename, session)
750 u.check_source_against_db(deb_filename, session)
752 u.pkg.changes["suite"] = copy.copy(u.pkg.changes["distribution"])
755 with lock_package(u.pkg.changes["source"]):
756 with clean_holding(u.pkg):
757 if not recheck(u, session):
760 # FIXME: This does need byhand checks added!
761 new = determine_new(u.pkg.changes, files)
767 new_accept(u, Options["No-Action"], session)
768 except CantGetLockError:
769 print "Hello? Operator! Give me the number for 911!"
770 print "Dinstall in the locked area, cant process packages, come back later"
772 except AlreadyLockedError, e:
773 print "Seems to be locked by %s already, skipping..." % (e)
775 ################################################################################
778 accept_count = SummaryStats().accept_count
779 accept_bytes = SummaryStats().accept_bytes
785 sys.stderr.write("Accepted %d package %s, %s.\n" % (accept_count, sets, utils.size_type(int(accept_bytes))))
786 Logger.log(["total",accept_count,accept_bytes])
788 if not Options["No-Action"] and not Options["Trainee"]:
791 ################################################################################
794 global Options, Logger, Sections, Priorities
797 session = DBConn().session()
799 Arguments = [('a',"automatic","Process-New::Options::Automatic"),
800 ('h',"help","Process-New::Options::Help"),
801 ('m',"manual-reject","Process-New::Options::Manual-Reject", "HasArg"),
802 ('t',"trainee","Process-New::Options::Trainee"),
803 ('n',"no-action","Process-New::Options::No-Action")]
805 for i in ["automatic", "help", "manual-reject", "no-action", "version", "trainee"]:
806 if not cnf.has_key("Process-New::Options::%s" % (i)):
807 cnf["Process-New::Options::%s" % (i)] = ""
809 changes_files = apt_pkg.ParseCommandLine(cnf.Cnf,Arguments,sys.argv)
810 if len(changes_files) == 0:
811 new_queue = get_policy_queue('new', session );
812 changes_files = utils.get_changes_files(new_queue.path)
814 Options = cnf.SubTree("Process-New::Options")
819 if not Options["No-Action"]:
821 Logger = daklog.Logger(cnf, "process-new")
822 except CantOpenError, e:
823 Options["Trainee"] = "True"
825 Sections = Section_Completer(session)
826 Priorities = Priority_Completer(session)
827 readline.parse_and_bind("tab: complete")
829 if len(changes_files) > 1:
830 sys.stderr.write("Sorting changes...\n")
831 changes_files = sort_changes(changes_files, session)
833 for changes_file in changes_files:
834 changes_file = utils.validate_changes_file_arg(changes_file, 0)
837 print "\n" + changes_file
839 do_pkg (changes_file, session)
843 ################################################################################
845 if __name__ == '__main__':