chiark / gitweb /
adt-virt-xenlvm more or less done; needing testing
[autopkgtest.git] / virt-subproc / VirtSubproc.py
1 # VirtSubproc is part of autopkgtest
2 # autopkgtest is a tool for testing Debian binary packages
3 #
4 # autopkgtest is Copyright (C) 2006 Canonical Ltd.
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., 675 Mass Ave, Cambridge, MA 02139, USA.
19 #
20 # See the file CREDITS for a full list of credits information (often
21 # installed as /usr/share/doc/autopkgtest/CREDITS).
22
23 import __main__
24
25 import sys
26 import os
27 import string
28 import urllib
29 import signal
30 import subprocess
31 import traceback
32
33 debuglevel = None
34 progname = "<VirtSubproc>"
35 devnull_read = file('/dev/null','r')
36 caller = __main__
37
38 class Quit:
39         def __init__(q,ec,m): q.ec = ec; q.m = m
40
41 def debug(m):
42         if not debuglevel: return
43         print >> sys.stderr, progname+": debug:", m
44
45 def bomb(m):
46         raise Quit(12, progname+": failure: %s" % m)
47
48 def ok(): print 'ok'
49
50 def cmdnumargs(c, ce, nargs=0):
51         if len(c) == nargs + 1: return
52         bomb("wrong number of arguments to command `%s'" % ce[0])
53
54 def cmd_capabilities(c, ce):
55         cmdnumargs(c, ce)
56         return caller.hook_capabilities()
57
58 def cmd_quit(c, ce):
59         cmdnumargs(c, ce)
60         raise Quit(0, '')
61
62 def execute_raw(what, instr, *popenargs, **popenargsk):
63         debug(" ++ %s" % string.join(popenargs[0]))
64         sp = subprocess.Popen(*popenargs, **popenargsk)
65         if instr is None: popenargsk['stdin'] = devnull_read
66         (out, err) = sp.communicate(instr)
67         if err: bomb("%s unexpectedly produced stderr output `%s'" %
68                         (what, err))
69         status = sp.wait()
70         return (status, out)
71
72 def execute(cmd_string, cmd_list=[], downp=False, outp=False):
73         cmdl = cmd_string.split()
74
75         if downp: perhaps_down = down
76         else: downp = []
77
78         if outp: stdout = subprocess.PIPE
79         else: stdout = None
80
81         cmd = cmdl + cmd_list
82         if len(perhaps_down): cmd = perhaps_down + [' '.join(cmd)]
83
84         (status, out) = execute_raw(cmdl[0], None, cmd, stdout=stdout)
85
86         if status: bomb("%s%s failed (exit status %d)" %
87                         ((downp and "(down) " or ""), cmdl[0], status))
88
89         if outp and out and out[-1]=='\n': out = out[:-1]
90         return out
91
92 def cmd_open(c, ce):
93         global downtmp
94         cmdnumargs(c, ce)
95         if downtmp: bomb("`open' when already open")
96         downtmp = caller.hook_open()
97         return [downtmp]
98
99 def cmd_close(c, ce):
100         global downtmp
101         cmdnumargs(c, ce)
102         if not downtmp: bomb("`close' when not open")
103         cleanup()
104
105 def cmd_stop(c, ce):
106         global downtmp
107         cmdnumargs(c, ce, 1)
108         if not downtmp: bomb("`stop' when not open")
109         caller.hook_stop()
110         cleanup()
111
112 def down_python_script(gobody, functions=''):
113         # Many things are made much harder by the inability of
114         # dchroot, ssh, et al, to cope without mangling the arguments.
115         # So we run a sub-python on the testbed and feed it a script
116         # on stdin.  The sub-python decodes the arguments.
117
118         script = (      "import urllib\n"
119                         "import os\n"
120                         "def setfd(fd,fnamee,write,mode=0666):\n"
121                         "       fname = urllib.unquote(fnamee)\n"
122                         "       if write: rw = os.O_WRONLY|os.O_CREAT\n"
123                         "       else: rw = os.O_RDONLY\n"
124                         "       nfd = os.open(fname, rw, mode)\n"
125                         "       os.dup2(nfd,fd)\n"
126                         + functions +
127                         "def go():\n" )
128         script += (     "       os.environ['TMPDIR']= urllib.unquote('%s')\n" %
129                                 urllib.quote(downtmp)   )
130         script += (     "       os.chdir(os.environ['TMPDIR'])\n" )
131         script += (     gobody +
132                         "go()\n" )
133
134         debug("+P ...\n"+script)
135
136         scripte = urllib.quote(script)
137         cmdl = down + ['python','-c',
138                 "'import urllib; s = urllib.unquote(%s); exec s'" %
139                         ('"%s"' % scripte)]
140         return cmdl
141
142 def cmd_execute(c, ce):
143         cmdnumargs(c, ce, 5)
144         gobody = "      import sys\n"
145         for ioe in range(3):
146                 gobody += "     setfd(%d,'%s',%d)\n" % (
147                         ioe, ce[ioe+2], ioe>0 )
148         gobody += "     os.chdir(urllib.unquote('" + ce[5] +"'))\n"
149         gobody += "     cmd = '%s'\n" % ce[1]
150         gobody += ("    cmd = cmd.split(',')\n"
151                 "       cmd = map(urllib.unquote, cmd)\n"
152                 "       c0 = cmd[0]\n"
153                 "       if '/' in c0:\n"
154                 "               if not os.access(c0, os.X_OK):\n"
155                 "                       status = os.stat(c0)\n"
156                 "                       mode = status.st_mode | 0111\n"
157                 "                       os.chmod(c0, mode)\n"
158                 "       try: os.execvp(c0, cmd)\n"
159                 "       except OSError, e:\n"
160                 "               print >>sys.stderr, \"%s: %s\" % (\n"
161                 "                       (c0, os.strerror(e.errno)))\n"
162                 "               os._exit(127)\n")
163         cmdl = down_python_script(gobody)
164
165         (status, out) = execute_raw('sub-python', None, cmdl,
166                                 stdin=devnull_read, stderr=subprocess.PIPE)
167         if out: bomb("sub-python unexpected produced stdout"
168                         " visible to us `%s'" % out)
169         return [`status`]
170
171 def copyupdown(c, ce, upp):
172         cmdnumargs(c, ce, 2)
173         isrc = 0
174         idst = 1
175         ilocal = 0 + upp
176         iremote = 1 - upp
177         wh = ce[0]
178         sd = c[1:]
179         sde = ce[1:]
180         if not sd[0] or not sd[1]:
181                 bomb("%s paths must be nonempty" % wh)
182         dirsp = sd[0][-1]=='/'
183         functions = "import errno\n"
184         if dirsp != (sd[1][-1]=='/'):
185                 bomb("% paths must agree about directoryness"
186                         " (presence or absence of trailing /)" % wh)
187         localfd = None
188         deststdout = devnull_read
189         srcstdin = devnull_read
190         preexecfns = [None, None]
191         if not dirsp:
192                 modestr = ''
193                 if upp:
194                         deststdout = file(sd[idst], 'w')
195                 else:
196                         srcstdin = file(sd[isrc], 'r')
197                         status = os.fstat(srcstdin.fileno())
198                         if status.st_mode & 0111: modestr = ',0777'
199                 gobody = "      setfd(%s,'%s',%s%s)\n" % (
200                                         1-upp, sde[iremote], not upp, modestr)
201                 gobody += "     os.execvp('cat', ['cat'])\n"
202                 localcmdl = ['cat']
203         else:
204                 gobody = "      dir = urllib.unquote('%s')\n" % sde[iremote]
205                 if upp:
206                         try: os.mkdir(sd[ilocal])
207                         except OSError, oe:
208                                 if oe.errno != errno.EEXIST: raise
209                 else:
210                         gobody += ("    try: os.mkdir(dir)\n"
211                                 "       except OSError, oe:\n"
212                                 "               if oe.errno != errno.EEXIST: raise\n")
213                 gobody +=( "    os.chdir(dir)\n"
214                         "       tarcmd = 'tar -f -'.split()\n")
215                 localcmdl = 'tar -f -'.split()
216                 taropts = [None, None]
217                 taropts[isrc] = '-c .'
218                 taropts[idst] = '-p -x --no-same-owner'
219                 gobody += "     tarcmd += '%s'.split()\n" % taropts[iremote]
220                 localcmdl += ['-C',sd[ilocal]]
221                 localcmdl += taropts[ilocal].split()
222                 gobody += "     os.execvp('tar', tarcmd)\n";
223
224         downcmdl = down_python_script(gobody, functions)
225
226         if upp: cmdls = (downcmdl, localcmdl)
227         else: cmdls = (localcmdl, downcmdl)
228
229         debug(`["cmdls", `cmdls`]`)
230         debug(`["srcstdin", `srcstdin`, "deststdout", `deststdout`, "devnull_read", devnull_read]`)
231
232         subprocs = [None,None]
233         debug(" +< %s" % string.join(cmdls[0]))
234         subprocs[0] = subprocess.Popen(cmdls[0], stdin=srcstdin,
235                         stdout=subprocess.PIPE, preexec_fn=preexecfns[0])
236         debug(" +> %s" % string.join(cmdls[1]))
237         subprocs[1] = subprocess.Popen(cmdls[1], stdin=subprocs[0].stdout,
238                         stdout=deststdout, preexec_fn=preexecfns[1])
239         for sdn in [1,0]:
240                 status = subprocs[sdn].wait()
241                 if status: bomb("%s %s failed, status %d" %
242                         (wh, ['source','destination'][sdn], status))
243
244 def cmd_copydown(c, ce): copyupdown(c, ce, False)
245 def cmd_copyup(c, ce): copyupdown(c, ce, True)
246
247 def command():
248         sys.stdout.flush()
249         ce = sys.stdin.readline()
250         if not ce: bomb('end of file - caller quit?')
251         ce = ce.rstrip().split()
252         c = map(urllib.unquote, ce)
253         if not c: bomb('empty commands are not permitted')
254         debug('executing '+string.join(ce))
255         try: f = globals()['cmd_'+c[0]]
256         except KeyError: bomb("unknown command `%s'" % ce[0])
257         r = f(c, ce)
258         if not r: r = []
259         r.insert(0, 'ok')
260         ru = map(urllib.quote, r)
261         print string.join(ru)
262
263 def cleanup():
264         global downtmp, cleaning
265         cleaning = True
266         if downtmp: caller.hook_cleanup()
267         cleaning = False
268         downtmp = False
269
270 def error_cleanup():
271         try:
272                 ok = False
273                 try:
274                         cleanup()
275                         ok = True
276                 except Quit, q:
277                         print >> sys.stderr, q.m
278                 except:
279                         print >> sys.stderr, "Unexpected cleanup error:"
280                         traceback.print_exc()
281                         print >> sys.stderr, ''
282                 if not ok:
283                         print >> sys.stderr, ("while cleaning up"
284                                 " because of another error:")
285         except:
286                 pass
287
288 def prepare():
289         global downtmp, cleaning
290         downtmp = None
291         signal_list = [ signal.SIGHUP, signal.SIGTERM,
292                         signal.SIGINT, signal.SIGPIPE ]
293         def sethandlers(f):
294                 for signum in signal_list: signal.signal(signum, f)
295         def handler(sig, *any):
296                 sethandlers(signal.SIG_DFL)
297                 cleanup()
298                 os.kill(os.getpid(), sig)
299         sethandlers(handler)
300
301 def mainloop():
302         try:
303                 while True: command()
304         except Quit, q:
305                 error_cleanup()
306                 if q.m: print >> sys.stderr, q.m
307                 sys.exit(q.ec)
308         except:
309                 error_cleanup()
310                 print >> sys.stderr, "Unexpected error:"
311                 traceback.print_exc()
312                 sys.exit(16)
313
314 def main():
315         debug("down = %s" % string.join(down))
316         ok()
317         prepare()
318         mainloop()