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 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): #{{{ 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 = call(*popenargs, **kwargs)
49 cmd = kwargs.get("args")
53 raise CalledProcessError(retcode, cmd)
59 class CalledProcessError(Exception): #{{{ Python 2.5 forward-compatibility
60 """This exception is raised when a process run by check_call() returns
61 a non-zero exit status. The exit status will be stored in the
62 returncode attribute."""
63 def __init__(self, returncode, cmd):
64 self.returncode = returncode
67 return ("Command '%s' returned non-zero exit status %d"
68 % (self.cmd, self.returncode))
71 class Error(StandardError): pass
73 class ExecutionFailure(Error, RuntimeError): pass
75 class RsyncError(Error, EnvironmentError): pass
79 def unzip(bytes, dst): # {{{
80 log.debug(_(u'Intentando descomprimir en %s'), dst)
83 zfile = ZipFile(StringIO(bytes), 'r')
84 for f in zfile.namelist():
85 if f.endswith(os.sep):
86 log.debug(_(u'Creando directorio %s'), f)
87 os.mkdir(join(dst, f))
89 log.debug(_(u'Descomprimiendo archivo %s'), f)
90 file(join(dst, f), 'w').write(zfile.read(f))
93 class SecureProcess(object): #{{{
98 max_cant_archivos = 5,
99 max_cant_procesos = 0,
100 max_locks_memoria = 0,
102 uid = config.get('sercom.tester.chroot.user', 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 uinfo = UserInfo(self.uid)
119 os.setuid(uinfo.uid) # Somos mortales irreversiblemente
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, uinfo.user, uinfo.uid,
130 uinfo.group, uinfo.gid, self.max_tiempo_cpu, self.max_memoria,
131 self.max_tam_archivo, self.max_cant_archivos,
132 self.max_cant_procesos, 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 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
147 user_info.user, user_info.group, user_info.uid, user_info.gid)
148 os.setegid(user_info.gid)
149 os.seteuid(user_info.uid)
152 def build_path(self):
153 return join(self.chroot, self.home, 'build')
157 return join(self.chroot, self.home, 'test')
161 return join(self.path, 'chroot_' + self.name)
165 def orig_chroot(self):
166 return join(self.path, 'chroot')
169 entrega_id = self.queue.get() # blocking
170 while entrega_id is not None:
171 entrega = Entrega.get(entrega_id)
172 log.debug(_(u'Nueva entrega para probar en tester %s: %s'),
175 log.debug(_(u'Fin de pruebas de: %s'), entrega)
176 entrega_id = self.queue.get() # blocking
179 def test(self, entrega): #{{{
180 log.debug(_(u'Tester.test(entrega=%s)'), entrega)
181 entrega.inicio_tareas = datetime.now()
184 self.setup_chroot(entrega)
185 self.ejecutar_tareas_fuente(entrega)
186 self.ejecutar_tareas_prueba(entrega)
187 self.clean_chroot(entrega)
188 except ExecutionFailure, e:
189 entrega.correcta = False
190 log.info(_(u'Entrega incorrecta: %s'), entrega)
192 if isinstance(e, SystemExit): raise
193 entrega.observaciones += error_interno
194 log.exception(_(u'Hubo una excepción inesperada: %s'), e)
196 entrega.observaciones += error_interno
197 log.exception(_(u'Hubo una excepción inesperada desconocida'))
199 entrega.correcta = True
200 log.debug(_(u'Entrega correcta: %s'), entrega)
202 entrega.fin_tareas = datetime.now()
205 def setup_chroot(self, entrega): #{{{ y clean_chroot()
206 log.debug(_(u'Tester.setup_chroot(entrega=%s)'), entrega.shortrepr())
207 rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
208 '--archive', '--acls', '--delete-during', '--force', # TODO config
209 join(self.orig_chroot, ''), self.chroot)
210 log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
211 os.seteuid(0) # Dios! (para chroot)
216 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
217 user_info.user, user_info.group, user_info.uid, user_info.gid)
218 os.setegid(user_info.gid) # Mortal de nuevo
219 os.seteuid(user_info.uid)
220 unzip(entrega.archivos, self.build_path)
222 def clean_chroot(self, entrega):
223 log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
224 pass # Se limpia con el próximo rsync
227 def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
228 log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
230 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
231 if isinstance(t, TareaFuente)]
233 tarea.ejecutar(self.build_path, entrega)
235 def ejecutar_tareas_prueba(self, entrega):
236 log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
238 for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
239 caso.ejecutar(self.test_path, entrega)
244 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
245 log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
247 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
248 if isinstance(t, TareaPrueba)]
249 prueba = entrega.add_prueba(self)
253 tarea.ejecutar(path, prueba)
254 except ExecutionFailure, e:
256 if self.rechazar_si_falla:
257 entrega.exito = False
258 if self.terminar_si_falla:
259 raise ExecutionError(e.comando, e.tarea, prueba)
263 prueba.fin = datetime.now()
264 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
267 def ejecutar_tarea_fuente(self, path, entrega): #{{{
268 log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
271 for cmd in self.comandos:
272 cmd.ejecutar(path, entrega)
273 except ExecutionFailure, e:
274 if self.rechazar_si_falla:
275 entrega.exito = False
276 if self.terminar_si_falla:
277 raise ExecutionError(e.comando, tarea)
278 TareaFuente.ejecutar = ejecutar_tarea_fuente
281 def ejecutar_tarea_prueba(self, path, prueba): #{{{
282 log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
285 for cmd in self.comandos:
286 cmd.ejecutar(path, prueba)
287 except ExecutionFailure, e:
288 if self.rechazar_si_falla:
290 if self.terminar_si_falla:
291 raise ExecutionError(e.comando, tarea)
292 TareaPrueba.ejecutar = ejecutar_tarea_prueba
295 def ejecutar_comando_fuente(self, path, entrega): #{{{
296 log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
298 unzip(self.archivos_entrada, path) # TODO try/except
299 comando_ejecutado = entrega.add_comando_ejecutado(self)
300 # Abro archivos para fds básicos (FIXME)
307 preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build')
309 log.debug(_(u'Ejecutando como root: %s'), self.comando)
310 os.seteuid(0) # Dios! (para chroot)
314 proc = Popen(self.comando, **options)
316 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
317 user_info.user, user_info.group, user_info.uid, user_info.gid)
318 os.setegid(user_info.gid) # Mortal de nuevo
319 os.seteuid(user_info.uid)
320 except Exception, e: # FIXME poner en el manejo de exceptiones estandar
321 if hasattr(e, 'child_traceback'):
322 log.error(_(u'Error en el hijo: %s'), e.child_traceback)
325 comando_ejecutado.fin = datetime.now()
326 # if no_anda_ejecucion: # TODO
327 # comando_ejecutado.exito = False
328 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
329 # if self.rechazar_si_falla:
330 # entrega.exito = False
331 # if self.terminar_si_falla: # TODO
332 # raise ExecutionFailure(self)
333 # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
334 # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
335 # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
336 # for archivo in self.archivos_salida:
337 # pass # TODO hacer diff
338 # if archivos_mal: # TODO
339 # comando_ejecutado.exito = False
340 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
341 # if self.rechazar_si_falla:
342 # entrega.exito = False
343 # if self.terminar_si_falla: # TODO
344 # raise ExecutionFailure(self)
346 # comando_ejecutado.exito = True
347 # comando_ejecutado.observaciones += 'xxx OK' # TODO
348 comando_ejecutado.exito = True
349 comando_ejecutado.observaciones += 'xxx OK' # TODO
350 ComandoFuente.ejecutar = ejecutar_comando_fuente
353 def ejecutar_comando_prueba(self, path, prueba): #{{{
354 log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
358 unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
359 unzip(self.archivos_entrada, path) # TODO try/except
360 comando_ejecutado = prueba.add_comando_ejecutado(self)
361 # TODO ejecutar en chroot (path)
362 comando_ejecutado.fin = datetime.now()
363 # if no_anda_ejecucion: # TODO
364 # comando_ejecutado.exito = False
365 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
366 # if self.rechazar_si_falla:
367 # entrega.exito = False
368 # if self.terminar_si_falla: # TODO
369 # raise ExecutionFailure(self) # TODO info de error
370 # for archivo in self.archivos_salida:
371 # pass # TODO hacer diff
372 # if archivos_mal: # TODO
373 # comando_ejecutado.exito = False
374 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
375 # if self.rechazar_si_falla:
376 # entrega.exito = False
377 # if self.terminar_si_falla: # TODO
378 # raise ExecutionFailure(comando=self) # TODO info de error
380 # comando_ejecutado.exito = True
381 # comando_ejecutado.observaciones += 'xxx OK' # TODO
382 comando_ejecutado.exito = True
383 comando_ejecutado.observaciones += 'xxx OK' # TODO
384 ComandoPrueba.ejecutar = ejecutar_comando_prueba