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 comando_ejecutado = entrega.add_comando_ejecutado(self)
307 unzip(self.archivos_entrada, path, # TODO try/except
308 dict(__stdin__='/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id)) # TODO /var/run/sercom
313 preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build')
315 if os.path.exists('/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id): # TODO
316 options['stdin'] = file('/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id, 'r') # TODO
317 if self.guardar_stdouterr:
318 options['stdout'] = file('/tmp/sercom.tester.%s.stdouterr'
319 % comando_ejecutado.id, 'w') #TODO /var/lib/sercom?
320 options['stderr'] = sp.STDOUT
322 if self.guardar_stdout:
323 options['stdout'] = file('/tmp/sercom.tester.%s.stdout'
324 % comando_ejecutado.id, 'w') #TODO /var/lib/sercom?
325 if self.guardar_stderr:
326 options['stderr'] = file('/tmp/sercom.tester.%s.stderr'
327 % comando_ejecutado.id, 'w') #TODO /var/lib/sercom?
328 log.debug(_(u'Ejecutando como root: %s'), self.comando)
329 os.seteuid(0) # Dios! (para chroot)
333 proc = sp.Popen(self.comando, **options)
335 log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
336 user_info.user, user_info.group, user_info.uid, user_info.gid)
337 os.setegid(user_info.gid) # Mortal de nuevo
338 os.seteuid(user_info.uid)
340 if hasattr(e, 'child_traceback'):
341 log.error(_(u'Error en el hijo: %s'), e.child_traceback)
343 proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
344 comando_ejecutado.fin = datetime.now()
346 zip = ZipFile(buffer, 'w')
347 if self.guardar_stdouterr:
348 zip.write('/tmp/sercom.tester.%s.stdouterr'
349 % comando_ejecutado.id, '__stdouterr__')
351 if self.guardar_stdout:
352 azipwrite('/tmp/sercom.tester.%s.stdout'
353 % comando_ejecutado.id, '__stdout__')
354 if self.guardar_stderr:
355 zip.write('/tmp/sercom.tester.%s.stderr'
356 % comando_ejecutado.id, '__stderr__')
358 comando_ejecutado.archivos_guardados = buffer.getvalue()
360 # if no_anda_ejecucion: # TODO
361 # comando_ejecutado.exito = False
362 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
363 # if self.rechazar_si_falla:
364 # entrega.exito = False
365 # if self.terminar_si_falla: # TODO
366 # raise ExecutionFailure(self)
367 # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
368 # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
369 # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
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 mas info
375 # if self.rechazar_si_falla:
376 # entrega.exito = False
377 # if self.terminar_si_falla: # TODO
378 # raise ExecutionFailure(self)
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 ComandoFuente.ejecutar = ejecutar_comando_fuente
387 def ejecutar_comando_prueba(self, path, prueba): #{{{
388 log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
392 unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
393 unzip(self.archivos_entrada, path) # TODO try/except
394 comando_ejecutado = prueba.add_comando_ejecutado(self)
395 # TODO ejecutar en chroot (path)
396 comando_ejecutado.fin = datetime.now()
397 # if no_anda_ejecucion: # TODO
398 # comando_ejecutado.exito = False
399 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
400 # if self.rechazar_si_falla:
401 # entrega.exito = False
402 # if self.terminar_si_falla: # TODO
403 # raise ExecutionFailure(self) # TODO info de error
404 # for archivo in self.archivos_salida:
405 # pass # TODO hacer diff
406 # if archivos_mal: # TODO
407 # comando_ejecutado.exito = False
408 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
409 # if self.rechazar_si_falla:
410 # entrega.exito = False
411 # if self.terminar_si_falla: # TODO
412 # raise ExecutionFailure(comando=self) # TODO info de error
414 # comando_ejecutado.exito = True
415 # comando_ejecutado.observaciones += 'xxx OK' # TODO
416 comando_ejecutado.exito = True
417 comando_ejecutado.observaciones += 'xxx OK' # TODO
418 ComandoPrueba.ejecutar = ejecutar_comando_prueba