1 # vim: set et sw=4 sts=4 encoding=utf-8 foldmethod=marker:
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
16 log = logging.getLogger('sercom.tester')
18 error_interno = _(u'\n**Error interno al preparar la entrega.**')
20 class UserInfo(object): #{{{
21 def __init__(self, user):
23 info = pwd.getpwnam(user)
25 info = pwd.get(int(user))
32 self.group = grp.getgrgid(self.gid)[0]
35 user_info = UserInfo(config.get('sercom.tester.user', 65534))
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)
44 The arguments are the same as for the Popen constructor. Example:
46 check_call(["ls", "-l"])
48 retcode = sp.call(*popenargs, **kwargs)
49 cmd = kwargs.get("args")
53 raise sp.CalledProcessError(retcode, cmd)
55 sp.check_call = check_call
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
68 return ("Command '%s' returned non-zero exit status %d"
69 % (self.cmd, self.returncode))
70 sp.CalledProcessError = CalledProcessError
73 class Error(StandardError): pass
75 class ExecutionFailure(Error, RuntimeError): #{{{
76 def __init__(self, comando, tarea=None, caso_de_prueba=None):
77 self.comando = comando
79 self.caso_de_prueba = caso_de_prueba
82 class RsyncError(Error, EnvironmentError): pass
86 def unzip(bytes, default_dst='.', specific_dst=dict()): # {{{
87 u"""Descomprime un buffer de datos en formato ZIP.
88 Los archivos se descomprimen en default_dst a menos que exista una entrada
89 en specific_dst cuya clave sea el nombre de archivo a descomprimir, en
90 cuyo caso, se descomprime usando como destino el valor de dicha clave.
92 log.debug(_(u'Intentando descomprimir'))
95 zfile = ZipFile(StringIO(bytes), 'r')
96 for f in zfile.namelist():
97 dst = join(specific_dst.get(f, default_dst), f)
98 if f.endswith(os.sep):
99 log.debug(_(u'Creando directorio "%s" en "%s"'), f, dst)
102 log.debug(_(u'Descomprimiendo archivo "%s" en "%s"'), f, dst)
103 file(dst, 'w').write(zfile.read(f))
107 class SecureProcess(object): #{{{
109 max_tiempo_cpu = 120,
112 max_cant_archivos = 5,
113 max_cant_procesos = 0,
114 max_locks_memoria = 0,
116 uid = config.get('sercom.tester.chroot.user', 65534)
118 # XXX probar! make de un solo archivo lleva nproc=100 y nofile=15
119 def __init__(self, comando, chroot, cwd, close_stdin=False,
120 close_stdout=False, close_stderr=False):
121 self.comando = comando
124 self.close_stdin = close_stdin
125 self.close_stdout = close_stdout
126 self.close_stderr = close_stderr
127 log.debug(_(u'Proceso segurizado: chroot=%s, cwd=%s, user=%s, cpu=%s, '
128 u'as=%sMiB, fsize=%sMiB, nofile=%s, nproc=%s, memlock=%s'),
129 self.chroot, self.cwd, self.uid, self.max_tiempo_cpu,
130 self.max_memoria, self.max_tam_archivo, self.max_cant_archivos,
131 self.max_cant_procesos, self.max_locks_memoria)
132 def __getattr__(self, name):
133 if getattr(self.comando, name) is not None:
134 return getattr(self.comando, name)
135 return config.get('sercom.tester.limits.' + name, self.default[name])
137 x2 = lambda x: (x, x)
140 if self.close_stdout:
142 if self.close_stderr:
144 os.chroot(self.chroot)
146 uinfo = UserInfo(self.uid)
148 os.setuid(uinfo.uid) # Somos mortales irreversiblemente
149 rsrc.setrlimit(rsrc.RLIMIT_CPU, x2(self.max_tiempo_cpu))
150 rsrc.setrlimit(rsrc.RLIMIT_AS, x2(self.max_memoria*self.MB))
151 rsrc.setrlimit(rsrc.RLIMIT_FSIZE, x2(self.max_tam_archivo*self.MB)) # XXX calcular en base a archivos esperados?
152 rsrc.setrlimit(rsrc.RLIMIT_NOFILE, x2(self.max_cant_archivos)) #XXX Obtener de archivos esperados?
153 rsrc.setrlimit(rsrc.RLIMIT_NPROC, x2(self.max_cant_procesos))
154 rsrc.setrlimit(rsrc.RLIMIT_MEMLOCK, x2(self.max_locks_memoria))
155 rsrc.setrlimit(rsrc.RLIMIT_CORE, x2(0))
156 # Tratamos de forzar un sync para que entre al sleep del padre FIXME
161 class Tester(object): #{{{
163 def __init__(self, name, path, home, queue): #{{{ y properties
168 # Ahora somos mortales (oid mortales)
169 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
170 user_info.user, user_info.group, user_info.uid, user_info.gid)
171 os.setegid(user_info.gid)
172 os.seteuid(user_info.uid)
175 def build_path(self):
176 return join(self.chroot, self.home, 'build')
180 return join(self.chroot, self.home, 'test')
184 return join(self.path, 'chroot_' + self.name)
187 def orig_chroot(self):
188 return join(self.path, 'chroot')
192 entrega_id = self.queue.get() # blocking
193 while entrega_id is not None:
194 entrega = Entrega.get(entrega_id)
195 log.debug(_(u'Nueva entrega para probar en tester %s: %s'),
198 log.debug(_(u'Fin de pruebas de: %s'), entrega)
199 entrega_id = self.queue.get() # blocking
202 def test(self, entrega): #{{{
203 log.debug(_(u'Tester.test(entrega=%s)'), entrega)
204 entrega.inicio_tareas = datetime.now()
207 self.setup_chroot(entrega)
208 self.ejecutar_tareas_fuente(entrega)
209 self.ejecutar_tareas_prueba(entrega)
210 self.clean_chroot(entrega)
211 except ExecutionFailure, e:
212 entrega.correcta = False
213 log.info(_(u'Entrega incorrecta: %s'), entrega)
215 if isinstance(e, SystemExit): raise
216 entrega.observaciones += error_interno
217 log.exception(_('Hubo una excepcion inesperada')) # FIXME encoding
219 entrega.observaciones += error_interno
220 log.exception(_('Hubo una excepcion inesperada desconocida')) # FIXME encoding
222 entrega.correcta = True
223 log.debug(_(u'Entrega correcta: %s'), entrega)
225 entrega.fin_tareas = datetime.now()
228 def setup_chroot(self, entrega): #{{{ y clean_chroot()
229 log.debug(_(u'Tester.setup_chroot(entrega=%s)'), entrega.shortrepr())
230 rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
231 '--archive', '--acls', '--delete-during', '--force', # TODO config
232 join(self.orig_chroot, ''), self.chroot)
233 log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
234 os.seteuid(0) # Dios! (para chroot)
239 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
240 user_info.user, user_info.group, user_info.uid, user_info.gid)
241 os.setegid(user_info.gid) # Mortal de nuevo
242 os.seteuid(user_info.uid)
243 unzip(entrega.archivos, self.build_path)
245 def clean_chroot(self, entrega):
246 log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
247 pass # Se limpia con el próximo rsync
250 def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
251 log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
253 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
254 if isinstance(t, TareaFuente)]
256 tarea.ejecutar(self.build_path, entrega)
258 def ejecutar_tareas_prueba(self, entrega):
259 log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
261 for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
262 caso.ejecutar(self.test_path, entrega)
267 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
268 log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
270 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
271 if isinstance(t, TareaPrueba)]
272 prueba = entrega.add_prueba(self)
276 tarea.ejecutar(path, prueba)
277 except ExecutionFailure, e:
279 if self.rechazar_si_falla:
280 entrega.exito = False
281 if self.terminar_si_falla:
282 raise ExecutionFailure(e.comando, e.tarea, self)
286 prueba.fin = datetime.now()
287 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
290 def ejecutar_tarea_fuente(self, path, entrega): #{{{
291 log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
294 for cmd in self.comandos:
295 cmd.ejecutar(path, entrega)
296 except ExecutionFailure, e:
297 if self.rechazar_si_falla:
298 entrega.exito = False
299 if self.terminar_si_falla:
300 raise ExecutionFailure(e.comando, self)
301 TareaFuente.ejecutar = ejecutar_tarea_fuente
304 def ejecutar_tarea_prueba(self, path, prueba): #{{{
305 log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
308 for cmd in self.comandos:
309 cmd.ejecutar(path, prueba)
310 except ExecutionFailure, e:
311 if self.rechazar_si_falla:
313 if self.terminar_si_falla:
314 raise ExecutionFailure(e.comando, self)
315 TareaPrueba.ejecutar = ejecutar_tarea_prueba
318 def ejecutar_comando_fuente(self, path, entrega): #{{{
319 log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
321 comando_ejecutado = entrega.add_comando_ejecutado(self)
322 unzip(self.archivos_entrada, path, # TODO try/except
323 {self.STDIN: '/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id}) # TODO /var/run/sercom
327 preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build')
329 if os.path.exists('/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id): # TODO
330 options['stdin'] = file('/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id, 'r') # TODO
332 options['preexec_fn'].close_stdin = True
333 a_guardar = set(self.archivos_a_guardar)
334 if self.STDOUTERR in a_guardar:
335 options['stdout'] = file('/tmp/sercom.tester.%s.stdouterr'
336 % comando_ejecutado.id, 'w') #TODO /var/run/sercom?
337 options['stderr'] = sp.STDOUT
339 if self.STDOUT in a_guardar:
340 options['stdout'] = file('/tmp/sercom.tester.%s.stdout'
341 % comando_ejecutado.id, 'w') #TODO /run/lib/sercom?
343 options['preexec_fn'].close_stdout = True
344 if self.STDERR in a_guardar:
345 options['stderr'] = file('/tmp/sercom.tester.%s.stderr'
346 % comando_ejecutado.id, 'w') #TODO /var/run/sercom?
348 options['preexec_fn'].close_stderr = True
349 log.debug(_(u'Ejecutando como root: %s'), self.comando)
350 os.seteuid(0) # Dios! (para chroot)
354 proc = sp.Popen(self.comando, **options)
356 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
357 user_info.user, user_info.group, user_info.uid, user_info.gid)
358 os.setegid(user_info.gid) # Mortal de nuevo
359 os.seteuid(user_info.uid)
361 if hasattr(e, 'child_traceback'):
362 log.error(_(u'Error en el hijo: %s'), e.child_traceback)
364 proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
365 if self.retorno != self.RET_ANY:
366 if self.retorno == self.RET_FAIL:
367 if proc.returncode == 0:
368 if self.rechazar_si_falla:
369 entrega.correcta = False
370 comando_ejecutado.exito = False
371 comando_ejecutado.observaciones += _(u'Se esperaba que el '
372 u'programa termine con un error (código de retorno '
373 u'distinto de 0) pero terminó bien (código de retorno '
375 log.debug(_(u'Se esperaba que el programa termine '
376 u'con un error (código de retorno distinto de 0) pero '
377 u'terminó bien (código de retorno 0).\n'))
378 elif self.retorno != proc.returncode:
379 if self.rechazar_si_falla:
380 entrega.correcta = False
381 comando_ejecutado.exito = False
382 if proc.returncode < 0:
383 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
384 u'con un código de retorno %s pero se obtuvo una señal %s '
385 u'(%s).\n') % (self.retorno, -proc.returncode,
386 -proc.returncode) # TODO poner con texto
387 log.debug(_(u'Se esperaba terminar con un código '
388 u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
389 self.retorno, -proc.returncode, -proc.returncode)
391 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
392 u'con un código de retorno %s pero se obtuvo %s.\n') \
393 % (self.retorno, proc.returncode)
394 log.debug(_(u'Se esperaba terminar con un código de retorno '
395 u'%s pero se obtuvo %s.\n'), self.retorno, proc.returncode)
396 if comando_ejecutado.exito is None:
397 log.debug(_(u'Código de retorno OK'))
398 comando_ejecutado.fin = datetime.now()
401 zip = ZipFile(buffer, 'w')
402 # Guardamos stdout/stderr
403 if self.STDOUTERR in a_guardar:
404 a_guardar.remove(self.STDOUTERR)
405 zip.write('/tmp/sercom.tester.%s.stdouterr'
406 % comando_ejecutado.id, self.STDOUTERR)
408 if self.STDOUT in a_guardar:
409 a_guardar.remove(self.STDOUT)
410 zip.write('/tmp/sercom.tester.%s.stdout'
411 % comando_ejecutado.id, self.STDOUT)
412 if self.STDERR in a_guardar:
413 a_guardar.remove(self.STDERR)
414 zip.write('/tmp/sercom.tester.%s.stderr'
415 % comando_ejecutado.id, self.STDERR)
418 if not os.path.exists(join(path, f)):
419 if self.rechazar_si_falla:
420 entrega.correcta = False
421 comando_ejecutado.exito = False
422 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
423 u'"%s" pero no fue encontrado') % f
424 log.debug(_(u'Se esperaba un archivo "%s" pero no fue '
427 zip.write(join(path, f), f)
429 comando_ejecutado.archivos_guardados = buffer.getvalue()
430 if comando_ejecutado.exito is None:
431 comando_ejecutado.exito = True
432 elif self.terminar_si_falla:
433 raise ExecutionFailure(self)
435 # if no_anda_ejecucion: # TODO
436 # comando_ejecutado.exito = False
437 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
438 # if self.rechazar_si_falla:
439 # entrega.exito = False
440 # if self.terminar_si_falla: # TODO
441 # raise ExecutionFailure(self)
442 # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
443 # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
444 # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
445 # for archivo in self.archivos_salida:
446 # pass # TODO hacer diff
447 # if archivos_mal: # TODO
448 # comando_ejecutado.exito = False
449 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
450 # if self.rechazar_si_falla:
451 # entrega.exito = False
452 # if self.terminar_si_falla: # TODO
453 # raise ExecutionFailure(self)
455 # comando_ejecutado.exito = True
456 # comando_ejecutado.observaciones += 'xxx OK' # TODO
457 ComandoFuente.ejecutar = ejecutar_comando_fuente
460 def ejecutar_comando_prueba(self, path, prueba): #{{{
461 log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
465 unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
466 unzip(self.archivos_entrada, path) # TODO try/except
467 comando_ejecutado = prueba.add_comando_ejecutado(self)
468 # TODO ejecutar en chroot (path)
469 comando_ejecutado.fin = datetime.now()
470 # if no_anda_ejecucion: # TODO
471 # comando_ejecutado.exito = False
472 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
473 # if self.rechazar_si_falla:
474 # entrega.exito = False
475 # if self.terminar_si_falla: # TODO
476 # raise ExecutionFailure(self) # TODO info de error
477 # for archivo in self.archivos_salida:
478 # pass # TODO hacer diff
479 # if archivos_mal: # TODO
480 # comando_ejecutado.exito = False
481 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
482 # if self.rechazar_si_falla:
483 # entrega.exito = False
484 # if self.terminar_si_falla: # TODO
485 # raise ExecutionFailure(comando=self) # TODO info de error
487 # comando_ejecutado.exito = True
488 # comando_ejecutado.observaciones += 'xxx OK' # TODO
489 comando_ejecutado.exito = True
490 comando_ejecutado.observaciones += 'xxx OK' # TODO
491 ComandoPrueba.ejecutar = ejecutar_comando_prueba