]> git.decadent.org.uk Git - dak.git/blob - dak/transitions.py
Move regexes into a module so we can keep track
[dak.git] / dak / transitions.py
1 #!/usr/bin/env python
2
3 # Display, edit and check the release manager's transition file.
4 # Copyright (C) 2008 Joerg Jaspert <joerg@debian.org>
5
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 ################################################################################
21
22 # <elmo> if klecker.d.o died, I swear to god, I'm going to migrate to gentoo.
23
24 ################################################################################
25
26 import os, pg, sys, time, errno, fcntl, tempfile, pwd
27 import apt_pkg
28 from daklib import database
29 from daklib import utils
30 from daklib.dak_exceptions import TransitionsError
31 from daklib.regexes import re_broken_package
32 import yaml
33
34 # Globals
35 Cnf = None
36 Options = None
37 projectB = None
38
39 ################################################################################
40
41 #####################################
42 #### This may run within sudo !! ####
43 #####################################
44 def init():
45     global Cnf, Options, projectB
46
47     apt_pkg.init()
48
49     Cnf = utils.get_conf()
50
51     Arguments = [('h',"help","Edit-Transitions::Options::Help"),
52                  ('e',"edit","Edit-Transitions::Options::Edit"),
53                  ('i',"import","Edit-Transitions::Options::Import", "HasArg"),
54                  ('c',"check","Edit-Transitions::Options::Check"),
55                  ('s',"sudo","Edit-Transitions::Options::Sudo"),
56                  ('n',"no-action","Edit-Transitions::Options::No-Action")]
57
58     for i in ["help", "no-action", "edit", "import", "check", "sudo"]:
59         if not Cnf.has_key("Edit-Transitions::Options::%s" % (i)):
60             Cnf["Edit-Transitions::Options::%s" % (i)] = ""
61
62     apt_pkg.ParseCommandLine(Cnf, Arguments, sys.argv)
63
64     Options = Cnf.SubTree("Edit-Transitions::Options")
65
66     if Options["help"]:
67         usage()
68
69     whoami = os.getuid()
70     whoamifull = pwd.getpwuid(whoami)
71     username = whoamifull[0]
72     if username != "dak":
73         print "Non-dak user: %s" % username
74         Options["sudo"] = "y"
75
76     projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]))
77     database.init(Cnf, projectB)
78
79 ################################################################################
80
81 def usage (exit_code=0):
82     print """Usage: transitions [OPTION]...
83 Update and check the release managers transition file.
84
85 Options:
86
87   -h, --help                show this help and exit.
88   -e, --edit                edit the transitions file
89   -i, --import <file>       check and import transitions from file
90   -c, --check               check the transitions file, remove outdated entries
91   -S, --sudo                use sudo to update transitions file
92   -n, --no-action           don't do anything (only affects check)"""
93
94     sys.exit(exit_code)
95
96 ################################################################################
97
98 #####################################
99 #### This may run within sudo !! ####
100 #####################################
101 def load_transitions(trans_file):
102     # Parse the yaml file
103     sourcefile = file(trans_file, 'r')
104     sourcecontent = sourcefile.read()
105     failure = False
106     try:
107         trans = yaml.load(sourcecontent)
108     except yaml.YAMLError, exc:
109         # Someone fucked it up
110         print "ERROR: %s" % (exc)
111         return None
112
113     # lets do further validation here
114     checkkeys = ["source", "reason", "packages", "new", "rm"]
115
116     # If we get an empty definition - we just have nothing to check, no transitions defined
117     if type(trans) != dict:
118         # This can be anything. We could have no transitions defined. Or someone totally fucked up the
119         # file, adding stuff in a way we dont know or want. Then we set it empty - and simply have no
120         # transitions anymore. User will see it in the information display after he quit the editor and
121         # could fix it
122         trans = ""
123         return trans
124
125     try:
126         for test in trans:
127             t = trans[test]
128
129             # First check if we know all the keys for the transition and if they have
130             # the right type (and for the packages also if the list has the right types
131             # included, ie. not a list in list, but only str in the list)
132             for key in t:
133                 if key not in checkkeys:
134                     print "ERROR: Unknown key %s in transition %s" % (key, test)
135                     failure = True
136
137                 if key == "packages":
138                     if type(t[key]) != list:
139                         print "ERROR: Unknown type %s for packages in transition %s." % (type(t[key]), test)
140                         failure = True
141                     try:
142                         for package in t["packages"]:
143                             if type(package) != str:
144                                 print "ERROR: Packages list contains invalid type %s (as %s) in transition %s" % (type(package), package, test)
145                                 failure = True
146                             if re_broken_package.match(package):
147                                 # Someone had a space too much (or not enough), we have something looking like
148                                 # "package1 - package2" now.
149                                 print "ERROR: Invalid indentation of package list in transition %s, around package(s): %s" % (test, package)
150                                 failure = True
151                     except TypeError:
152                         # In case someone has an empty packages list
153                         print "ERROR: No packages defined in transition %s" % (test)
154                         failure = True
155                         continue
156
157                 elif type(t[key]) != str:
158                     if key == "new" and type(t[key]) == int:
159                         # Ok, debian native version
160                         continue
161                     else:
162                         print "ERROR: Unknown type %s for key %s in transition %s" % (type(t[key]), key, test)
163                         failure = True
164
165             # And now the other way round - are all our keys defined?
166             for key in checkkeys:
167                 if key not in t:
168                     print "ERROR: Missing key %s in transition %s" % (key, test)
169                     failure = True
170     except TypeError:
171         # In case someone defined very broken things
172         print "ERROR: Unable to parse the file"
173         failure = True
174
175
176     if failure:
177         return None
178
179     return trans
180
181 ################################################################################
182
183 #####################################
184 #### This may run within sudo !! ####
185 #####################################
186 def lock_file(f):
187     for retry in range(10):
188         lock_fd = os.open(f, os.O_RDWR | os.O_CREAT)
189         try:
190             fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
191             return lock_fd
192         except OSError, e:
193             if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EEXIST':
194                 print "Unable to get lock for %s (try %d of 10)" % \
195                         (file, retry+1)
196                 time.sleep(60)
197             else:
198                 raise
199
200     utils.fubar("Couldn't obtain lock for %s." % (f))
201
202 ################################################################################
203
204 #####################################
205 #### This may run within sudo !! ####
206 #####################################
207 def write_transitions(from_trans):
208     """Update the active transitions file safely.
209        This function takes a parsed input file (which avoids invalid
210        files or files that may be be modified while the function is
211        active), and ensure the transitions file is updated atomically
212        to avoid locks."""
213
214     trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
215     trans_temp = trans_file + ".tmp"
216
217     trans_lock = lock_file(trans_file)
218     temp_lock  = lock_file(trans_temp)
219
220     destfile = file(trans_temp, 'w')
221     yaml.dump(from_trans, destfile, default_flow_style=False)
222     destfile.close()
223
224     os.rename(trans_temp, trans_file)
225     os.close(temp_lock)
226     os.close(trans_lock)
227
228 ################################################################################
229
230 ##########################################
231 #### This usually runs within sudo !! ####
232 ##########################################
233 def write_transitions_from_file(from_file):
234     """We have a file we think is valid; if we're using sudo, we invoke it
235        here, otherwise we just parse the file and call write_transitions"""
236
237     # Lets check if from_file is in the directory we expect it to be in
238     if not os.path.abspath(from_file).startswith(Cnf["Transitions::TempPath"]):
239         print "Will not accept transitions file outside of %s" % (Cnf["Transitions::TempPath"])
240         sys.exit(3)
241
242     if Options["sudo"]:
243         os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H",
244               "/usr/local/bin/dak", "transitions", "--import", from_file)
245     else:
246         trans = load_transitions(from_file)
247         if trans is None:
248             raise TransitionsError, "Unparsable transitions file %s" % (file)
249         write_transitions(trans)
250
251 ################################################################################
252
253 def temp_transitions_file(transitions):
254     # NB: file is unlinked by caller, but fd is never actually closed.
255     # We need the chmod, as the file is (most possibly) copied from a
256     # sudo-ed script and would be unreadable if it has default mkstemp mode
257
258     (fd, path) = tempfile.mkstemp("", "transitions", Cnf["Transitions::TempPath"])
259     os.chmod(path, 0644)
260     f = open(path, "w")
261     yaml.dump(transitions, f, default_flow_style=False)
262     return path
263
264 ################################################################################
265
266 def edit_transitions():
267     trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
268     edit_file = temp_transitions_file(load_transitions(trans_file))
269
270     editor = os.environ.get("EDITOR", "vi")
271
272     while True:
273         result = os.system("%s %s" % (editor, edit_file))
274         if result != 0:
275             os.unlink(edit_file)
276             utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file))
277
278         # Now try to load the new file
279         test = load_transitions(edit_file)
280
281         if test == None:
282             # Edit is broken
283             print "Edit was unparsable."
284             prompt = "[E]dit again, Drop changes?"
285             default = "E"
286         else:
287             print "Edit looks okay.\n"
288             print "The following transitions are defined:"
289             print "------------------------------------------------------------------------"
290             transition_info(test)
291
292             prompt = "[S]ave, Edit again, Drop changes?"
293             default = "S"
294
295         answer = "XXX"
296         while prompt.find(answer) == -1:
297             answer = utils.our_raw_input(prompt)
298             if answer == "":
299                 answer = default
300             answer = answer[:1].upper()
301
302         if answer == 'E':
303             continue
304         elif answer == 'D':
305             os.unlink(edit_file)
306             print "OK, discarding changes"
307             sys.exit(0)
308         elif answer == 'S':
309             # Ready to save
310             break
311         else:
312             print "You pressed something you shouldn't have :("
313             sys.exit(1)
314
315     # We seem to be done and also have a working file. Copy over.
316     write_transitions_from_file(edit_file)
317     os.unlink(edit_file)
318
319     print "Transitions file updated."
320
321 ################################################################################
322
323 def check_transitions(transitions):
324     to_dump = 0
325     to_remove = []
326     # Now look through all defined transitions
327     for trans in transitions:
328         t = transitions[trans]
329         source = t["source"]
330         expected = t["new"]
331
332         # Will be None if nothing is in testing.
333         current = database.get_suite_version(source, "testing")
334
335         print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
336
337         if current == None:
338             # No package in testing
339             print "Transition source %s not in testing, transition still ongoing." % (source)
340         else:
341             compare = apt_pkg.VersionCompare(current, expected)
342             if compare < 0:
343                 # This is still valid, the current version in database is older than
344                 # the new version we wait for
345                 print "This transition is still ongoing, we currently have version %s" % (current)
346             else:
347                 print "REMOVE: This transition is over, the target package reached testing. REMOVE"
348                 print "%s wanted version: %s, has %s" % (source, expected, current)
349                 to_remove.append(trans)
350                 to_dump = 1
351         print "-------------------------------------------------------------------------"
352
353     if to_dump:
354         prompt = "Removing: "
355         for remove in to_remove:
356             prompt += remove
357             prompt += ","
358
359         prompt += " Commit Changes? (y/N)"
360         answer = ""
361
362         if Options["no-action"]:
363             answer="n"
364         else:
365             answer = utils.our_raw_input(prompt).lower()
366
367         if answer == "":
368             answer = "n"
369
370         if answer == 'n':
371             print "Not committing changes"
372             sys.exit(0)
373         elif answer == 'y':
374             print "Committing"
375             for remove in to_remove:
376                 del transitions[remove]
377
378             edit_file = temp_transitions_file(transitions)
379             write_transitions_from_file(edit_file)
380
381             print "Done"
382         else:
383             print "WTF are you typing?"
384             sys.exit(0)
385
386 ################################################################################
387
388 def print_info(trans, source, expected, rm, reason, packages):
389     print """Looking at transition: %s
390 Source:      %s
391 New Version: %s
392 Responsible: %s
393 Description: %s
394 Blocked Packages (total: %d): %s
395 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
396     return
397
398 ################################################################################
399
400 def transition_info(transitions):
401     for trans in transitions:
402         t = transitions[trans]
403         source = t["source"]
404         expected = t["new"]
405
406         # Will be None if nothing is in testing.
407         current = database.get_suite_version(source, "testing")
408
409         print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
410
411         if current == None:
412             # No package in testing
413             print "Transition source %s not in testing, transition still ongoing." % (source)
414         else:
415             compare = apt_pkg.VersionCompare(current, expected)
416             print "Apt compare says: %s" % (compare)
417             if compare < 0:
418                 # This is still valid, the current version in database is older than
419                 # the new version we wait for
420                 print "This transition is still ongoing, we currently have version %s" % (current)
421             else:
422                 print "This transition is over, the target package reached testing, should be removed"
423                 print "%s wanted version: %s, has %s" % (source, expected, current)
424         print "-------------------------------------------------------------------------"
425
426 ################################################################################
427
428 def main():
429     global Cnf
430
431     #####################################
432     #### This can run within sudo !! ####
433     #####################################
434     init()
435
436     # Check if there is a file defined (and existant)
437     transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
438     if transpath == "":
439         utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
440         sys.exit(1)
441     if not os.path.exists(transpath):
442         utils.warn("ReleaseTransitions file, %s, not found." %
443                           (Cnf["Dinstall::Reject::ReleaseTransitions"]))
444         sys.exit(1)
445     # Also check if our temp directory is defined and existant
446     temppath = Cnf.get("Transitions::TempPath", "")
447     if temppath == "":
448         utils.warn("Transitions::TempPath not defined")
449         sys.exit(1)
450     if not os.path.exists(temppath):
451         utils.warn("Temporary path %s not found." %
452                           (Cnf["Transitions::TempPath"]))
453         sys.exit(1)
454
455     if Options["import"]:
456         try:
457             write_transitions_from_file(Options["import"])
458         except TransitionsError, m:
459             print m
460             sys.exit(2)
461         sys.exit(0)
462     ##############################################
463     #### Up to here it can run within sudo !! ####
464     ##############################################
465
466     # Parse the yaml file
467     transitions = load_transitions(transpath)
468     if transitions == None:
469         # Something very broken with the transitions, exit
470         utils.warn("Could not parse existing transitions file. Aborting.")
471         sys.exit(2)
472
473     if Options["edit"]:
474         # Let's edit the transitions file
475         edit_transitions()
476     elif Options["check"]:
477         # Check and remove outdated transitions
478         check_transitions(transitions)
479     else:
480         # Output information about the currently defined transitions.
481         print "Currently defined transitions:"
482         transition_info(transitions)
483
484     sys.exit(0)
485
486 ################################################################################
487
488 if __name__ == '__main__':
489     main()