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