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, default_dst='.', specific_dst=dict()): # {{{
82 """Descomprime un buffer de datos en formato ZIP.
83 Los archivos se descomprimen en default_dst a menos que exista una entrada
84 en specific_dst cuya clave sea el nombre de archivo a descomprimir, en
85 cuyo caso, se descomprime usando como destino el valor de dicha clave.
87 log.debug(_(u'Intentando descomprimir'))
90 zfile = ZipFile(StringIO(bytes), 'r')
91 for f in zfile.namelist():
92 dst = join(specific_dst.get(f, default_dst), f)
93 if f.endswith(os.sep):
94 log.debug(_(u'Creando directorio "%s" en "%s"'), f, dst)
97 log.debug(_(u'Descomprimiendo archivo "%s" en "%s"'), f, dst)
98 file(dst, 'w').write(zfile.read(f))
102 class SecureProcess(object): #{{{
104 max_tiempo_cpu = 120,
107 max_cant_archivos = 5,
108 max_cant_procesos = 0,
109 max_locks_memoria = 0,
111 uid = config.get('sercom.tester.chroot.user', 65534)
113 # XXX probar! make de un solo archivo lleva nproc=100 y nofile=15
114 def __init__(self, comando, chroot, cwd):
115 self.comando = comando
118 log.debug('Proceso segurizado: chroot=%s, cwd=%s, user=%s, cpu=%s, '
119 'as=%sMiB, fsize=%sMiB, nofile=%s, nproc=%s, memlock=%s',
120 self.chroot, self.cwd, self.uid, self.max_tiempo_cpu,
121 self.max_memoria, self.max_tam_archivo, self.max_cant_archivos,
122 self.max_cant_procesos, self.max_locks_memoria)
123 def __getattr__(self, name):
124 if getattr(self.comando, name) is not None:
125 return getattr(self.comando, name)
126 return config.get('sercom.tester.limits.' + name, self.default[name])
128 x2 = lambda x: (x, x)
129 os.chroot(self.chroot)
131 uinfo = UserInfo(self.uid)
133 os.setuid(uinfo.uid) # Somos mortales irreversiblemente
134 rsrc.setrlimit(rsrc.RLIMIT_CPU, x2(self.max_tiempo_cpu))
135 rsrc.setrlimit(rsrc.RLIMIT_AS, x2(self.max_memoria*self.MB))
136 rsrc.setrlimit(rsrc.RLIMIT_FSIZE, x2(self.max_tam_archivo*self.MB)) # XXX calcular en base a archivos esperados?
137 rsrc.setrlimit(rsrc.RLIMIT_NOFILE, x2(self.max_cant_archivos)) #XXX Obtener de archivos esperados?
138 rsrc.setrlimit(rsrc.RLIMIT_NPROC, x2(self.max_cant_procesos))
139 rsrc.setrlimit(rsrc.RLIMIT_MEMLOCK, x2(self.max_locks_memoria))
140 rsrc.setrlimit(rsrc.RLIMIT_CORE, x2(0))
141 # Tratamos de forzar un sync para que entre al sleep del padre FIXME
146 class Tester(object): #{{{
148 def __init__(self, name, path, home, queue): #{{{ y properties
153 # Ahora somos mortales (oid mortales)
154 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
155 user_info.user, user_info.group, user_info.uid, user_info.gid)
156 os.setegid(user_info.gid)
157 os.seteuid(user_info.uid)
160 def build_path(self):
161 return join(self.chroot, self.home, 'build')
165 return join(self.chroot, self.home, 'test')
169 return join(self.path, 'chroot_' + self.name)
172 def orig_chroot(self):
173 return join(self.path, 'chroot')
177 entrega_id = self.queue.get() # blocking
178 while entrega_id is not None:
179 entrega = Entrega.get(entrega_id)
180 log.debug(_(u'Nueva entrega para probar en tester %s: %s'),
183 log.debug(_(u'Fin de pruebas de: %s'), entrega)
184 entrega_id = self.queue.get() # blocking
187 def test(self, entrega): #{{{
188 log.debug(_(u'Tester.test(entrega=%s)'), entrega)
189 entrega.inicio_tareas = datetime.now()
192 self.setup_chroot(entrega)
193 self.ejecutar_tareas_fuente(entrega)
194 self.ejecutar_tareas_prueba(entrega)
195 self.clean_chroot(entrega)
196 except ExecutionFailure, e:
197 entrega.correcta = False
198 log.info(_(u'Entrega incorrecta: %s'), entrega)
200 if isinstance(e, SystemExit): raise
201 entrega.observaciones += error_interno
202 log.exception(_(u'Hubo una excepción inesperada: %s'), e)
204 entrega.observaciones += error_interno
205 log.exception(_(u'Hubo una excepción inesperada desconocida'))
207 entrega.correcta = True
208 log.debug(_(u'Entrega correcta: %s'), entrega)
210 entrega.fin_tareas = datetime.now()
213 def setup_chroot(self, entrega): #{{{ y clean_chroot()
214 log.debug(_(u'Tester.setup_chroot(entrega=%s)'), entrega.shortrepr())
215 rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
216 '--archive', '--acls', '--delete-during', '--force', # TODO config
217 join(self.orig_chroot, ''), self.chroot)
218 log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
219 os.seteuid(0) # Dios! (para chroot)
224 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
225 user_info.user, user_info.group, user_info.uid, user_info.gid)
226 os.setegid(user_info.gid) # Mortal de nuevo
227 os.seteuid(user_info.uid)
228 unzip(entrega.archivos, self.build_path)
230 def clean_chroot(self, entrega):
231 log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
232 pass # Se limpia con el próximo rsync
235 def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
236 log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
238 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
239 if isinstance(t, TareaFuente)]
241 tarea.ejecutar(self.build_path, entrega)
243 def ejecutar_tareas_prueba(self, entrega):
244 log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
246 for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
247 caso.ejecutar(self.test_path, entrega)
252 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
253 log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
255 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
256 if isinstance(t, TareaPrueba)]
257 prueba = entrega.add_prueba(self)
261 tarea.ejecutar(path, prueba)
262 except ExecutionFailure, e:
264 if self.rechazar_si_falla:
265 entrega.exito = False
266 if self.terminar_si_falla:
267 raise ExecutionError(e.comando, e.tarea, prueba)
271 prueba.fin = datetime.now()
272 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
275 def ejecutar_tarea_fuente(self, path, entrega): #{{{
276 log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
279 for cmd in self.comandos:
280 cmd.ejecutar(path, entrega)
281 except ExecutionFailure, e:
282 if self.rechazar_si_falla:
283 entrega.exito = False
284 if self.terminar_si_falla:
285 raise ExecutionError(e.comando, tarea)
286 TareaFuente.ejecutar = ejecutar_tarea_fuente
289 def ejecutar_tarea_prueba(self, path, prueba): #{{{
290 log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
293 for cmd in self.comandos:
294 cmd.ejecutar(path, prueba)
295 except ExecutionFailure, e:
296 if self.rechazar_si_falla:
298 if self.terminar_si_falla:
299 raise ExecutionError(e.comando, tarea)
300 TareaPrueba.ejecutar = ejecutar_tarea_prueba
303 def ejecutar_comando_fuente(self, path, entrega): #{{{
304 log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
306 unzip(self.archivos_entrada, path) # TODO try/except
307 comando_ejecutado = entrega.add_comando_ejecutado(self)
308 # Abro archivos para fds básicos (FIXME)
313 preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build')
315 if self.guardar_stdouterr:
316 options['stdout'] = file('/tmp/sercom.tester.%s.stdouterr'
317 % comando_ejecutado.id, 'w') #TODO /var/lib/sercom?
318 options['stderr'] = sp.STDOUT
320 if self.guardar_stdout:
321 options['stdout'] = file('/tmp/sercom.tester.%s.stdout'
322 % comando_ejecutado.id, 'w') #TODO /var/lib/sercom?
323 if self.guardar_stderr:
324 options['stderr'] = file('/tmp/sercom.tester.%s.stderr'
325 % comando_ejecutado.id, 'w') #TODO /var/lib/sercom?
326 log.debug(_(u'Ejecutando como root: %s'), self.comando)
327 os.seteuid(0) # Dios! (para chroot)
331 proc = sp.Popen(self.comando, **options)
333 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
334 user_info.user, user_info.group, user_info.uid, user_info.gid)
335 os.setegid(user_info.gid) # Mortal de nuevo
336 os.seteuid(user_info.uid)
338 if hasattr(e, 'child_traceback'):
339 log.error(_(u'Error en el hijo: %s'), e.child_traceback)
341 proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
342 comando_ejecutado.fin = datetime.now()
344 zip = ZipFile(buffer, 'w')
345 if self.guardar_stdouterr:
346 zip.write('/tmp/sercom.tester.%s.stdouterr'
347 % comando_ejecutado.id, '__stdouterr__')
349 if self.guardar_stdout:
350 azipwrite('/tmp/sercom.tester.%s.stdout'
351 % comando_ejecutado.id, '__stdout__')
352 if self.guardar_stderr:
353 zip.write('/tmp/sercom.tester.%s.stderr'
354 % comando_ejecutado.id, '__stderr__')
356 comando_ejecutado.archivos_guardados = buffer.getvalue()
358 # if no_anda_ejecucion: # TODO
359 # comando_ejecutado.exito = False
360 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
361 # if self.rechazar_si_falla:
362 # entrega.exito = False
363 # if self.terminar_si_falla: # TODO
364 # raise ExecutionFailure(self)
365 # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
366 # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
367 # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
368 # for archivo in self.archivos_salida:
369 # pass # TODO hacer diff
370 # if archivos_mal: # TODO
371 # comando_ejecutado.exito = False
372 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
373 # if self.rechazar_si_falla:
374 # entrega.exito = False
375 # if self.terminar_si_falla: # TODO
376 # raise ExecutionFailure(self)
378 # comando_ejecutado.exito = True
379 # comando_ejecutado.observaciones += 'xxx OK' # TODO
380 comando_ejecutado.exito = True
381 comando_ejecutado.observaciones += 'xxx OK' # TODO
382 ComandoFuente.ejecutar = ejecutar_comando_fuente
385 def ejecutar_comando_prueba(self, path, prueba): #{{{
386 log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
390 unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
391 unzip(self.archivos_entrada, path) # TODO try/except
392 comando_ejecutado = prueba.add_comando_ejecutado(self)
393 # TODO ejecutar en chroot (path)
394 comando_ejecutado.fin = datetime.now()
395 # if no_anda_ejecucion: # TODO
396 # comando_ejecutado.exito = False
397 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
398 # if self.rechazar_si_falla:
399 # entrega.exito = False
400 # if self.terminar_si_falla: # TODO
401 # raise ExecutionFailure(self) # TODO info de error
402 # for archivo in self.archivos_salida:
403 # pass # TODO hacer diff
404 # if archivos_mal: # TODO
405 # comando_ejecutado.exito = False
406 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
407 # if self.rechazar_si_falla:
408 # entrega.exito = False
409 # if self.terminar_si_falla: # TODO
410 # raise ExecutionFailure(comando=self) # TODO info de error
412 # comando_ejecutado.exito = True
413 # comando_ejecutado.observaciones += 'xxx OK' # TODO
414 comando_ejecutado.exito = True
415 comando_ejecutado.observaciones += 'xxx OK' # TODO
416 ComandoPrueba.ejecutar = ejecutar_comando_prueba