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 class CalledProcessError(Exception): #{{{ Python 2.5 forward-compatibility
19 """This exception is raised when a process run by check_call() returns
20 a non-zero exit status. The exit status will be stored in the
21 returncode attribute."""
22 def __init__(self, returncode, cmd):
23 self.returncode = returncode
26 return ("Command '%s' returned non-zero exit status %d"
27 % (self.cmd, self.returncode))
30 def check_call(*popenargs, **kwargs): #{{{ Python 2.5 forward-compatibility
31 """Run command with arguments. Wait for command to complete. If
32 the exit code was zero then return, otherwise raise
33 CalledProcessError. The CalledProcessError object will have the
34 return code in the returncode attribute.
35 ret = call(*popenargs, **kwargs)
37 The arguments are the same as for the Popen constructor. Example:
39 check_call(["ls", "-l"])
41 retcode = call(*popenargs, **kwargs)
42 cmd = kwargs.get("args")
46 raise CalledProcessError(retcode, cmd)
50 class Error(StandardError): pass
52 class ExecutionFailure(Error, RuntimeError): pass
54 class RsyncError(Error, EnvironmentError): pass
56 error_interno = _(u'\n**Error interno al preparar la entrega.**')
58 def unzip(bytes, dst): # {{{
59 log.debug(_(u'Intentando descomprimir en %s'), dst)
62 zfile = ZipFile(StringIO(bytes), 'r')
63 for f in zfile.namelist():
64 if f.endswith(os.sep):
65 log.debug(_(u'Creando directorio %s'), f)
66 os.mkdir(join(dst, f))
68 log.debug(_(u'Descomprimiendo archivo %s'), f)
69 file(join(dst, f), 'w').write(zfile.read(f))
72 def get_pwdgrp(unam, gnam): #{{{
73 def do(type, funcnam, funcid, name):
81 log.critical(_(u'No existe el %s %s (%s)'), type, name, e)
84 return do('usuario', pwd.getpwnam, pwd.getpwuid, unam) \
85 + do('grupo', grp.getgrnam, grp.getgrgid, gnam)
88 class SecureProcess(object): #{{{
93 max_cant_archivos = 5,
94 max_cant_procesos = 0,
95 max_locks_memoria = 0,
97 uid = config.get('sercom.tester.chroot.user', 65534)
98 gid = config.get('sercom.tester.chroot.group', 65534)
100 # XXX probar! make de un solo archivo lleva nproc=100 y nofile=15
101 def __init__(self, comando, chroot, cwd):
102 self.comando = comando
105 def __getattr__(self, name):
106 if getattr(self.comando, name) is not None:
107 return getattr(self.comando, name)
108 return config.get('sercom.tester.limits.' + name, self.default[name])
110 x2 = lambda x: (x, x)
111 os.chroot(self.chroot)
113 (uid, unam, gid, gnam) = get_pwdgrp(self.uid, self.gid)
116 rsrc.setrlimit(rsrc.RLIMIT_CPU, x2(self.max_tiempo_cpu))
117 rsrc.setrlimit(rsrc.RLIMIT_AS, x2(self.max_memoria*self.MB))
118 rsrc.setrlimit(rsrc.RLIMIT_FSIZE, x2(self.max_tam_archivo*self.MB)) # XXX calcular en base a archivos esperados?
119 rsrc.setrlimit(rsrc.RLIMIT_NOFILE, x2(self.max_cant_archivos)) #XXX Obtener de archivos esperados?
120 rsrc.setrlimit(rsrc.RLIMIT_NPROC, x2(self.max_cant_procesos))
121 rsrc.setrlimit(rsrc.RLIMIT_MEMLOCK, x2(self.max_locks_memoria))
122 rsrc.setrlimit(rsrc.RLIMIT_CORE, x2(0))
123 log.debug('Proceso segurizado: chroot=%s, cwd=%s, user=%s(%s), '
124 'group=%s(%s), cpu=%s, as=%sMiB, fsize=%sMiB, nofile=%s, nproc=%s, '
125 'memlock=%s', self.chroot, self.cwd, unam, uid, gnam, gid,
126 self.max_tiempo_cpu, self.max_memoria, self.max_tam_archivo,
127 self.max_cant_archivos, self.max_cant_procesos,
128 self.max_locks_memoria)
129 # Tratamos de forzar un sync para que entre al sleep del padre FIXME
134 class Tester(object): #{{{
136 def __init__(self, name, path, home, queue): #{{{ y properties
141 # Ahora somos mortales (oid mortales)
142 euid = config.get('sercom.tester.user', 65534)
143 egid = config.get('sercom.tester.group', 65534)
144 (self.euid, self.eunam, self.egid, self.egnam) = get_pwdgrp(euid, egid)
145 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
146 self.eunam, self.egnam, self.euid, self.egid)
147 os.setegid(self.egid)
148 os.seteuid(self.euid)
151 def build_path(self):
152 return join(self.chroot, self.home, 'build')
156 return join(self.chroot, self.home, 'test')
160 return join(self.path, 'chroot_' + self.name)
164 def orig_chroot(self):
165 return join(self.path, 'chroot')
168 entrega_id = self.queue.get() # blocking
169 while entrega_id is not None:
170 entrega = Entrega.get(entrega_id)
171 log.debug(_(u'Nueva entrega para probar en tester %s: %s'),
174 log.debug(_(u'Fin de pruebas de: %s'), entrega)
175 entrega_id = self.queue.get() # blocking
178 def test(self, entrega): #{{{
179 log.debug(_(u'Tester.test(entrega=%s)'), entrega)
180 entrega.inicio_tareas = datetime.now()
183 self.setup_chroot(entrega)
184 self.ejecutar_tareas_fuente(entrega)
185 self.ejecutar_tareas_prueba(entrega)
186 self.clean_chroot(entrega)
187 except ExecutionFailure, e:
188 entrega.correcta = False
189 log.info(_(u'Entrega incorrecta: %s'), entrega)
191 if isinstance(e, SystemExit): raise
192 entrega.observaciones += error_interno
193 log.exception(_(u'Hubo una excepción inesperada: %s'), e)
195 entrega.observaciones += error_interno
196 log.exception(_(u'Hubo una excepción inesperada desconocida'))
198 entrega.correcta = True
199 log.debug(_(u'Entrega correcta: %s'), entrega)
201 entrega.fin_tareas = datetime.now()
204 def setup_chroot(self, entrega): #{{{ y clean_chroot()
205 log.debug(_(u'Tester.setup_chroot(entrega=%s)'), entrega.shortrepr())
206 rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
207 '--archive', '--acls', '--delete-during', '--force', # TODO config
208 join(self.orig_chroot, ''), self.chroot)
209 log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
210 os.seteuid(0) # Dios! (para chroot)
215 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
216 self.eunam, self.egnam, self.euid, self.egid)
217 os.setegid(self.egid) # Mortal de nuevo
218 os.seteuid(self.euid)
219 unzip(entrega.archivos, self.build_path)
221 def clean_chroot(self, entrega):
222 log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
223 pass # Se limpia con el próximo rsync
226 def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
227 log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
229 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
230 if isinstance(t, TareaFuente)]
232 tarea.ejecutar(self.build_path, entrega)
234 def ejecutar_tareas_prueba(self, entrega):
235 log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
237 for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
238 caso.ejecutar(self.test_path, entrega)
243 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
244 log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
246 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
247 if isinstance(t, TareaPrueba)]
248 prueba = entrega.add_prueba(self)
252 tarea.ejecutar(path, prueba)
253 except ExecutionFailure, e:
254 prueba.pasada = False
255 if self.rechazar_si_falla:
256 entrega.exito = False
257 if self.terminar_si_falla:
258 raise ExecutionError(e.comando, e.tarea, prueba)
262 prueba.fin = datetime.now()
263 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
266 def ejecutar_tarea_fuente(self, path, entrega): #{{{
267 log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
270 for cmd in self.comandos:
271 cmd.ejecutar(path, entrega)
272 except ExecutionFailure, e:
273 if self.rechazar_si_falla:
274 entrega.exito = False
275 if self.terminar_si_falla:
276 raise ExecutionError(e.comando, tarea)
277 TareaFuente.ejecutar = ejecutar_tarea_fuente
280 def ejecutar_tarea_prueba(self, path, prueba): #{{{
281 log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
284 for cmd in self.comandos:
285 cmd.ejecutar(path, prueba)
286 except ExecutionFailure, e:
287 if self.rechazar_si_falla:
289 if self.terminar_si_falla:
290 raise ExecutionError(e.comando, tarea)
291 TareaPrueba.ejecutar = ejecutar_tarea_prueba
294 def ejecutar_comando_fuente(self, path, entrega): #{{{
295 log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
297 unzip(self.archivos_entrada, path) # TODO try/except
298 comando_ejecutado = entrega.add_comando_ejecutado(self)
299 # Abro archivos para fds básicos (FIXME)
300 options = dict(close_fds=True, stdin=None, stdout=None, stderr=None,
301 preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build'))
302 log.debug(_(u'Ejecutando como root: %s'), ' '.join(self.comando))
305 os.seteuid(0) # Dios! (para chroot)
309 proc = Popen(self.comando, **options)
311 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s'),
313 os.setegid(gid) # Mortal de nuevo
315 except Exception, e: # FIXME poner en el manejo de exceptiones estandar
316 if hasattr(e, 'child_traceback'):
317 log.error(_(u'Error en el hijo: %s'), e.child_traceback)
320 comando_ejecutado.fin = datetime.now()
321 # if no_anda_ejecucion: # TODO
322 # comando_ejecutado.exito = False
323 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
324 # if self.rechazar_si_falla:
325 # entrega.exito = False
326 # if self.terminar_si_falla: # TODO
327 # raise ExecutionFailure(self)
328 # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
329 # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
330 # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
331 # for archivo in self.archivos_salida:
332 # pass # TODO hacer diff
333 # if archivos_mal: # TODO
334 # comando_ejecutado.exito = False
335 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
336 # if self.rechazar_si_falla:
337 # entrega.exito = False
338 # if self.terminar_si_falla: # TODO
339 # raise ExecutionFailure(self)
341 # comando_ejecutado.exito = True
342 # comando_ejecutado.observaciones += 'xxx OK' # TODO
343 comando_ejecutado.exito = True
344 comando_ejecutado.observaciones += 'xxx OK' # TODO
345 ComandoFuente.ejecutar = ejecutar_comando_fuente
348 def ejecutar_comando_prueba(self, path, prueba): #{{{
349 log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
353 unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
354 unzip(self.archivos_entrada, path) # TODO try/except
355 comando_ejecutado = prueba.add_comando_ejecutado(self)
356 # TODO ejecutar en chroot (path)
357 comando_ejecutado.fin = datetime.now()
358 # if no_anda_ejecucion: # TODO
359 # comando_ejecutado.exito = False
360 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
361 # if self.rechazar_si_falla:
362 # entrega.exito = False
363 # if self.terminar_si_falla: # TODO
364 # raise ExecutionFailure(self) # TODO info de error
365 # for archivo in self.archivos_salida:
366 # pass # TODO hacer diff
367 # if archivos_mal: # TODO
368 # comando_ejecutado.exito = False
369 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
370 # if self.rechazar_si_falla:
371 # entrega.exito = False
372 # if self.terminar_si_falla: # TODO
373 # raise ExecutionFailure(comando=self) # TODO info de error
375 # comando_ejecutado.exito = True
376 # comando_ejecutado.observaciones += 'xxx OK' # TODO
377 comando_ejecutado.exito = True
378 comando_ejecutado.observaciones += 'xxx OK' # TODO
379 ComandoPrueba.ejecutar = ejecutar_comando_prueba