]> git.llucax.com Git - software/sercom.git/blob - sercom/tester.py
Capturar stdout/stderr/stdouterr y guardar en archivos_guardados en formato zip.
[software/sercom.git] / sercom / tester.py
1 # vim: set et sw=4 sts=4 encoding=utf-8 foldmethod=marker:
2
3 from sercom.model import Entrega, CasoDePrueba
4 from sercom.model import TareaFuente, TareaPrueba, ComandoFuente, ComandoPrueba
5 from zipfile import ZipFile, BadZipfile
6 from cStringIO import StringIO
7 from shutil import rmtree
8 from datetime import datetime
9 from os.path import join
10 from turbogears import config
11 import subprocess as sp
12 import os, sys, pwd, grp
13 import resource as rsrc
14 import logging
15
16 log = logging.getLogger('sercom.tester')
17
18 error_interno = _(u'\n**Error interno al preparar la entrega.**')
19
20 class UserInfo(object): #{{{
21     def __init__(self, user):
22         try:
23             info = pwd.getpwnam(user)
24         except:
25             info = pwd.get(int(user))
26         self.user = info[0]
27         self.uid = info[2]
28         self.gid = info[3]
29         self.name = info[4]
30         self.home = info[5]
31         self.shell = info[6]
32         self.group = grp.getgrgid(self.gid)[0]
33 #}}}
34
35 user_info = UserInfo(config.get('sercom.tester.user', 65534))
36
37 def check_call(*popenargs, **kwargs): #{{{ XXX Python 2.5 forward-compatibility
38     """Run command with arguments.  Wait for command to complete.  If
39     the exit code was zero then return, otherwise raise
40     CalledProcessError.  The CalledProcessError object will have the
41     return code in the returncode attribute.
42     ret = call(*popenargs, **kwargs)
43
44     The arguments are the same as for the Popen constructor.  Example:
45
46     check_call(["ls", "-l"])
47     """
48     retcode = sp.call(*popenargs, **kwargs)
49     cmd = kwargs.get("args")
50     if cmd is None:
51         cmd = popenargs[0]
52     if retcode:
53         raise sp.CalledProcessError(retcode, cmd)
54     return retcode
55 sp.check_call = check_call
56 #}}}
57
58 #{{{ Excepciones
59
60 class CalledProcessError(Exception): #{{{ XXX Python 2.5 forward-compatibility
61     """This exception is raised when a process run by check_call() returns
62     a non-zero exit status.  The exit status will be stored in the
63     returncode attribute."""
64     def __init__(self, returncode, cmd):
65         self.returncode = returncode
66         self.cmd = cmd
67     def __str__(self):
68         return ("Command '%s' returned non-zero exit status %d"
69             % (self.cmd, self.returncode))
70 sp.CalledProcessError = CalledProcessError
71 #}}}
72
73 class Error(StandardError): pass
74
75 class ExecutionFailure(Error, RuntimeError): pass
76
77 class RsyncError(Error, EnvironmentError): pass
78
79 #}}}
80
81 def unzip(bytes, dst): # {{{
82     log.debug(_(u'Intentando descomprimir en %s'), dst)
83     if bytes is None:
84         return
85     zfile = ZipFile(StringIO(bytes), 'r')
86     for f in zfile.namelist():
87         if f.endswith(os.sep):
88             log.debug(_(u'Creando directorio %s'), f)
89             os.mkdir(join(dst, f))
90         else:
91             log.debug(_(u'Descomprimiendo archivo %s'), f)
92             file(join(dst, f), 'w').write(zfile.read(f))
93 #}}}
94
95 class SecureProcess(object): #{{{
96     default = dict(
97         max_tiempo_cpu      = 120,
98         max_memoria         = 16,
99         max_tam_archivo     = 5,
100         max_cant_archivos   = 5,
101         max_cant_procesos   = 0,
102         max_locks_memoria   = 0,
103     )
104     uid = config.get('sercom.tester.chroot.user', 65534)
105     MB = 1048576
106     # XXX probar! make de un solo archivo lleva nproc=100 y nofile=15
107     def __init__(self, comando, chroot, cwd):
108         self.comando = comando
109         self.chroot = chroot
110         self.cwd = cwd
111         log.debug('Proceso segurizado: chroot=%s, cwd=%s, user=%s, cpu=%s, '
112             'as=%sMiB, fsize=%sMiB, nofile=%s, nproc=%s, memlock=%s',
113             self.chroot, self.cwd, self.uid, self.max_tiempo_cpu,
114             self.max_memoria, self.max_tam_archivo, self.max_cant_archivos,
115             self.max_cant_procesos, self.max_locks_memoria)
116     def __getattr__(self, name):
117         if getattr(self.comando, name) is not None:
118             return getattr(self.comando, name)
119         return config.get('sercom.tester.limits.' + name, self.default[name])
120     def __call__(self):
121         x2 = lambda x: (x, x)
122         os.chroot(self.chroot)
123         os.chdir(self.cwd)
124         uinfo = UserInfo(self.uid)
125         os.setgid(uinfo.gid)
126         os.setuid(uinfo.uid) # Somos mortales irreversiblemente
127         rsrc.setrlimit(rsrc.RLIMIT_CPU, x2(self.max_tiempo_cpu))
128         rsrc.setrlimit(rsrc.RLIMIT_AS, x2(self.max_memoria*self.MB))
129         rsrc.setrlimit(rsrc.RLIMIT_FSIZE, x2(self.max_tam_archivo*self.MB)) # XXX calcular en base a archivos esperados?
130         rsrc.setrlimit(rsrc.RLIMIT_NOFILE, x2(self.max_cant_archivos)) #XXX Obtener de archivos esperados?
131         rsrc.setrlimit(rsrc.RLIMIT_NPROC, x2(self.max_cant_procesos))
132         rsrc.setrlimit(rsrc.RLIMIT_MEMLOCK, x2(self.max_locks_memoria))
133         rsrc.setrlimit(rsrc.RLIMIT_CORE, x2(0))
134         # Tratamos de forzar un sync para que entre al sleep del padre FIXME
135         import time
136         time.sleep(0)
137 #}}}
138
139 class Tester(object): #{{{
140
141     def __init__(self, name, path, home, queue): #{{{ y properties
142         self.name = name
143         self.path = path
144         self.home = home
145         self.queue = queue
146         # Ahora somos mortales (oid mortales)
147         log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
148             user_info.user, user_info.group, user_info.uid, user_info.gid)
149         os.setegid(user_info.gid)
150         os.seteuid(user_info.uid)
151
152     @property
153     def build_path(self):
154         return join(self.chroot, self.home, 'build')
155
156     @property
157     def test_path(self):
158         return join(self.chroot, self.home, 'test')
159
160     @property
161     def chroot(self):
162         return join(self.path, 'chroot_' + self.name)
163
164     @property
165     def orig_chroot(self):
166         return join(self.path, 'chroot')
167     #}}}
168
169     def run(self): #{{{
170         entrega_id = self.queue.get() # blocking
171         while entrega_id is not None:
172             entrega = Entrega.get(entrega_id)
173             log.debug(_(u'Nueva entrega para probar en tester %s: %s'),
174                 self.name, entrega)
175             self.test(entrega)
176             log.debug(_(u'Fin de pruebas de: %s'), entrega)
177             entrega_id = self.queue.get() # blocking
178     #}}}
179
180     def test(self, entrega): #{{{
181         log.debug(_(u'Tester.test(entrega=%s)'), entrega)
182         entrega.inicio_tareas = datetime.now()
183         try:
184             try:
185                 self.setup_chroot(entrega)
186                 self.ejecutar_tareas_fuente(entrega)
187                 self.ejecutar_tareas_prueba(entrega)
188                 self.clean_chroot(entrega)
189             except ExecutionFailure, e:
190                 entrega.correcta = False
191                 log.info(_(u'Entrega incorrecta: %s'), entrega)
192             except Exception, e:
193                 if isinstance(e, SystemExit): raise
194                 entrega.observaciones += error_interno
195                 log.exception(_(u'Hubo una excepción inesperada: %s'), e)
196             except:
197                 entrega.observaciones += error_interno
198                 log.exception(_(u'Hubo una excepción inesperada desconocida'))
199             else:
200                 entrega.correcta = True
201                 log.debug(_(u'Entrega correcta: %s'), entrega)
202         finally:
203             entrega.fin_tareas = datetime.now()
204     #}}}
205
206     def setup_chroot(self, entrega): #{{{ y clean_chroot()
207         log.debug(_(u'Tester.setup_chroot(entrega=%s)'), entrega.shortrepr())
208         rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
209             '--archive', '--acls', '--delete-during', '--force', # TODO config
210             join(self.orig_chroot, ''), self.chroot)
211         log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
212         os.seteuid(0) # Dios! (para chroot)
213         os.setegid(0)
214         try:
215             sp.check_call(rsync)
216         finally:
217             log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
218                 user_info.user, user_info.group, user_info.uid, user_info.gid)
219             os.setegid(user_info.gid) # Mortal de nuevo
220             os.seteuid(user_info.uid)
221         unzip(entrega.archivos, self.build_path)
222
223     def clean_chroot(self, entrega):
224         log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
225         pass # Se limpia con el próximo rsync
226     #}}}
227
228     def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
229         log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
230             entrega.shortrepr())
231         tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
232                     if isinstance(t, TareaFuente)]
233         for tarea in tareas:
234             tarea.ejecutar(self.build_path, entrega)
235
236     def ejecutar_tareas_prueba(self, entrega):
237         log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
238             entrega.shortrepr())
239         for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
240             caso.ejecutar(self.test_path, entrega)
241     #}}}
242
243 #}}}
244
245 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
246     log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
247         entrega.shortrepr())
248     tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
249                 if isinstance(t, TareaPrueba)]
250     prueba = entrega.add_prueba(self)
251     try:
252         try:
253             for tarea in tareas:
254                 tarea.ejecutar(path, prueba)
255         except ExecutionFailure, e:
256             prueba.exito = False
257             if self.rechazar_si_falla:
258                 entrega.exito = False
259             if self.terminar_si_falla:
260                 raise ExecutionError(e.comando, e.tarea, prueba)
261         else:
262             prueba.exito = True
263     finally:
264         prueba.fin = datetime.now()
265 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
266 #}}}
267
268 def ejecutar_tarea_fuente(self, path, entrega): #{{{
269     log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
270         entrega.shortrepr())
271     try:
272         for cmd in self.comandos:
273             cmd.ejecutar(path, entrega)
274     except ExecutionFailure, e:
275         if self.rechazar_si_falla:
276             entrega.exito = False
277         if self.terminar_si_falla:
278             raise ExecutionError(e.comando, tarea)
279 TareaFuente.ejecutar = ejecutar_tarea_fuente
280 #}}}
281
282 def ejecutar_tarea_prueba(self, path, prueba): #{{{
283     log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
284         prueba.shortrepr())
285     try:
286         for cmd in self.comandos:
287             cmd.ejecutar(path, prueba)
288     except ExecutionFailure, e:
289         if self.rechazar_si_falla:
290             prueba.exito = False
291         if self.terminar_si_falla:
292             raise ExecutionError(e.comando, tarea)
293 TareaPrueba.ejecutar = ejecutar_tarea_prueba
294 #}}}
295
296 def ejecutar_comando_fuente(self, path, entrega): #{{{
297     log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
298         entrega.shortrepr())
299     unzip(self.archivos_entrada, path) # TODO try/except
300     comando_ejecutado = entrega.add_comando_ejecutado(self)
301     # Abro archivos para fds básicos (FIXME)
302     options = dict(
303         close_fds=True,
304         stdin=None,
305         shell=True,
306         preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build')
307     )
308     if self.guardar_stdouterr:
309         options['stdout'] = file('/tmp/sercom.tester.%s.stdouterr'
310             % comando_ejecutado.id, 'w') #TODO /var/lib/sercom?
311         options['stderr'] = sp.STDOUT
312     else:
313         if self.guardar_stdout:
314             options['stdout'] = file('/tmp/sercom.tester.%s.stdout'
315                 % comando_ejecutado.id, 'w') #TODO /var/lib/sercom?
316         if self.guardar_stderr:
317             options['stderr'] = file('/tmp/sercom.tester.%s.stderr'
318                 % comando_ejecutado.id, 'w') #TODO /var/lib/sercom?
319     log.debug(_(u'Ejecutando como root: %s'), self.comando)
320     os.seteuid(0) # Dios! (para chroot)
321     os.setegid(0)
322     try:
323         try:
324             proc = sp.Popen(self.comando, **options)
325         finally:
326             log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
327                 user_info.user, user_info.group, user_info.uid, user_info.gid)
328             os.setegid(user_info.gid) # Mortal de nuevo
329             os.seteuid(user_info.uid)
330     except Exception, e:
331         if hasattr(e, 'child_traceback'):
332             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
333         raise
334     proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
335     comando_ejecutado.fin = datetime.now()
336     buffer = StringIO()
337     zip = ZipFile(buffer, 'w')
338     if self.guardar_stdouterr:
339         zip.write('/tmp/sercom.tester.%s.stdouterr'
340             % comando_ejecutado.id, '__stdouterr__')
341     else:
342         if self.guardar_stdout:
343             azipwrite('/tmp/sercom.tester.%s.stdout'
344                 % comando_ejecutado.id, '__stdout__')
345         if self.guardar_stderr:
346             zip.write('/tmp/sercom.tester.%s.stderr'
347                 % comando_ejecutado.id, '__stderr__')
348     zip.close()
349     comando_ejecutado.archivos_guardados = buffer.getvalue()
350
351 #    if no_anda_ejecucion: # TODO
352 #        comando_ejecutado.exito = False
353 #        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
354 #        if self.rechazar_si_falla:
355 #            entrega.exito = False
356 #        if self.terminar_si_falla: # TODO
357 #            raise ExecutionFailure(self)
358     # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
359     # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
360     # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
361 #    for archivo in self.archivos_salida:
362 #        pass # TODO hacer diff
363 #    if archivos_mal: # TODO
364 #        comando_ejecutado.exito = False
365 #        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
366 #        if self.rechazar_si_falla:
367 #            entrega.exito = False
368 #        if self.terminar_si_falla: # TODO
369 #            raise ExecutionFailure(self)
370 #    else:
371 #        comando_ejecutado.exito = True
372 #        comando_ejecutado.observaciones += 'xxx OK' # TODO
373     comando_ejecutado.exito = True
374     comando_ejecutado.observaciones += 'xxx OK' # TODO
375 ComandoFuente.ejecutar = ejecutar_comando_fuente
376 #}}}
377
378 def ejecutar_comando_prueba(self, path, prueba): #{{{
379     log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
380         prueba.shortrepr())
381     rmtree(path)
382     os.mkdir(path)
383     unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
384     unzip(self.archivos_entrada, path) # TODO try/except
385     comando_ejecutado = prueba.add_comando_ejecutado(self)
386     # TODO ejecutar en chroot (path)
387     comando_ejecutado.fin = datetime.now()
388 #    if no_anda_ejecucion: # TODO
389 #        comando_ejecutado.exito = False
390 #        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
391 #        if self.rechazar_si_falla:
392 #            entrega.exito = False
393 #        if self.terminar_si_falla: # TODO
394 #            raise ExecutionFailure(self) # TODO info de error
395 #    for archivo in self.archivos_salida:
396 #        pass # TODO hacer diff
397 #    if archivos_mal: # TODO
398 #        comando_ejecutado.exito = False
399 #        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
400 #        if self.rechazar_si_falla:
401 #            entrega.exito = False
402 #        if self.terminar_si_falla: # TODO
403 #            raise ExecutionFailure(comando=self) # TODO info de error
404 #    else:
405 #        comando_ejecutado.exito = True
406 #        comando_ejecutado.observaciones += 'xxx OK' # TODO
407     comando_ejecutado.exito = True
408     comando_ejecutado.observaciones += 'xxx OK' # TODO
409 ComandoPrueba.ejecutar = ejecutar_comando_prueba
410 #}}}
411