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:
255 prueba.pasada = False
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)
301 options = dict(close_fds=True, stdin=None, stdout=None, stderr=None,
302 preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build'))
303 log.debug(_(u'Ejecutando como root: %s'), ' '.join(self.comando))
304 os.seteuid(0) # Dios! (para chroot)
308 proc = Popen(self.comando, **options)
310 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
311 user_info.user, user_info.group, user_info.uid, user_info.gid)
312 os.setegid(user_info.gid) # Mortal de nuevo
313 os.seteuid(user_info.uid)
314 except Exception, e: # FIXME poner en el manejo de exceptiones estandar
315 if hasattr(e, 'child_traceback'):
316 log.error(_(u'Error en el hijo: %s'), e.child_traceback)
319 comando_ejecutado.fin = datetime.now()
320 # if no_anda_ejecucion: # TODO
321 # comando_ejecutado.exito = False
322 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
323 # if self.rechazar_si_falla:
324 # entrega.exito = False
325 # if self.terminar_si_falla: # TODO
326 # raise ExecutionFailure(self)
327 # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
328 # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
329 # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
330 # for archivo in self.archivos_salida:
331 # pass # TODO hacer diff
332 # if archivos_mal: # TODO
333 # comando_ejecutado.exito = False
334 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
335 # if self.rechazar_si_falla:
336 # entrega.exito = False
337 # if self.terminar_si_falla: # TODO
338 # raise ExecutionFailure(self)
340 # comando_ejecutado.exito = True
341 # comando_ejecutado.observaciones += 'xxx OK' # TODO
342 comando_ejecutado.exito = True
343 comando_ejecutado.observaciones += 'xxx OK' # TODO
344 ComandoFuente.ejecutar = ejecutar_comando_fuente
347 def ejecutar_comando_prueba(self, path, prueba): #{{{
348 log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
352 unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
353 unzip(self.archivos_entrada, path) # TODO try/except
354 comando_ejecutado = prueba.add_comando_ejecutado(self)
355 # TODO ejecutar en chroot (path)
356 comando_ejecutado.fin = datetime.now()
357 # if no_anda_ejecucion: # TODO
358 # comando_ejecutado.exito = False
359 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
360 # if self.rechazar_si_falla:
361 # entrega.exito = False
362 # if self.terminar_si_falla: # TODO
363 # raise ExecutionFailure(self) # TODO info de error
364 # for archivo in self.archivos_salida:
365 # pass # TODO hacer diff
366 # if archivos_mal: # TODO
367 # comando_ejecutado.exito = False
368 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
369 # if self.rechazar_si_falla:
370 # entrega.exito = False
371 # if self.terminar_si_falla: # TODO
372 # raise ExecutionFailure(comando=self) # TODO info de error
374 # comando_ejecutado.exito = True
375 # comando_ejecutado.observaciones += 'xxx OK' # TODO
376 comando_ejecutado.exito = True
377 comando_ejecutado.observaciones += 'xxx OK' # TODO
378 ComandoPrueba.ejecutar = ejecutar_comando_prueba