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)
308 preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build')
310 log.debug(_(u'Ejecutando como root: %s'), self.comando)
311 os.seteuid(0) # Dios! (para chroot)
315 proc = sp.Popen(self.comando, **options)
317 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
318 user_info.user, user_info.group, user_info.uid, user_info.gid)
319 os.setegid(user_info.gid) # Mortal de nuevo
320 os.seteuid(user_info.uid)
322 if hasattr(e, 'child_traceback'):
323 log.error(_(u'Error en el hijo: %s'), e.child_traceback)
325 proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
326 comando_ejecutado.fin = datetime.now()
327 # if no_anda_ejecucion: # TODO
328 # comando_ejecutado.exito = False
329 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
330 # if self.rechazar_si_falla:
331 # entrega.exito = False
332 # if self.terminar_si_falla: # TODO
333 # raise ExecutionFailure(self)
334 # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
335 # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
336 # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
337 # for archivo in self.archivos_salida:
338 # pass # TODO hacer diff
339 # if archivos_mal: # TODO
340 # comando_ejecutado.exito = False
341 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
342 # if self.rechazar_si_falla:
343 # entrega.exito = False
344 # if self.terminar_si_falla: # TODO
345 # raise ExecutionFailure(self)
347 # comando_ejecutado.exito = True
348 # comando_ejecutado.observaciones += 'xxx OK' # TODO
349 comando_ejecutado.exito = True
350 comando_ejecutado.observaciones += 'xxx OK' # TODO
351 ComandoFuente.ejecutar = ejecutar_comando_fuente
354 def ejecutar_comando_prueba(self, path, prueba): #{{{
355 log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
359 unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
360 unzip(self.archivos_entrada, path) # TODO try/except
361 comando_ejecutado = prueba.add_comando_ejecutado(self)
362 # TODO ejecutar en chroot (path)
363 comando_ejecutado.fin = datetime.now()
364 # if no_anda_ejecucion: # TODO
365 # comando_ejecutado.exito = False
366 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
367 # if self.rechazar_si_falla:
368 # entrega.exito = False
369 # if self.terminar_si_falla: # TODO
370 # raise ExecutionFailure(self) # TODO info de error
371 # for archivo in self.archivos_salida:
372 # pass # TODO hacer diff
373 # if archivos_mal: # TODO
374 # comando_ejecutado.exito = False
375 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
376 # if self.rechazar_si_falla:
377 # entrega.exito = False
378 # if self.terminar_si_falla: # TODO
379 # raise ExecutionFailure(comando=self) # TODO info de error
381 # comando_ejecutado.exito = True
382 # comando_ejecutado.observaciones += 'xxx OK' # TODO
383 comando_ejecutado.exito = True
384 comando_ejecutado.observaciones += 'xxx OK' # TODO
385 ComandoPrueba.ejecutar = ejecutar_comando_prueba