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 def __getattr__(self, name):
112 if getattr(self.comando, name) is not None:
113 return getattr(self.comando, name)
114 return config.get('sercom.tester.limits.' + name, self.default[name])
116 x2 = lambda x: (x, x)
117 os.chroot(self.chroot)
119 uinfo = UserInfo(self.uid)
121 os.setuid(uinfo.uid) # Somos mortales irreversiblemente
122 rsrc.setrlimit(rsrc.RLIMIT_CPU, x2(self.max_tiempo_cpu))
123 rsrc.setrlimit(rsrc.RLIMIT_AS, x2(self.max_memoria*self.MB))
124 rsrc.setrlimit(rsrc.RLIMIT_FSIZE, x2(self.max_tam_archivo*self.MB)) # XXX calcular en base a archivos esperados?
125 rsrc.setrlimit(rsrc.RLIMIT_NOFILE, x2(self.max_cant_archivos)) #XXX Obtener de archivos esperados?
126 rsrc.setrlimit(rsrc.RLIMIT_NPROC, x2(self.max_cant_procesos))
127 rsrc.setrlimit(rsrc.RLIMIT_MEMLOCK, x2(self.max_locks_memoria))
128 rsrc.setrlimit(rsrc.RLIMIT_CORE, x2(0))
129 log.debug('Proceso segurizado: chroot=%s, cwd=%s, user=%s(%s), '
130 'group=%s(%s), cpu=%s, as=%sMiB, fsize=%sMiB, nofile=%s, nproc=%s, '
131 'memlock=%s', self.chroot, self.cwd, uinfo.user, uinfo.uid,
132 uinfo.group, uinfo.gid, self.max_tiempo_cpu, self.max_memoria,
133 self.max_tam_archivo, self.max_cant_archivos,
134 self.max_cant_procesos, self.max_locks_memoria)
135 # Tratamos de forzar un sync para que entre al sleep del padre FIXME
140 class Tester(object): #{{{
142 def __init__(self, name, path, home, queue): #{{{ y properties
147 # Ahora somos mortales (oid mortales)
148 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
149 user_info.user, user_info.group, user_info.uid, user_info.gid)
150 os.setegid(user_info.gid)
151 os.seteuid(user_info.uid)
154 def build_path(self):
155 return join(self.chroot, self.home, 'build')
159 return join(self.chroot, self.home, 'test')
163 return join(self.path, 'chroot_' + self.name)
167 def orig_chroot(self):
168 return join(self.path, 'chroot')
171 entrega_id = self.queue.get() # blocking
172 while entrega_id is not None:
173 entrega = Entrega.get(entrega_id)
174 log.debug(_(u'Nueva entrega para probar en tester %s: %s'),
177 log.debug(_(u'Fin de pruebas de: %s'), entrega)
178 entrega_id = self.queue.get() # blocking
181 def test(self, entrega): #{{{
182 log.debug(_(u'Tester.test(entrega=%s)'), entrega)
183 entrega.inicio_tareas = datetime.now()
186 self.setup_chroot(entrega)
187 self.ejecutar_tareas_fuente(entrega)
188 self.ejecutar_tareas_prueba(entrega)
189 self.clean_chroot(entrega)
190 except ExecutionFailure, e:
191 entrega.correcta = False
192 log.info(_(u'Entrega incorrecta: %s'), entrega)
194 if isinstance(e, SystemExit): raise
195 entrega.observaciones += error_interno
196 log.exception(_(u'Hubo una excepción inesperada: %s'), e)
198 entrega.observaciones += error_interno
199 log.exception(_(u'Hubo una excepción inesperada desconocida'))
201 entrega.correcta = True
202 log.debug(_(u'Entrega correcta: %s'), entrega)
204 entrega.fin_tareas = datetime.now()
207 def setup_chroot(self, entrega): #{{{ y clean_chroot()
208 log.debug(_(u'Tester.setup_chroot(entrega=%s)'), entrega.shortrepr())
209 rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
210 '--archive', '--acls', '--delete-during', '--force', # TODO config
211 join(self.orig_chroot, ''), self.chroot)
212 log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
213 os.seteuid(0) # Dios! (para chroot)
218 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
219 user_info.user, user_info.group, user_info.uid, user_info.gid)
220 os.setegid(user_info.gid) # Mortal de nuevo
221 os.seteuid(user_info.uid)
222 unzip(entrega.archivos, self.build_path)
224 def clean_chroot(self, entrega):
225 log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
226 pass # Se limpia con el próximo rsync
229 def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
230 log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
232 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
233 if isinstance(t, TareaFuente)]
235 tarea.ejecutar(self.build_path, entrega)
237 def ejecutar_tareas_prueba(self, entrega):
238 log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
240 for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
241 caso.ejecutar(self.test_path, entrega)
246 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
247 log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
249 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
250 if isinstance(t, TareaPrueba)]
251 prueba = entrega.add_prueba(self)
255 tarea.ejecutar(path, prueba)
256 except ExecutionFailure, e:
258 if self.rechazar_si_falla:
259 entrega.exito = False
260 if self.terminar_si_falla:
261 raise ExecutionError(e.comando, e.tarea, prueba)
265 prueba.fin = datetime.now()
266 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
269 def ejecutar_tarea_fuente(self, path, entrega): #{{{
270 log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
273 for cmd in self.comandos:
274 cmd.ejecutar(path, entrega)
275 except ExecutionFailure, e:
276 if self.rechazar_si_falla:
277 entrega.exito = False
278 if self.terminar_si_falla:
279 raise ExecutionError(e.comando, tarea)
280 TareaFuente.ejecutar = ejecutar_tarea_fuente
283 def ejecutar_tarea_prueba(self, path, prueba): #{{{
284 log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
287 for cmd in self.comandos:
288 cmd.ejecutar(path, prueba)
289 except ExecutionFailure, e:
290 if self.rechazar_si_falla:
292 if self.terminar_si_falla:
293 raise ExecutionError(e.comando, tarea)
294 TareaPrueba.ejecutar = ejecutar_tarea_prueba
297 def ejecutar_comando_fuente(self, path, entrega): #{{{
298 log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
300 unzip(self.archivos_entrada, path) # TODO try/except
301 comando_ejecutado = entrega.add_comando_ejecutado(self)
302 # Abro archivos para fds básicos (FIXME)
309 preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build')
311 log.debug(_(u'Ejecutando como root: %s'), self.comando)
312 os.seteuid(0) # Dios! (para chroot)
316 proc = sp.Popen(self.comando, **options)
318 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
319 user_info.user, user_info.group, user_info.uid, user_info.gid)
320 os.setegid(user_info.gid) # Mortal de nuevo
321 os.seteuid(user_info.uid)
322 except Exception, e: # FIXME poner en el manejo de exceptiones estandar
323 if hasattr(e, 'child_traceback'):
324 log.error(_(u'Error en el hijo: %s'), e.child_traceback)
327 comando_ejecutado.fin = datetime.now()
328 # if no_anda_ejecucion: # TODO
329 # comando_ejecutado.exito = False
330 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
331 # if self.rechazar_si_falla:
332 # entrega.exito = False
333 # if self.terminar_si_falla: # TODO
334 # raise ExecutionFailure(self)
335 # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
336 # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
337 # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
338 # for archivo in self.archivos_salida:
339 # pass # TODO hacer diff
340 # if archivos_mal: # TODO
341 # comando_ejecutado.exito = False
342 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
343 # if self.rechazar_si_falla:
344 # entrega.exito = False
345 # if self.terminar_si_falla: # TODO
346 # raise ExecutionFailure(self)
348 # comando_ejecutado.exito = True
349 # comando_ejecutado.observaciones += 'xxx OK' # TODO
350 comando_ejecutado.exito = True
351 comando_ejecutado.observaciones += 'xxx OK' # TODO
352 ComandoFuente.ejecutar = ejecutar_comando_fuente
355 def ejecutar_comando_prueba(self, path, prueba): #{{{
356 log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
360 unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
361 unzip(self.archivos_entrada, path) # TODO try/except
362 comando_ejecutado = prueba.add_comando_ejecutado(self)
363 # TODO ejecutar en chroot (path)
364 comando_ejecutado.fin = datetime.now()
365 # if no_anda_ejecucion: # TODO
366 # comando_ejecutado.exito = False
367 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
368 # if self.rechazar_si_falla:
369 # entrega.exito = False
370 # if self.terminar_si_falla: # TODO
371 # raise ExecutionFailure(self) # TODO info de error
372 # for archivo in self.archivos_salida:
373 # pass # TODO hacer diff
374 # if archivos_mal: # TODO
375 # comando_ejecutado.exito = False
376 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
377 # if self.rechazar_si_falla:
378 # entrega.exito = False
379 # if self.terminar_si_falla: # TODO
380 # raise ExecutionFailure(comando=self) # TODO info de error
382 # comando_ejecutado.exito = True
383 # comando_ejecutado.observaciones += 'xxx OK' # TODO
384 comando_ejecutado.exito = True
385 comando_ejecutado.observaciones += 'xxx OK' # TODO
386 ComandoPrueba.ejecutar = ejecutar_comando_prueba