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 ################################################################################
54 import apt_pkg, apt_inst
55 import examine_package
57 from daklib.dbconn import *
58 from daklib.queue import *
59 from daklib import daklog
60 from daklib import utils
61 from daklib.regexes import re_no_epoch, re_default_answer, re_isanum, re_package
62 from daklib.dak_exceptions import CantOpenError, AlreadyLockedError, CantGetLockError
63 from daklib.summarystats import SummaryStats
64 from daklib.config import Config
65 from daklib.changesutils import *
74 ################################################################################
75 ################################################################################
76 ################################################################################
78 def recheck(upload, session):
79 # STU: I'm not sure, but I don't thin kthis is necessary any longer: upload.recheck(session)
80 if len(upload.rejects) > 0:
82 if Options["No-Action"] or Options["Automatic"] or Options["Trainee"]:
85 print "REJECT\n%s" % '\n'.join(upload.rejects)
86 prompt = "[R]eject, Skip, Quit ?"
88 while prompt.find(answer) == -1:
89 answer = utils.our_raw_input(prompt)
90 m = re_default_answer.match(prompt)
93 answer = answer[:1].upper()
96 upload.do_reject(manual=0, reject_message='\n'.join(upload.rejects))
97 upload.pkg.remove_known_changes(session=session)
108 ################################################################################
110 class Section_Completer:
111 def __init__ (self, session):
114 for s, in session.query(Section.section):
115 self.sections.append(s)
117 def complete(self, text, state):
121 for word in self.sections:
123 self.matches.append(word)
125 return self.matches[state]
129 ############################################################
131 class Priority_Completer:
132 def __init__ (self, session):
135 for p, in session.query(Priority.priority):
136 self.priorities.append(p)
138 def complete(self, text, state):
142 for word in self.priorities:
144 self.matches.append(word)
146 return self.matches[state]
150 ################################################################################
152 def print_new (new, upload, indexed, file=sys.stdout):
156 for pkg in new.keys():
158 section = new[pkg]["section"]
159 priority = new[pkg]["priority"]
160 if new[pkg]["section id"] == -1:
163 if new[pkg]["priority id"] == -1:
167 line = "(%s): %-20s %-20s %-20s" % (index, pkg, priority, section)
169 line = "%-20s %-20s %-20s" % (pkg, priority, section)
170 line = line.strip()+'\n'
172 notes = get_new_comments(upload.pkg.changes.get("source"))
174 print "\nAuthor: %s\nVersion: %s\nTimestamp: %s\n\n%s" \
175 % (note.author, note.version, note.notedate, note.comment)
177 return broken, len(notes) > 0
179 ################################################################################
181 def index_range (index):
185 return "1-%s" % (index)
187 ################################################################################
188 ################################################################################
190 def edit_new (new, upload):
191 # Write the current data to a temporary file
192 (fd, temp_filename) = utils.temp_filename()
193 temp_file = os.fdopen(fd, 'w')
194 print_new (new, upload, indexed=0, file=temp_file)
196 # Spawn an editor on that file
197 editor = os.environ.get("EDITOR","vi")
198 result = os.system("%s %s" % (editor, temp_filename))
200 utils.fubar ("%s invocation failed for %s." % (editor, temp_filename), result)
201 # Read the edited data back in
202 temp_file = utils.open_file(temp_filename)
203 lines = temp_file.readlines()
205 os.unlink(temp_filename)
212 # Pad the list if necessary
213 s[len(s):3] = [None] * (3-len(s))
214 (pkg, priority, section) = s[:3]
215 if not new.has_key(pkg):
216 utils.warn("Ignoring unknown package '%s'" % (pkg))
218 # Strip off any invalid markers, print_new will readd them.
219 if section.endswith("[!]"):
220 section = section[:-3]
221 if priority.endswith("[!]"):
222 priority = priority[:-3]
223 for f in new[pkg]["files"]:
224 upload.pkg.files[f]["section"] = section
225 upload.pkg.files[f]["priority"] = priority
226 new[pkg]["section"] = section
227 new[pkg]["priority"] = priority
229 ################################################################################
231 def edit_index (new, upload, index):
232 priority = new[index]["priority"]
233 section = new[index]["section"]
234 ftype = new[index]["type"]
237 print "\t".join([index, priority, section])
241 prompt = "[B]oth, Priority, Section, Done ? "
243 prompt = "[S]ection, Done ? "
244 edit_priority = edit_section = 0
246 while prompt.find(answer) == -1:
247 answer = utils.our_raw_input(prompt)
248 m = re_default_answer.match(prompt)
251 answer = answer[:1].upper()
258 edit_priority = edit_section = 1
264 readline.set_completer(Priorities.complete)
266 while not got_priority:
267 new_priority = utils.our_raw_input("New priority: ").strip()
268 if new_priority not in Priorities.priorities:
269 print "E: '%s' is not a valid priority, try again." % (new_priority)
272 priority = new_priority
276 readline.set_completer(Sections.complete)
278 while not got_section:
279 new_section = utils.our_raw_input("New section: ").strip()
280 if new_section not in Sections.sections:
281 print "E: '%s' is not a valid section, try again." % (new_section)
284 section = new_section
286 # Reset the readline completer
287 readline.set_completer(None)
289 for f in new[index]["files"]:
290 upload.pkg.files[f]["section"] = section
291 upload.pkg.files[f]["priority"] = priority
292 new[index]["priority"] = priority
293 new[index]["section"] = section
296 ################################################################################
298 def edit_overrides (new, upload, session):
302 print_new (new, upload, indexed=1)
309 prompt = "(%s) edit override <n>, Editor, Done ? " % (index_range(index))
312 while not got_answer:
313 answer = utils.our_raw_input(prompt)
314 if not answer.isdigit():
315 answer = answer[:1].upper()
316 if answer == "E" or answer == "D":
318 elif re_isanum.match (answer):
320 if (answer < 1) or (answer > index):
321 print "%s is not a valid index (%s). Please retry." % (answer, index_range(index))
326 edit_new(new, upload)
330 edit_index (new, upload, new_index[answer])
335 ################################################################################
337 def check_pkg (upload):
338 save_stdout = sys.stdout
340 sys.stdout = os.popen("less -R -", 'w', 0)
341 changes = utils.parse_changes (upload.pkg.changes_file)
342 print examine_package.display_changes(changes['distribution'], upload.pkg.changes_file)
343 files = upload.pkg.files
344 for f in files.keys():
345 if files[f].has_key("new"):
346 ftype = files[f]["type"]
348 print examine_package.check_deb(changes['distribution'], f)
350 print examine_package.check_dsc(changes['distribution'], f)
351 print examine_package.output_package_relations()
353 if e.errno == errno.EPIPE:
354 utils.warn("[examine_package] Caught EPIPE; skipping.")
356 sys.stdout = save_stdout
358 except KeyboardInterrupt:
359 utils.warn("[examine_package] Caught C-c; skipping.")
360 sys.stdout = save_stdout
362 ################################################################################
364 ## FIXME: horribly Debian specific
366 def do_bxa_notification(upload):
367 files = upload.pkg.files
369 for f in files.keys():
370 if files[f]["type"] == "deb":
371 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(f)))
373 summary += "Package: %s\n" % (control.Find("Package"))
374 summary += "Description: %s\n" % (control.Find("Description"))
375 upload.Subst["__BINARY_DESCRIPTIONS__"] = summary
376 bxa_mail = utils.TemplateSubst(upload.Subst,Config()["Dir::Templates"]+"/process-new.bxa_notification")
377 utils.send_mail(bxa_mail)
379 ################################################################################
381 def add_overrides (new, upload, session):
382 changes = upload.pkg.changes
383 files = upload.pkg.files
384 srcpkg = changes.get("source")
386 for suite in changes["suite"].keys():
387 suite_id = get_suite(suite).suite_id
388 for pkg in new.keys():
389 component_id = get_component(new[pkg]["component"]).component_id
390 type_id = get_override_type(new[pkg]["type"]).overridetype_id
391 priority_id = new[pkg]["priority id"]
392 section_id = new[pkg]["section id"]
393 Logger.log(["%s (%s) overrides" % (pkg, srcpkg), suite, new[pkg]["component"], new[pkg]["type"], new[pkg]["priority"], new[pkg]["section"]])
394 session.execute("INSERT INTO override (suite, component, type, package, priority, section, maintainer) VALUES (:sid, :cid, :tid, :pkg, :pid, :sectid, '')",
395 { 'sid': suite_id, 'cid': component_id, 'tid':type_id, 'pkg': pkg, 'pid': priority_id, 'sectid': section_id})
396 for f in new[pkg]["files"]:
397 if files[f].has_key("new"):
403 if Config().FindB("Dinstall::BXANotify"):
404 do_bxa_notification(upload)
406 ################################################################################
408 def do_new(upload, session):
410 files = upload.pkg.files
411 upload.check_files(not Options["No-Action"])
412 changes = upload.pkg.changes
415 # Check for a valid distribution
416 upload.check_distributions()
418 # Make a copy of distribution we can happily trample on
419 changes["suite"] = copy.copy(changes["distribution"])
421 # Try to get an included dsc
423 (status, _) = upload.load_dsc()
427 # The main NEW processing loop
431 # Find out what's new
432 new, byhand = determine_new(upload.pkg.changes_file, changes, files, dsc=dsc, session=session, new=new)
438 if Options["No-Action"] or Options["Automatic"]:
441 (broken, note) = print_new(new, upload, indexed=0)
444 if not broken and not note:
445 prompt = "Add overrides, "
447 print "W: [!] marked entries must be fixed before package can be processed."
449 print "W: note must be removed before package can be processed."
450 prompt += "RemOve all notes, Remove note, "
452 prompt += "Edit overrides, Check, Manual reject, Note edit, Prod, [S]kip, Quit ?"
454 while prompt.find(answer) == -1:
455 answer = utils.our_raw_input(prompt)
456 m = re_default_answer.search(prompt)
459 answer = answer[:1].upper()
461 if answer in ( 'A', 'E', 'M', 'O', 'R' ) and Options["Trainee"]:
462 utils.warn("Trainees can't do that")
465 if answer == 'A' and not Options["Trainee"]:
468 done = add_overrides (new, upload, session)
469 new_accept(upload, Options["No-Action"], session)
470 Logger.log(["NEW ACCEPT: %s" % (upload.pkg.changes_file)])
471 except CantGetLockError:
472 print "Hello? Operator! Give me the number for 911!"
473 print "Dinstall in the locked area, cant process packages, come back later"
476 elif answer == 'E' and not Options["Trainee"]:
477 new = edit_overrides (new, upload, session)
478 elif answer == 'M' and not Options["Trainee"]:
479 aborted = upload.do_reject(manual=1,
480 reject_message=Options["Manual-Reject"],
481 notes=get_new_comments(changes.get("source", ""), session=session))
483 upload.pkg.remove_known_changes(session=session)
485 Logger.log(["NEW REJECT: %s" % (upload.pkg.changes_file)])
488 edit_note(get_new_comments(changes.get("source", ""), session=session),
489 upload, session, bool(Options["Trainee"]))
490 elif answer == 'P' and not Options["Trainee"]:
491 prod_maintainer(get_new_comments(changes.get("source", ""), session=session),
493 Logger.log(["NEW PROD: %s" % (upload.pkg.changes_file)])
494 elif answer == 'R' and not Options["Trainee"]:
495 confirm = utils.our_raw_input("Really clear note (y/N)? ").lower()
497 for c in get_new_comments(changes.get("source", ""), changes.get("version", ""), session=session):
500 elif answer == 'O' and not Options["Trainee"]:
501 confirm = utils.our_raw_input("Really clear all notes (y/N)? ").lower()
503 for c in get_new_comments(changes.get("source", ""), session=session):
513 ################################################################################
514 ################################################################################
515 ################################################################################
517 def usage (exit_code=0):
518 print """Usage: dak process-new [OPTION]... [CHANGES]...
519 -a, --automatic automatic run
520 -b, --no-binaries do not sort binary-NEW packages first
521 -c, --comments show NEW comments
522 -h, --help show this help and exit.
523 -m, --manual-reject=MSG manual reject with `msg'
524 -n, --no-action don't do anything
525 -t, --trainee FTP Trainee mode
526 -V, --version display the version number and exit"""
529 ################################################################################
531 def do_byhand(upload, session):
534 files = upload.pkg.files
538 for f in files.keys():
539 if files[f]["section"] == "byhand":
540 if os.path.exists(f):
541 print "W: %s still present; please process byhand components and try again." % (f)
547 if Options["No-Action"]:
550 if Options["Automatic"] and not Options["No-Action"]:
552 prompt = "[A]ccept, Manual reject, Skip, Quit ?"
554 prompt = "Manual reject, [S]kip, Quit ?"
556 while prompt.find(answer) == -1:
557 answer = utils.our_raw_input(prompt)
558 m = re_default_answer.search(prompt)
561 answer = answer[:1].upper()
564 dbchg = get_dbchange(upload.pkg.changes_file, session)
566 print "Warning: cannot find changes file in database; can't process BYHAND"
572 # Find the file entry in the database
574 for f in dbchg.files:
581 print "Warning: Couldn't find BYHAND item %s in the database to mark it processed" % b
584 Logger.log(["BYHAND ACCEPT: %s" % (upload.pkg.changes_file)])
585 except CantGetLockError:
586 print "Hello? Operator! Give me the number for 911!"
587 print "Dinstall in the locked area, cant process packages, come back later"
589 aborted = upload.do_reject(manual=1,
590 reject_message=Options["Manual-Reject"],
591 notes=get_new_comments(changes.get("source", ""), session=session))
593 upload.pkg.remove_known_changes(session=session)
595 Logger.log(["BYHAND REJECT: %s" % (upload.pkg.changes_file)])
603 ################################################################################
605 def check_daily_lock():
607 Raises CantGetLockError if the dinstall daily.lock exists.
612 lockfile = cnf.get("Process-New::DinstallLockFile",
613 os.path.join(cnf['Dir::Lock'], 'processnew.lock'))
616 os.O_RDONLY | os.O_CREAT | os.O_EXCL)
618 if e.errno == errno.EEXIST or e.errno == errno.EACCES:
619 raise CantGetLockError
624 @contextlib.contextmanager
625 def lock_package(package):
627 Lock C{package} so that noone else jumps in processing it.
629 @type package: string
630 @param package: source package name to lock
635 path = os.path.join(cnf.get("Process-New::LockDir", cnf['Dir::Lock']), package)
638 fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDONLY)
640 if e.errno == errno.EEXIST or e.errno == errno.EACCES:
641 user = pwd.getpwuid(os.stat(path)[stat.ST_UID])[4].split(',')[0].replace('.', '')
642 raise AlreadyLockedError(user)
649 class clean_holding(object):
650 def __init__(self,pkg):
656 def __exit__(self, type, value, traceback):
659 for f in self.pkg.files.keys():
660 if os.path.exists(os.path.join(h.holding_dir, f)):
661 os.unlink(os.path.join(h.holding_dir, f))
664 def do_pkg(changes_full_path, session):
665 changes_dir = os.path.dirname(changes_full_path)
666 changes_file = os.path.basename(changes_full_path)
669 u.pkg.changes_file = changes_file
670 (u.pkg.changes["fingerprint"], rejects) = utils.check_signature(changes_file)
671 u.load_changes(changes_file)
672 u.pkg.directory = changes_dir
675 origchanges = os.path.abspath(u.pkg.changes_file)
677 # Try to get an included dsc
679 (status, _) = u.load_dsc()
684 bcc = "X-DAK: dak process-new"
685 if cnf.has_key("Dinstall::Bcc"):
686 u.Subst["__BCC__"] = bcc + "\nBcc: %s" % (cnf["Dinstall::Bcc"])
688 u.Subst["__BCC__"] = bcc
691 u.check_distributions()
692 for deb_filename, f in files.items():
693 if deb_filename.endswith(".udeb") or deb_filename.endswith(".deb"):
694 u.binary_file_checks(deb_filename, session)
695 u.check_binary_against_db(deb_filename, session)
697 u.source_file_checks(deb_filename, session)
698 u.check_source_against_db(deb_filename, session)
700 u.pkg.changes["suite"] = copy.copy(u.pkg.changes["distribution"])
703 with lock_package(u.pkg.changes["source"]):
704 with clean_holding(u.pkg):
705 if not recheck(u, session):
708 new, byhand = determine_new(u.pkg.changes_file, u.pkg.changes, files, dsc=dsc, session=session)
710 do_byhand(u, session)
716 new_accept(u, Options["No-Action"], session)
717 except CantGetLockError:
718 print "Hello? Operator! Give me the number for 911!"
719 print "Dinstall in the locked area, cant process packages, come back later"
721 except AlreadyLockedError as e:
722 print "Seems to be locked by %s already, skipping..." % (e)
724 def show_new_comments(changes_files, session):
726 query = """SELECT package, version, comment, author
728 WHERE package IN ('"""
730 for changes in changes_files:
731 sources.add(os.path.basename(changes).split("_")[0])
733 query += "%s') ORDER BY package, version" % "', '".join(sources)
734 r = session.execute(query)
737 print "%s_%s\n%s\n(%s)\n\n\n" % (i[0], i[1], i[2], i[3])
741 ################################################################################
744 accept_count = SummaryStats().accept_count
745 accept_bytes = SummaryStats().accept_bytes
751 sys.stderr.write("Accepted %d package %s, %s.\n" % (accept_count, sets, utils.size_type(int(accept_bytes))))
752 Logger.log(["total",accept_count,accept_bytes])
754 if not Options["No-Action"] and not Options["Trainee"]:
757 ################################################################################
760 global Options, Logger, Sections, Priorities
763 session = DBConn().session()
765 Arguments = [('a',"automatic","Process-New::Options::Automatic"),
766 ('b',"no-binaries","Process-New::Options::No-Binaries"),
767 ('c',"comments","Process-New::Options::Comments"),
768 ('h',"help","Process-New::Options::Help"),
769 ('m',"manual-reject","Process-New::Options::Manual-Reject", "HasArg"),
770 ('t',"trainee","Process-New::Options::Trainee"),
771 ('n',"no-action","Process-New::Options::No-Action")]
773 for i in ["automatic", "no-binaries", "comments", "help", "manual-reject", "no-action", "version", "trainee"]:
774 if not cnf.has_key("Process-New::Options::%s" % (i)):
775 cnf["Process-New::Options::%s" % (i)] = ""
777 changes_files = apt_pkg.ParseCommandLine(cnf.Cnf,Arguments,sys.argv)
778 if len(changes_files) == 0:
779 new_queue = get_policy_queue('new', session );
780 changes_paths = [ os.path.join(new_queue.path, j) for j in utils.get_changes_files(new_queue.path) ]
782 changes_paths = [ os.path.abspath(j) for j in changes_files ]
784 Options = cnf.SubTree("Process-New::Options")
789 if not Options["No-Action"]:
791 Logger = daklog.Logger("process-new")
792 except CantOpenError as e:
793 Options["Trainee"] = "True"
795 Sections = Section_Completer(session)
796 Priorities = Priority_Completer(session)
797 readline.parse_and_bind("tab: complete")
799 if len(changes_paths) > 1:
800 sys.stderr.write("Sorting changes...\n")
801 changes_files = sort_changes(changes_paths, session, Options["No-Binaries"])
803 if Options["Comments"]:
804 show_new_comments(changes_files, session)
806 for changes_file in changes_files:
807 changes_file = utils.validate_changes_file_arg(changes_file, 0)
810 print "\n" + os.path.basename(changes_file)
812 do_pkg (changes_file, session)
816 ################################################################################
818 if __name__ == '__main__':