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 subprocess import Popen, PIPE, call #, check_call XXX Python 2.5
10 from os.path import join
11 from turbogears import config
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 def check_call(*popenargs, **kwargs): #{{{ Python 2.5 forward-compatibility
21 """Run command with arguments. Wait for command to complete. If
22 the exit code was zero then return, otherwise raise
23 CalledProcessError. The CalledProcessError object will have the
24 return code in the returncode attribute.
25 ret = call(*popenargs, **kwargs)
27 The arguments are the same as for the Popen constructor. Example:
29 check_call(["ls", "-l"])
31 retcode = call(*popenargs, **kwargs)
32 cmd = kwargs.get("args")
36 raise CalledProcessError(retcode, cmd)
42 class CalledProcessError(Exception): #{{{ Python 2.5 forward-compatibility
43 """This exception is raised when a process run by check_call() returns
44 a non-zero exit status. The exit status will be stored in the
45 returncode attribute."""
46 def __init__(self, returncode, cmd):
47 self.returncode = returncode
50 return ("Command '%s' returned non-zero exit status %d"
51 % (self.cmd, self.returncode))
54 class Error(StandardError): pass
56 class ExecutionFailure(Error, RuntimeError): pass
58 class RsyncError(Error, EnvironmentError): pass
62 def unzip(bytes, dst): # {{{
63 log.debug(_(u'Intentando descomprimir en %s'), dst)
66 zfile = ZipFile(StringIO(bytes), 'r')
67 for f in zfile.namelist():
68 if f.endswith(os.sep):
69 log.debug(_(u'Creando directorio %s'), f)
70 os.mkdir(join(dst, f))
72 log.debug(_(u'Descomprimiendo archivo %s'), f)
73 file(join(dst, f), 'w').write(zfile.read(f))
76 def get_pwdgrp(unam, gnam): #{{{
77 def do(type, funcnam, funcid, name):
85 log.critical(_(u'No existe el %s %s (%s)'), type, name, e)
88 return do('usuario', pwd.getpwnam, pwd.getpwuid, unam) \
89 + do('grupo', grp.getgrnam, grp.getgrgid, gnam)
92 class SecureProcess(object): #{{{
97 max_cant_archivos = 5,
98 max_cant_procesos = 0,
99 max_locks_memoria = 0,
101 uid = config.get('sercom.tester.chroot.user', 65534)
102 gid = config.get('sercom.tester.chroot.group', 65534)
104 # XXX probar! make de un solo archivo lleva nproc=100 y nofile=15
105 def __init__(self, comando, chroot, cwd):
106 self.comando = comando
109 def __getattr__(self, name):
110 if getattr(self.comando, name) is not None:
111 return getattr(self.comando, name)
112 return config.get('sercom.tester.limits.' + name, self.default[name])
114 x2 = lambda x: (x, x)
115 os.chroot(self.chroot)
117 (uid, unam, gid, gnam) = get_pwdgrp(self.uid, self.gid)
120 rsrc.setrlimit(rsrc.RLIMIT_CPU, x2(self.max_tiempo_cpu))
121 rsrc.setrlimit(rsrc.RLIMIT_AS, x2(self.max_memoria*self.MB))
122 rsrc.setrlimit(rsrc.RLIMIT_FSIZE, x2(self.max_tam_archivo*self.MB)) # XXX calcular en base a archivos esperados?
123 rsrc.setrlimit(rsrc.RLIMIT_NOFILE, x2(self.max_cant_archivos)) #XXX Obtener de archivos esperados?
124 rsrc.setrlimit(rsrc.RLIMIT_NPROC, x2(self.max_cant_procesos))
125 rsrc.setrlimit(rsrc.RLIMIT_MEMLOCK, x2(self.max_locks_memoria))
126 rsrc.setrlimit(rsrc.RLIMIT_CORE, x2(0))
127 log.debug('Proceso segurizado: chroot=%s, cwd=%s, user=%s(%s), '
128 'group=%s(%s), cpu=%s, as=%sMiB, fsize=%sMiB, nofile=%s, nproc=%s, '
129 'memlock=%s', self.chroot, self.cwd, unam, uid, gnam, gid,
130 self.max_tiempo_cpu, self.max_memoria, self.max_tam_archivo,
131 self.max_cant_archivos, self.max_cant_procesos,
132 self.max_locks_memoria)
133 # Tratamos de forzar un sync para que entre al sleep del padre FIXME
138 class Tester(object): #{{{
140 def __init__(self, name, path, home, queue): #{{{ y properties
145 # Ahora somos mortales (oid mortales)
146 euid = config.get('sercom.tester.user', 65534)
147 egid = config.get('sercom.tester.group', 65534)
148 (self.euid, self.eunam, self.egid, self.egnam) = get_pwdgrp(euid, egid)
149 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
150 self.eunam, self.egnam, self.euid, self.egid)
151 os.setegid(self.egid)
152 os.seteuid(self.euid)
155 def build_path(self):
156 return join(self.chroot, self.home, 'build')
160 return join(self.chroot, self.home, 'test')
164 return join(self.path, 'chroot_' + self.name)
168 def orig_chroot(self):
169 return join(self.path, 'chroot')
172 entrega_id = self.queue.get() # blocking
173 while entrega_id is not None:
174 entrega = Entrega.get(entrega_id)
175 log.debug(_(u'Nueva entrega para probar en tester %s: %s'),
178 log.debug(_(u'Fin de pruebas de: %s'), entrega)
179 entrega_id = self.queue.get() # blocking
182 def test(self, entrega): #{{{
183 log.debug(_(u'Tester.test(entrega=%s)'), entrega)
184 entrega.inicio_tareas = datetime.now()
187 self.setup_chroot(entrega)
188 self.ejecutar_tareas_fuente(entrega)
189 self.ejecutar_tareas_prueba(entrega)
190 self.clean_chroot(entrega)
191 except ExecutionFailure, e:
192 entrega.correcta = False
193 log.info(_(u'Entrega incorrecta: %s'), entrega)
195 if isinstance(e, SystemExit): raise
196 entrega.observaciones += error_interno
197 log.exception(_(u'Hubo una excepción inesperada: %s'), e)
199 entrega.observaciones += error_interno
200 log.exception(_(u'Hubo una excepción inesperada desconocida'))
202 entrega.correcta = True
203 log.debug(_(u'Entrega correcta: %s'), entrega)
205 entrega.fin_tareas = datetime.now()
208 def setup_chroot(self, entrega): #{{{ y clean_chroot()
209 log.debug(_(u'Tester.setup_chroot(entrega=%s)'), entrega.shortrepr())
210 rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
211 '--archive', '--acls', '--delete-during', '--force', # TODO config
212 join(self.orig_chroot, ''), self.chroot)
213 log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
214 os.seteuid(0) # Dios! (para chroot)
219 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
220 self.eunam, self.egnam, self.euid, self.egid)
221 os.setegid(self.egid) # Mortal de nuevo
222 os.seteuid(self.euid)
223 unzip(entrega.archivos, self.build_path)
225 def clean_chroot(self, entrega):
226 log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
227 pass # Se limpia con el próximo rsync
230 def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
231 log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
233 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
234 if isinstance(t, TareaFuente)]
236 tarea.ejecutar(self.build_path, entrega)
238 def ejecutar_tareas_prueba(self, entrega):
239 log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
241 for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
242 caso.ejecutar(self.test_path, entrega)
247 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
248 log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
250 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
251 if isinstance(t, TareaPrueba)]
252 prueba = entrega.add_prueba(self)
256 tarea.ejecutar(path, prueba)
257 except ExecutionFailure, e:
258 prueba.pasada = False
259 if self.rechazar_si_falla:
260 entrega.exito = False
261 if self.terminar_si_falla:
262 raise ExecutionError(e.comando, e.tarea, prueba)
266 prueba.fin = datetime.now()
267 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
270 def ejecutar_tarea_fuente(self, path, entrega): #{{{
271 log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
274 for cmd in self.comandos:
275 cmd.ejecutar(path, entrega)
276 except ExecutionFailure, e:
277 if self.rechazar_si_falla:
278 entrega.exito = False
279 if self.terminar_si_falla:
280 raise ExecutionError(e.comando, tarea)
281 TareaFuente.ejecutar = ejecutar_tarea_fuente
284 def ejecutar_tarea_prueba(self, path, prueba): #{{{
285 log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
288 for cmd in self.comandos:
289 cmd.ejecutar(path, prueba)
290 except ExecutionFailure, e:
291 if self.rechazar_si_falla:
293 if self.terminar_si_falla:
294 raise ExecutionError(e.comando, tarea)
295 TareaPrueba.ejecutar = ejecutar_tarea_prueba
298 def ejecutar_comando_fuente(self, path, entrega): #{{{
299 log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
301 unzip(self.archivos_entrada, path) # TODO try/except
302 comando_ejecutado = entrega.add_comando_ejecutado(self)
303 # Abro archivos para fds básicos (FIXME)
304 options = dict(close_fds=True, stdin=None, stdout=None, stderr=None,
305 preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build'))
306 log.debug(_(u'Ejecutando como root: %s'), ' '.join(self.comando))
309 os.seteuid(0) # Dios! (para chroot)
313 proc = Popen(self.comando, **options)
315 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s'),
317 os.setegid(gid) # Mortal de nuevo
319 except Exception, e: # FIXME poner en el manejo de exceptiones estandar
320 if hasattr(e, 'child_traceback'):
321 log.error(_(u'Error en el hijo: %s'), e.child_traceback)
324 comando_ejecutado.fin = datetime.now()
325 # if no_anda_ejecucion: # TODO
326 # comando_ejecutado.exito = False
327 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
328 # if self.rechazar_si_falla:
329 # entrega.exito = False
330 # if self.terminar_si_falla: # TODO
331 # raise ExecutionFailure(self)
332 # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
333 # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
334 # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
335 # for archivo in self.archivos_salida:
336 # pass # TODO hacer diff
337 # if archivos_mal: # TODO
338 # comando_ejecutado.exito = False
339 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
340 # if self.rechazar_si_falla:
341 # entrega.exito = False
342 # if self.terminar_si_falla: # TODO
343 # raise ExecutionFailure(self)
345 # comando_ejecutado.exito = True
346 # comando_ejecutado.observaciones += 'xxx OK' # TODO
347 comando_ejecutado.exito = True
348 comando_ejecutado.observaciones += 'xxx OK' # TODO
349 ComandoFuente.ejecutar = ejecutar_comando_fuente
352 def ejecutar_comando_prueba(self, path, prueba): #{{{
353 log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
357 unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
358 unzip(self.archivos_entrada, path) # TODO try/except
359 comando_ejecutado = prueba.add_comando_ejecutado(self)
360 # TODO ejecutar en chroot (path)
361 comando_ejecutado.fin = datetime.now()
362 # if no_anda_ejecucion: # TODO
363 # comando_ejecutado.exito = False
364 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
365 # if self.rechazar_si_falla:
366 # entrega.exito = False
367 # if self.terminar_si_falla: # TODO
368 # raise ExecutionFailure(self) # TODO info de error
369 # for archivo in self.archivos_salida:
370 # pass # TODO hacer diff
371 # if archivos_mal: # TODO
372 # comando_ejecutado.exito = False
373 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
374 # if self.rechazar_si_falla:
375 # entrega.exito = False
376 # if self.terminar_si_falla: # TODO
377 # raise ExecutionFailure(comando=self) # TODO info de error
379 # comando_ejecutado.exito = True
380 # comando_ejecutado.observaciones += 'xxx OK' # TODO
381 comando_ejecutado.exito = True
382 comando_ejecutado.observaciones += 'xxx OK' # TODO
383 ComandoPrueba.ejecutar = ejecutar_comando_prueba