]> git.decadent.org.uk Git - dak.git/blob - dak/transitions.py
d8845a0bfe153b9aea560f681a48613b75e8e68d
[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 def load_transitions(trans_file):
94     # Parse the yaml file
95     sourcefile = file(trans_file, 'r')
96     sourcecontent = sourcefile.read()
97     try:
98         trans = syck.load(sourcecontent)
99     except syck.error, msg:
100         # Someone fucked it up
101         print "ERROR: %s" % (msg)
102         return None
103     # could do further validation here
104     return trans
105
106 ################################################################################
107
108 def lock_file(file):
109     for retry in range(10):
110         lock_fd = os.open(file, os.O_RDWR | os.O_CREAT)
111         try:
112             fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
113             return lock_fd
114         except OSError, e:
115             if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EEXIST':
116                 print "Unable to get lock for %s (try %d of 10)" % \
117                         (file, retry+1)
118                 time.sleep(60)
119             else:
120                 raise
121
122     daklib.utils.fubar("Couldn't obtain lock for %s." % (lockfile))
123
124 ################################################################################
125
126 def write_transitions(from_trans):
127     """Update the active transitions file safely.
128        This function takes a parsed input file (which avoids invalid
129        files or files that may be be modified while the function is
130        active), and ensure the transitions file is updated atomically
131        to avoid locks."""
132
133     trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
134     trans_temp = trans_file + ".tmp"
135   
136     trans_lock = lock_file(trans_file)
137     temp_lock  = lock_file(trans_temp)
138
139     destfile = file(trans_temp, 'w')
140     syck.dump(from_trans, destfile)
141     destfile.close()
142
143     os.rename(trans_temp, trans_file)
144     os.close(temp_lock)
145     os.close(trans_lock)
146
147 ################################################################################
148
149 class ParseException(Exception):
150     pass
151
152 def write_transitions_from_file(from_file):
153     """We have a file we think is valid; if we're using sudo, we invoke it
154        here, otherwise we just parse the file and call write_transitions"""
155
156     if Options["sudo"]:
157         os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H", 
158               "/usr/local/bin/dak", "transitions", "--import", from_file)
159     else:
160         trans = load_transitions(from_file)
161         if trans is None:
162             raise ParseException, "Unparsable transitions file %s" % (file)
163         write_transitions(trans)
164
165 ################################################################################
166
167 def temp_transitions_file(transitions):
168     # NB: file is unlinked by caller, but fd is never actually closed.
169     # We need the chmod, as the file is (most possibly) copied from a
170     # sudo-ed script and would be unreadable if it has default mkstemp mode
171     
172     (fd, path) = tempfile.mkstemp("","transitions")
173     os.chmod(path, 0644)
174     f = open(path, "w")
175     syck.dump(transitions, f)
176     return path
177
178 ################################################################################
179
180 def edit_transitions():
181     trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
182     edit_file = temp_transitions_file(load_transitions(trans_file))
183
184     editor = os.environ.get("EDITOR", "vi")
185
186     while True:
187         result = os.system("%s %s" % (editor, edit_file))
188         if result != 0:
189             os.unlink(edit_file)
190             daklib.utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file))
191     
192         # Now try to load the new file
193         test = load_transitions(edit_file)
194
195         if test == None:
196             # Edit is broken
197             print "Edit was unparsable."
198             prompt = "[E]dit again, Drop changes?"
199             default = "E"
200         else:
201             print "Edit looks okay.\n"
202             print "The following transitions are defined:"
203             print "------------------------------------------------------------------------"
204             transition_info(test)
205
206             prompt = "[S]ave, Edit again, Drop changes?"
207             default = "S"
208
209         answer = "XXX"
210         while prompt.find(answer) == -1:
211             answer = daklib.utils.our_raw_input(prompt)
212             if answer == "":
213                 answer = default
214             answer = answer[:1].upper()
215
216         if answer == 'E':
217             continue
218         elif answer == 'D':
219             os.unlink(edit_file)
220             print "OK, discarding changes"
221             sys.exit(0)
222         elif answer == 'S':
223             # Ready to save
224             break
225         else:
226             print "You pressed something you shouldn't have :("
227             sys.exit(1)
228
229     # We seem to be done and also have a working file. Copy over.
230     write_transitions_from_file(edit_file)
231     os.unlink(edit_file)
232
233     print "Transitions file updated."
234
235 ################################################################################
236
237 def check_transitions(transitions):
238     to_dump = 0
239     to_remove = []
240     # Now look through all defined transitions
241     for trans in transitions:
242         t = transitions[trans]
243         source = t["source"]
244         expected = t["new"]
245
246         # Will be None if nothing is in testing.
247         current = daklib.database.get_suite_version(source, "testing")
248
249         print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
250
251         if current == None:
252             # No package in testing
253             print "Transition source %s not in testing, transition still ongoing." % (source)
254         else:
255             compare = apt_pkg.VersionCompare(current, expected)
256             if compare < 0:
257                 # This is still valid, the current version in database is older than
258                 # the new version we wait for
259                 print "This transition is still ongoing, we currently have version %s" % (current)
260             else:
261                 print "REMOVE: This transition is over, the target package reached testing. REMOVE"
262                 print "%s wanted version: %s, has %s" % (source, expected, current)
263                 to_remove.append(trans)
264                 to_dump = 1
265         print "-------------------------------------------------------------------------"
266
267     if to_dump:
268         prompt = "Removing: "
269         for remove in to_remove:
270             prompt += remove
271             prompt += ","
272
273         prompt += " Commit Changes? (y/N)"
274         answer = ""
275
276         if Options["no-action"]:
277             answer="n"
278         else:
279             answer = daklib.utils.our_raw_input(prompt).lower()
280
281         if answer == "":
282             answer = "n"
283
284         if answer == 'n':
285             print "Not committing changes"
286             sys.exit(0)
287         elif answer == 'y':
288             print "Committing"
289             for remove in to_remove:
290                 del transitions[remove]
291     
292             edit_file = temp_transitions_file(transitions)
293             write_transitions_from_file(edit_file)
294
295             print "Done"
296         else:
297             print "WTF are you typing?"
298             sys.exit(0)
299
300 ################################################################################
301
302 def print_info(trans, source, expected, rm, reason, packages):
303         print """Looking at transition: %s
304  Source:      %s
305  New Version: %s
306  Responsible: %s
307  Description: %s
308  Blocked Packages (total: %d): %s
309 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
310         return
311
312 ################################################################################
313
314 def transition_info(transitions):
315     for trans in transitions:
316         t = transitions[trans]
317         source = t["source"]
318         expected = t["new"]
319
320         # Will be None if nothing is in testing.
321         current = daklib.database.get_suite_version(source, "testing")
322
323         print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
324
325         if current == None:
326             # No package in testing
327             print "Transition source %s not in testing, transition still ongoing." % (source)
328         else:
329             compare = apt_pkg.VersionCompare(current, expected)
330             print "Apt compare says: %s" % (compare)
331             if compare < 0:
332                 # This is still valid, the current version in database is older than
333                 # the new version we wait for
334                 print "This transition is still ongoing, we currently have version %s" % (current)
335             else:
336                 print "This transition is over, the target package reached testing, should be removed"
337                 print "%s wanted version: %s, has %s" % (source, expected, current)
338         print "-------------------------------------------------------------------------"
339
340 ################################################################################
341
342 def main():
343     global Cnf
344
345     init()
346     
347     # Check if there is a file defined (and existant)
348     transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
349     if transpath == "":
350         daklib.utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
351         sys.exit(1)
352     if not os.path.exists(transpath):
353         daklib.utils.warn("ReleaseTransitions file, %s, not found." %
354                           (Cnf["Dinstall::Reject::ReleaseTransitions"]))
355         sys.exit(1)
356    
357     if Options["import"]:
358         try:
359             write_transitions_from_file(Options["import"])
360         except ParseException, m:
361             print m
362             sys.exit(2)
363         sys.exit(0)
364
365     # Parse the yaml file
366     transitions = load_transitions(transpath)
367     if transitions == None:
368         # Something very broken with the transitions, exit
369         daklib.utils.warn("Could not parse existing transitions file. Aborting.")
370         sys.exit(2)
371
372     if Options["edit"]:
373         # Let's edit the transitions file
374         edit_transitions()
375     elif Options["check"]:
376         # Check and remove outdated transitions
377         check_transitions(transitions)
378     else:
379         # Output information about the currently defined transitions.
380         print "Currently defined transitions:"
381         transition_info(transitions)
382
383     sys.exit(0)
384     
385 ################################################################################
386
387 if __name__ == '__main__':
388     main()