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, dst): # {{{
82 log.debug(_(u'Intentando descomprimir en %s'), dst)
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))
91 log.debug(_(u'Descomprimiendo archivo %s'), f)
92 file(join(dst, f), 'w').write(zfile.read(f))
95 class SecureProcess(object): #{{{
100 max_cant_archivos = 5,
101 max_cant_procesos = 0,
102 max_locks_memoria = 0,
104 uid = config.get('sercom.tester.chroot.user', 65534)
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
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])
121 x2 = lambda x: (x, x)
122 os.chroot(self.chroot)
124 uinfo = UserInfo(self.uid)
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
139 class Tester(object): #{{{
141 def __init__(self, name, path, home, queue): #{{{ y properties
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)
153 def build_path(self):
154 return join(self.chroot, self.home, 'build')
158 return join(self.chroot, self.home, 'test')
162 return join(self.path, 'chroot_' + self.name)
165 def orig_chroot(self):
166 return join(self.path, 'chroot')
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'),
176 log.debug(_(u'Fin de pruebas de: %s'), entrega)
177 entrega_id = self.queue.get() # blocking
180 def test(self, entrega): #{{{
181 log.debug(_(u'Tester.test(entrega=%s)'), entrega)
182 entrega.inicio_tareas = datetime.now()
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)
193 if isinstance(e, SystemExit): raise
194 entrega.observaciones += error_interno
195 log.exception(_(u'Hubo una excepción inesperada: %s'), e)
197 entrega.observaciones += error_interno
198 log.exception(_(u'Hubo una excepción inesperada desconocida'))
200 entrega.correcta = True
201 log.debug(_(u'Entrega correcta: %s'), entrega)
203 entrega.fin_tareas = datetime.now()
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)
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)
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
228 def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
229 log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
231 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
232 if isinstance(t, TareaFuente)]
234 tarea.ejecutar(self.build_path, entrega)
236 def ejecutar_tareas_prueba(self, entrega):
237 log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
239 for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
240 caso.ejecutar(self.test_path, entrega)
245 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
246 log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
248 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
249 if isinstance(t, TareaPrueba)]
250 prueba = entrega.add_prueba(self)
254 tarea.ejecutar(path, prueba)
255 except ExecutionFailure, e:
257 if self.rechazar_si_falla:
258 entrega.exito = False
259 if self.terminar_si_falla:
260 raise ExecutionError(e.comando, e.tarea, prueba)
264 prueba.fin = datetime.now()
265 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
268 def ejecutar_tarea_fuente(self, path, entrega): #{{{
269 log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
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
282 def ejecutar_tarea_prueba(self, path, prueba): #{{{
283 log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
286 for cmd in self.comandos:
287 cmd.ejecutar(path, prueba)
288 except ExecutionFailure, e:
289 if self.rechazar_si_falla:
291 if self.terminar_si_falla:
292 raise ExecutionError(e.comando, tarea)
293 TareaPrueba.ejecutar = ejecutar_tarea_prueba
296 def ejecutar_comando_fuente(self, path, entrega): #{{{
297 log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
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)
306 preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build')
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
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)
324 proc = sp.Popen(self.comando, **options)
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)
331 if hasattr(e, 'child_traceback'):
332 log.error(_(u'Error en el hijo: %s'), e.child_traceback)
334 proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
335 comando_ejecutado.fin = datetime.now()
337 zip = ZipFile(buffer, 'w')
338 if self.guardar_stdouterr:
339 zip.write('/tmp/sercom.tester.%s.stdouterr'
340 % comando_ejecutado.id, '__stdouterr__')
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__')
349 comando_ejecutado.archivos_guardados = buffer.getvalue()
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)
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
378 def ejecutar_comando_prueba(self, path, prueba): #{{{
379 log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), 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
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