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): pass
77 class RsyncError(Error, EnvironmentError): pass
81 def unzip(bytes, default_dst='.', specific_dst=dict()): # {{{
82 u"""Descomprime un buffer de datos en formato ZIP.
83 Los archivos se descomprimen en default_dst a menos que exista una entrada
84 en specific_dst cuya clave sea el nombre de archivo a descomprimir, en
85 cuyo caso, se descomprime usando como destino el valor de dicha clave.
87 log.debug(_(u'Intentando descomprimir'))
90 zfile = ZipFile(StringIO(bytes), 'r')
91 for f in zfile.namelist():
92 dst = join(specific_dst.get(f, default_dst), f)
93 if f.endswith(os.sep):
94 log.debug(_(u'Creando directorio "%s" en "%s"'), f, dst)
97 log.debug(_(u'Descomprimiendo archivo "%s" en "%s"'), f, dst)
98 file(dst, 'w').write(zfile.read(f))
102 class SecureProcess(object): #{{{
104 max_tiempo_cpu = 120,
107 max_cant_archivos = 5,
108 max_cant_procesos = 0,
109 max_locks_memoria = 0,
111 uid = config.get('sercom.tester.chroot.user', 65534)
113 # XXX probar! make de un solo archivo lleva nproc=100 y nofile=15
114 def __init__(self, comando, chroot, cwd, close_stdin=False,
115 close_stdout=False, close_stderr=False):
116 self.comando = comando
119 self.close_stdin = close_stdin
120 self.close_stdout = close_stdout
121 self.close_stderr = close_stderr
122 log.debug(_(u'Proceso segurizado: chroot=%s, cwd=%s, user=%s, cpu=%s, '
123 u'as=%sMiB, fsize=%sMiB, nofile=%s, nproc=%s, memlock=%s'),
124 self.chroot, self.cwd, self.uid, self.max_tiempo_cpu,
125 self.max_memoria, self.max_tam_archivo, self.max_cant_archivos,
126 self.max_cant_procesos, self.max_locks_memoria)
127 def __getattr__(self, name):
128 if getattr(self.comando, name) is not None:
129 return getattr(self.comando, name)
130 return config.get('sercom.tester.limits.' + name, self.default[name])
132 x2 = lambda x: (x, x)
135 if self.close_stdout:
137 if self.close_stderr:
139 os.chroot(self.chroot)
141 uinfo = UserInfo(self.uid)
143 os.setuid(uinfo.uid) # Somos mortales irreversiblemente
144 rsrc.setrlimit(rsrc.RLIMIT_CPU, x2(self.max_tiempo_cpu))
145 rsrc.setrlimit(rsrc.RLIMIT_AS, x2(self.max_memoria*self.MB))
146 rsrc.setrlimit(rsrc.RLIMIT_FSIZE, x2(self.max_tam_archivo*self.MB)) # XXX calcular en base a archivos esperados?
147 rsrc.setrlimit(rsrc.RLIMIT_NOFILE, x2(self.max_cant_archivos)) #XXX Obtener de archivos esperados?
148 rsrc.setrlimit(rsrc.RLIMIT_NPROC, x2(self.max_cant_procesos))
149 rsrc.setrlimit(rsrc.RLIMIT_MEMLOCK, x2(self.max_locks_memoria))
150 rsrc.setrlimit(rsrc.RLIMIT_CORE, x2(0))
151 # Tratamos de forzar un sync para que entre al sleep del padre FIXME
156 class Tester(object): #{{{
158 def __init__(self, name, path, home, queue): #{{{ y properties
163 # Ahora somos mortales (oid mortales)
164 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
165 user_info.user, user_info.group, user_info.uid, user_info.gid)
166 os.setegid(user_info.gid)
167 os.seteuid(user_info.uid)
170 def build_path(self):
171 return join(self.chroot, self.home, 'build')
175 return join(self.chroot, self.home, 'test')
179 return join(self.path, 'chroot_' + self.name)
182 def orig_chroot(self):
183 return join(self.path, 'chroot')
187 entrega_id = self.queue.get() # blocking
188 while entrega_id is not None:
189 entrega = Entrega.get(entrega_id)
190 log.debug(_(u'Nueva entrega para probar en tester %s: %s'),
193 log.debug(_(u'Fin de pruebas de: %s'), entrega)
194 entrega_id = self.queue.get() # blocking
197 def test(self, entrega): #{{{
198 log.debug(_(u'Tester.test(entrega=%s)'), entrega)
199 entrega.inicio_tareas = datetime.now()
202 self.setup_chroot(entrega)
203 self.ejecutar_tareas_fuente(entrega)
204 self.ejecutar_tareas_prueba(entrega)
205 self.clean_chroot(entrega)
206 except ExecutionFailure, e:
207 entrega.correcta = False
208 log.info(_(u'Entrega incorrecta: %s'), entrega)
210 if isinstance(e, SystemExit): raise
211 entrega.observaciones += error_interno
212 log.exception(_('Hubo una excepcion inesperada'))
214 entrega.observaciones += error_interno
215 log.exception(_('Hubo una excepcion inesperada desconocida'))
217 entrega.correcta = True
218 log.debug(_(u'Entrega correcta: %s'), entrega)
220 entrega.fin_tareas = datetime.now()
223 def setup_chroot(self, entrega): #{{{ y clean_chroot()
224 log.debug(_(u'Tester.setup_chroot(entrega=%s)'), entrega.shortrepr())
225 rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
226 '--archive', '--acls', '--delete-during', '--force', # TODO config
227 join(self.orig_chroot, ''), self.chroot)
228 log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
229 os.seteuid(0) # Dios! (para chroot)
234 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
235 user_info.user, user_info.group, user_info.uid, user_info.gid)
236 os.setegid(user_info.gid) # Mortal de nuevo
237 os.seteuid(user_info.uid)
238 unzip(entrega.archivos, self.build_path)
240 def clean_chroot(self, entrega):
241 log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
242 pass # Se limpia con el próximo rsync
245 def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
246 log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
248 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
249 if isinstance(t, TareaFuente)]
251 tarea.ejecutar(self.build_path, entrega)
253 def ejecutar_tareas_prueba(self, entrega):
254 log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
256 for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
257 caso.ejecutar(self.test_path, entrega)
262 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
263 log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
265 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
266 if isinstance(t, TareaPrueba)]
267 prueba = entrega.add_prueba(self)
271 tarea.ejecutar(path, prueba)
272 except ExecutionFailure, e:
274 if self.rechazar_si_falla:
275 entrega.exito = False
276 if self.terminar_si_falla:
277 raise ExecutionError(e.comando, e.tarea, prueba)
281 prueba.fin = datetime.now()
282 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
285 def ejecutar_tarea_fuente(self, path, entrega): #{{{
286 log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
289 for cmd in self.comandos:
290 cmd.ejecutar(path, entrega)
291 except ExecutionFailure, e:
292 if self.rechazar_si_falla:
293 entrega.exito = False
294 if self.terminar_si_falla:
295 raise ExecutionError(e.comando, tarea)
296 TareaFuente.ejecutar = ejecutar_tarea_fuente
299 def ejecutar_tarea_prueba(self, path, prueba): #{{{
300 log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
303 for cmd in self.comandos:
304 cmd.ejecutar(path, prueba)
305 except ExecutionFailure, e:
306 if self.rechazar_si_falla:
308 if self.terminar_si_falla:
309 raise ExecutionError(e.comando, tarea)
310 TareaPrueba.ejecutar = ejecutar_tarea_prueba
313 def ejecutar_comando_fuente(self, path, entrega): #{{{
314 log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
316 comando_ejecutado = entrega.add_comando_ejecutado(self)
317 unzip(self.archivos_entrada, path, # TODO try/except
318 dict(__stdin__='/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id)) # TODO /var/run/sercom
322 preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build')
324 if os.path.exists('/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id): # TODO
325 options['stdin'] = file('/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id, 'r') # TODO
327 options['preexec_fn'].close_stdin = True
328 if self.guardar_stdouterr:
329 options['stdout'] = file('/tmp/sercom.tester.%s.stdouterr'
330 % comando_ejecutado.id, 'w') #TODO /var/run/sercom?
331 options['stderr'] = sp.STDOUT
333 if self.guardar_stdout:
334 options['stdout'] = file('/tmp/sercom.tester.%s.stdout'
335 % comando_ejecutado.id, 'w') #TODO /run/lib/sercom?
337 options['preexec_fn'].close_stdout = True
338 if self.guardar_stderr:
339 options['stderr'] = file('/tmp/sercom.tester.%s.stderr'
340 % comando_ejecutado.id, 'w') #TODO /var/run/sercom?
342 options['preexec_fn'].close_stderr = True
343 log.debug(_(u'Ejecutando como root: %s'), self.comando)
344 os.seteuid(0) # Dios! (para chroot)
348 proc = sp.Popen(self.comando, **options)
350 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
351 user_info.user, user_info.group, user_info.uid, user_info.gid)
352 os.setegid(user_info.gid) # Mortal de nuevo
353 os.seteuid(user_info.uid)
355 if hasattr(e, 'child_traceback'):
356 log.error(_(u'Error en el hijo: %s'), e.child_traceback)
358 proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
359 if self.retorno != self.RET_ANY:
360 if self.retorno == self.RET_FAIL:
361 if proc.returncode == 0:
362 comando_ejecutado.exito = False
363 comando_ejecutado.observaciones += _(u'Se esperaba que el '
364 u'programa termine con un error (código de retorno '
365 u'distinto de 0) pero terminó bien (código de retorno '
367 log.debug(_(u'Se esperaba que el programa termine '
368 u'con un error (código de retorno distinto de 0) pero '
369 u'terminó bien (código de retorno 0).\n'))
370 elif self.retorno != proc.returncode:
371 comando_ejecutado.exito = False
372 if proc.returncode < 0:
373 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
374 u'con un código de retorno %s pero se obtuvo una señal %s '
375 u'(%s).\n') % (self.retorno, -proc.returncode,
376 -proc.returncode) # TODO poner con texto
377 log.debug(_(u'Se esperaba terminar con un código '
378 u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
379 self.retorno, -proc.returncode, -proc.returncode)
381 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
382 u'con un código de retorno %s pero se obtuvo %s.\n') \
383 % (self.retorno, proc.returncode)
384 log.debug(_(u'Se esperaba terminar con un código de retorno '
385 u'%s pero se obtuvo %s.\n'), self.retorno, proc.returncode)
386 if comando_ejecutado.exito is None:
387 log.debug(_(u'Código de retorno OK'))
388 comando_ejecutado.fin = datetime.now()
390 zip = ZipFile(buffer, 'w')
391 # Guardamos stdout/stderr
392 if self.guardar_stdouterr:
393 zip.write('/tmp/sercom.tester.%s.stdouterr'
394 % comando_ejecutado.id, '__stdouterr__')
396 if self.guardar_stdout:
397 zip.write('/tmp/sercom.tester.%s.stdout'
398 % comando_ejecutado.id, '__stdout__')
399 if self.guardar_stderr:
400 zip.write('/tmp/sercom.tester.%s.stderr'
401 % comando_ejecutado.id, '__stderr__')
403 for f in self.archivos_a_guardar:
404 if not os.path.exists(join(path, f)):
405 comando_ejecutado.exito = False
406 comando_ejecutado.observaciones += _(u'Se esperaba un archivo "%s" pero no fue '
408 log.debug(_(u'Se esperaba un archivo "%s" pero no fue '
411 zip.write(join(path, f), f)
413 comando_ejecutado.archivos_guardados = buffer.getvalue()
415 # if no_anda_ejecucion: # TODO
416 # comando_ejecutado.exito = False
417 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
418 # if self.rechazar_si_falla:
419 # entrega.exito = False
420 # if self.terminar_si_falla: # TODO
421 # raise ExecutionFailure(self)
422 # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
423 # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
424 # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
425 # for archivo in self.archivos_salida:
426 # pass # TODO hacer diff
427 # if archivos_mal: # TODO
428 # comando_ejecutado.exito = False
429 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
430 # if self.rechazar_si_falla:
431 # entrega.exito = False
432 # if self.terminar_si_falla: # TODO
433 # raise ExecutionFailure(self)
435 # comando_ejecutado.exito = True
436 # comando_ejecutado.observaciones += 'xxx OK' # TODO
437 comando_ejecutado.exito = True
438 comando_ejecutado.observaciones += 'xxx OK' # TODO
439 ComandoFuente.ejecutar = ejecutar_comando_fuente
442 def ejecutar_comando_prueba(self, path, prueba): #{{{
443 log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
447 unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
448 unzip(self.archivos_entrada, path) # TODO try/except
449 comando_ejecutado = prueba.add_comando_ejecutado(self)
450 # TODO ejecutar en chroot (path)
451 comando_ejecutado.fin = datetime.now()
452 # if no_anda_ejecucion: # TODO
453 # comando_ejecutado.exito = False
454 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
455 # if self.rechazar_si_falla:
456 # entrega.exito = False
457 # if self.terminar_si_falla: # TODO
458 # raise ExecutionFailure(self) # TODO info de error
459 # for archivo in self.archivos_salida:
460 # pass # TODO hacer diff
461 # if archivos_mal: # TODO
462 # comando_ejecutado.exito = False
463 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
464 # if self.rechazar_si_falla:
465 # entrega.exito = False
466 # if self.terminar_si_falla: # TODO
467 # raise ExecutionFailure(comando=self) # TODO info de error
469 # comando_ejecutado.exito = True
470 # comando_ejecutado.observaciones += 'xxx OK' # TODO
471 comando_ejecutado.exito = True
472 comando_ejecutado.observaciones += 'xxx OK' # TODO
473 ComandoPrueba.ejecutar = ejecutar_comando_prueba