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