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
13 import resource as rsrc
16 # Ahora somos mortales
17 euid = config.get('sercom.tester.uid', 65534)
18 egid = config.get('sercom.tester.gid', 65534)
22 log = logging.getLogger('sercom.tester')
24 class CalledProcessError(Exception): #{{{ Python 2.5 forward-compatibility
25 """This exception is raised when a process run by check_call() returns
26 a non-zero exit status. The exit status will be stored in the
27 returncode attribute."""
28 def __init__(self, returncode, cmd):
29 self.returncode = returncode
32 return ("Command '%s' returned non-zero exit status %d"
33 % (self.cmd, self.returncode))
36 def check_call(*popenargs, **kwargs): #{{{ Python 2.5 forward-compatibility
37 """Run command with arguments. Wait for command to complete. If
38 the exit code was zero then return, otherwise raise
39 CalledProcessError. The CalledProcessError object will have the
40 return code in the returncode attribute.
41 ret = call(*popenargs, **kwargs)
43 The arguments are the same as for the Popen constructor. Example:
45 check_call(["ls", "-l"])
47 retcode = call(*popenargs, **kwargs)
48 cmd = kwargs.get("args")
52 raise CalledProcessError(retcode, cmd)
56 class Error(StandardError): pass
58 class ExecutionFailure(Error, RuntimeError): pass
60 class RsyncError(Error, EnvironmentError): pass
62 error_interno = _(u'\n**Error interno al preparar la entrega.**')
64 def unzip(bytes, dst): # {{{
65 log.debug(_(u'Intentando descomprimir en %s'), dst)
68 zfile = ZipFile(StringIO(bytes), 'r')
69 for f in zfile.namelist():
70 if f.endswith(os.sep):
71 log.debug(_(u'Creando directorio %s'), f)
72 os.mkdir(join(dst, f))
74 log.debug(_(u'Descomprimiendo archivo %s'), f)
75 file(join(dst, f), 'w').write(zfile.read(f))
78 class SecureProcess(object): #{{{
83 max_cant_archivos = 5,
84 max_cant_procesos = 0,
85 max_locks_memoria = 0,
87 gid = config.get('sercom.tester.chroot.gid', 65534)
88 uid = config.get('sercom.tester.chroot.uid', 65534)
90 # XXX probar! make de un solo archivo lleva nproc=100 y nofile=15
91 def __init__(self, comando, chroot, cwd):
92 self.comando = comando
95 def __getattr__(self, name):
96 if getattr(self.comando, name) is not None:
97 return getattr(self.comando, name)
98 return config.get('sercom.tester.limits.' + name, self.default[name])
100 x2 = lambda x: (x, x)
101 os.chroot(self.chroot)
105 rsrc.setrlimit(rsrc.RLIMIT_CPU, x2(self.max_tiempo_cpu))
106 rsrc.setrlimit(rsrc.RLIMIT_AS, x2(self.max_memoria*self.MB))
107 rsrc.setrlimit(rsrc.RLIMIT_FSIZE, x2(self.max_tam_archivo*self.MB)) # XXX calcular en base a archivos esperados?
108 rsrc.setrlimit(rsrc.RLIMIT_NOFILE, x2(self.max_cant_archivos)) #XXX Obtener de archivos esperados?
109 rsrc.setrlimit(rsrc.RLIMIT_NPROC, x2(self.max_cant_procesos))
110 rsrc.setrlimit(rsrc.RLIMIT_MEMLOCK, x2(self.max_locks_memoria))
111 rsrc.setrlimit(rsrc.RLIMIT_CORE, x2(0))
112 log.debug('Proceso segurizado: chroot=%s, cwd=%s, uid=%s, gid=%s, '
113 'cpu=%s, as=%s, fsize=%s, nofile=%s, nproc=%s, memlock=%s',
114 self.chroot, self.cwd, self.uid, self.gid, self.max_tiempo_cpu,
115 self.max_memoria*self.MB, self.max_tam_archivo*self.MB,
116 self.max_cant_archivos, self.max_cant_procesos,
117 self.max_locks_memoria)
118 # Tratamos de forzar un sync para que entre al sleep del padre FIXME
123 class Tester(object): #{{{
125 def __init__(self, name, path, home, queue): #{{{ y properties
132 def build_path(self):
133 return join(self.chroot, self.home, 'build')
137 return join(self.chroot, self.home, 'test')
141 return join(self.path, 'chroot_' + self.name)
145 def orig_chroot(self):
146 return join(self.path, 'chroot')
149 entrega_id = self.queue.get() # blocking
150 while entrega_id is not None:
151 entrega = Entrega.get(entrega_id)
152 log.debug(_(u'Nueva entrega para probar en tester %s: %s'),
155 log.debug(_(u'Fin de pruebas de: %s'), entrega)
156 entrega_id = self.queue.get() # blocking
159 def test(self, entrega): #{{{
160 log.debug(_(u'Tester.test(entrega=%s)'), entrega)
161 entrega.inicio_tareas = datetime.now()
164 self.setup_chroot(entrega)
165 self.ejecutar_tareas_fuente(entrega)
166 self.ejecutar_tareas_prueba(entrega)
167 self.clean_chroot(entrega)
168 except ExecutionFailure, e:
169 entrega.correcta = False
170 log.info(_(u'Entrega incorrecta: %s'), entrega)
172 if isinstance(e, SystemExit): raise
173 entrega.observaciones += error_interno
174 log.exception(_(u'Hubo una excepción inesperada: %s'), e)
176 entrega.observaciones += error_interno
177 log.exception(_(u'Hubo una excepción inesperada desconocida'))
179 entrega.correcta = True
180 log.debug(_(u'Entrega correcta: %s'), entrega)
182 entrega.fin_tareas = datetime.now()
185 def setup_chroot(self, entrega): #{{{ y clean_chroot()
186 log.debug(_(u'Tester.setup_chroot(entrega=%s)'), entrega.shortrepr())
187 rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
188 '--archive', '--acls', '--delete-during', '--force', # TODO config
189 join(self.orig_chroot, ''), self.chroot)
190 log.debug(_(u'Ejecutando: %s'), ' '.join(rsync))
191 os.seteuid(0) # Dios! (para chroot)
196 os.setegid(egid) # Mortal de nuevo
198 unzip(entrega.archivos, self.build_path)
200 def clean_chroot(self, entrega):
201 log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
202 pass # Se limpia con el próximo rsync
205 def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
206 log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
208 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
209 if isinstance(t, TareaFuente)]
211 tarea.ejecutar(self.build_path, entrega)
213 def ejecutar_tareas_prueba(self, entrega):
214 log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
216 for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
217 caso.ejecutar(self.test_path, entrega)
222 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
223 log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
225 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
226 if isinstance(t, TareaPrueba)]
227 prueba = entrega.add_prueba(self)
231 tarea.ejecutar(path, prueba)
232 except ExecutionFailure, e:
233 prueba.pasada = False
234 if self.rechazar_si_falla:
235 entrega.exito = False
236 if self.terminar_si_falla:
237 raise ExecutionError(e.comando, e.tarea, prueba)
241 prueba.fin = datetime.now()
242 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
245 def ejecutar_tarea_fuente(self, path, entrega): #{{{
246 log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
249 for cmd in self.comandos:
250 cmd.ejecutar(path, entrega)
251 except ExecutionFailure, e:
252 if self.rechazar_si_falla:
253 entrega.exito = False
254 if self.terminar_si_falla:
255 raise ExecutionError(e.comando, tarea)
256 TareaFuente.ejecutar = ejecutar_tarea_fuente
259 def ejecutar_tarea_prueba(self, path, prueba): #{{{
260 log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
263 for cmd in self.comandos:
264 cmd.ejecutar(path, prueba)
265 except ExecutionFailure, e:
266 if self.rechazar_si_falla:
268 if self.terminar_si_falla:
269 raise ExecutionError(e.comando, tarea)
270 TareaPrueba.ejecutar = ejecutar_tarea_prueba
273 def ejecutar_comando_fuente(self, path, entrega): #{{{
274 log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
276 unzip(self.archivos_entrada, path) # TODO try/except
277 comando_ejecutado = entrega.add_comando_ejecutado(self)
278 # Abro archivos para fds básicos (FIXME)
279 options = dict(close_fds=True, stdin=None, stdout=None, stderr=None,
280 preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build'))
281 log.debug(_(u'Ejecutando %s'), ' '.join(self.comando))
282 os.seteuid(0) # Dios! (para chroot)
286 proc = Popen(self.comando, **options)
288 os.setegid(egid) # Mortal de nuevo
290 except Exception, e: # FIXME poner en el manejo de exceptiones estandar
291 if hasattr(e, 'child_traceback'):
292 log.error(_(u'Error en el hijo: %s'), e.child_traceback)
295 comando_ejecutado.fin = datetime.now()
296 # if no_anda_ejecucion: # TODO
297 # comando_ejecutado.exito = False
298 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
299 # if self.rechazar_si_falla:
300 # entrega.exito = False
301 # if self.terminar_si_falla: # TODO
302 # raise ExecutionFailure(self)
303 # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
304 # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
305 # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
306 # for archivo in self.archivos_salida:
307 # pass # TODO hacer diff
308 # if archivos_mal: # TODO
309 # comando_ejecutado.exito = False
310 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
311 # if self.rechazar_si_falla:
312 # entrega.exito = False
313 # if self.terminar_si_falla: # TODO
314 # raise ExecutionFailure(self)
316 # comando_ejecutado.exito = True
317 # comando_ejecutado.observaciones += 'xxx OK' # TODO
318 comando_ejecutado.exito = True
319 comando_ejecutado.observaciones += 'xxx OK' # TODO
320 ComandoFuente.ejecutar = ejecutar_comando_fuente
323 def ejecutar_comando_prueba(self, path, prueba): #{{{
324 log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
328 unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
329 unzip(self.archivos_entrada, path) # TODO try/except
330 comando_ejecutado = prueba.add_comando_ejecutado(self)
331 # TODO ejecutar en chroot (path)
332 comando_ejecutado.fin = datetime.now()
333 # if no_anda_ejecucion: # TODO
334 # comando_ejecutado.exito = False
335 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
336 # if self.rechazar_si_falla:
337 # entrega.exito = False
338 # if self.terminar_si_falla: # TODO
339 # raise ExecutionFailure(self) # TODO info de error
340 # for archivo in self.archivos_salida:
341 # pass # TODO hacer diff
342 # if archivos_mal: # TODO
343 # comando_ejecutado.exito = False
344 # comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
345 # if self.rechazar_si_falla:
346 # entrega.exito = False
347 # if self.terminar_si_falla: # TODO
348 # raise ExecutionFailure(comando=self) # TODO info de error
350 # comando_ejecutado.exito = True
351 # comando_ejecutado.observaciones += 'xxx OK' # TODO
352 comando_ejecutado.exito = True
353 comando_ejecutado.observaciones += 'xxx OK' # TODO
354 ComandoPrueba.ejecutar = ejecutar_comando_prueba