]> git.llucax.com Git - software/sercom.git/blob - sercom/tester.py
Avanzar sobre implementación de Tester.
[software/sercom.git] / sercom / tester.py
1 # vim: set et sw=4 sts=4 encoding=utf-8 foldmethod=marker:
2
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
13 import resource as rsrc
14 import logging
15
16 # Ahora somos mortales
17 euid = config.get('sercom.tester.uid', 65534)
18 egid = config.get('sercom.tester.gid', 65534)
19 os.setegid(egid)
20 os.seteuid(euid)
21
22 log = logging.getLogger('sercom.tester')
23
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
30         self.cmd = cmd
31     def __str__(self):
32         return ("Command '%s' returned non-zero exit status %d"
33             % (self.cmd, self.returncode))
34 #}}}
35
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)
42
43     The arguments are the same as for the Popen constructor.  Example:
44
45     check_call(["ls", "-l"])
46     """
47     retcode = call(*popenargs, **kwargs)
48     cmd = kwargs.get("args")
49     if cmd is None:
50         cmd = popenargs[0]
51     if retcode:
52         raise CalledProcessError(retcode, cmd)
53     return retcode
54 #}}}
55
56 class Error(StandardError): pass
57
58 class ExecutionFailure(Error, RuntimeError): pass
59
60 class RsyncError(Error, EnvironmentError): pass
61
62 error_interno = _(u'\n**Error interno al preparar la entrega.**')
63
64 def unzip(bytes, dst): # {{{
65     log.debug(_(u'Intentando descomprimir en %s'), dst)
66     if bytes is None:
67         return
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))
73         else:
74             log.debug(_(u'Descomprimiendo archivo %s'), f)
75             file(join(dst, f), 'w').write(zfile.read(f))
76 #}}}
77
78 class SecureProcess(object): #{{{
79     default = dict(
80         max_tiempo_cpu      = 120,
81         max_memoria         = 16,
82         max_tam_archivo     = 5,
83         max_cant_archivos   = 5,
84         max_cant_procesos   = 0,
85         max_locks_memoria   = 0,
86     )
87     gid = config.get('sercom.tester.chroot.gid', 65534)
88     uid = config.get('sercom.tester.chroot.uid', 65534)
89     MB = 1048576
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
93             self.chroot = chroot
94             self.cwd = cwd
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])
99     def __call__(self):
100         x2 = lambda x: (x, x)
101         os.chroot(self.chroot)
102         os.chdir(self.cwd)
103         os.setgid(self.gid)
104         os.setuid(self.uid)
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
119         import time
120         time.sleep(0)
121 #}}}
122
123 class Tester(object): #{{{
124
125     def __init__(self, name, path, home, queue): #{{{ y properties
126         self.name = name
127         self.path = path
128         self.home = home
129         self.queue = queue
130
131     @property
132     def build_path(self):
133         return join(self.chroot, self.home, 'build')
134
135     @property
136     def test_path(self):
137         return join(self.chroot, self.home, 'test')
138
139     @property
140     def chroot(self):
141         return join(self.path, 'chroot_' + self.name)
142     #}}}
143
144     @property
145     def orig_chroot(self):
146         return join(self.path, 'chroot')
147
148     def run(self): #{{{
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'),
153                 self.name, entrega)
154             self.test(entrega)
155             log.debug(_(u'Fin de pruebas de: %s'), entrega)
156             entrega_id = self.queue.get() # blocking
157     #}}}
158
159     def test(self, entrega): #{{{
160         log.debug(_(u'Tester.test(entrega=%s)'), entrega)
161         entrega.inicio_tareas = datetime.now()
162         try:
163             try:
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)
171             except Exception, e:
172                 if isinstance(e, SystemExit): raise
173                 entrega.observaciones += error_interno
174                 log.exception(_(u'Hubo una excepción inesperada: %s'), e)
175             except:
176                 entrega.observaciones += error_interno
177                 log.exception(_(u'Hubo una excepción inesperada desconocida'))
178             else:
179                 entrega.correcta = True
180                 log.debug(_(u'Entrega correcta: %s'), entrega)
181         finally:
182             entrega.fin_tareas = datetime.now()
183     #}}}
184
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)
192         os.setegid(0)
193         try:
194             check_call(rsync)
195         finally:
196             os.setegid(egid) # Mortal de nuevo
197             os.seteuid(euid)
198         unzip(entrega.archivos, self.build_path)
199
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
203     #}}}
204
205     def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
206         log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
207             entrega.shortrepr())
208         tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
209                     if isinstance(t, TareaFuente)]
210         for tarea in tareas:
211             tarea.ejecutar(self.build_path, entrega)
212
213     def ejecutar_tareas_prueba(self, entrega):
214         log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
215             entrega.shortrepr())
216         for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
217             caso.ejecutar(self.test_path, entrega)
218     #}}}
219
220 #}}}
221
222 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
223     log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
224         entrega.shortrepr())
225     tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
226                 if isinstance(t, TareaPrueba)]
227     prueba = entrega.add_prueba(self)
228     try:
229         try:
230             for tarea in tareas:
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)
238         else:
239             prueba.pasada = True
240     finally:
241         prueba.fin = datetime.now()
242 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
243 #}}}
244
245 def ejecutar_tarea_fuente(self, path, entrega): #{{{
246     log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
247         entrega.shortrepr())
248     try:
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
257 #}}}
258
259 def ejecutar_tarea_prueba(self, path, prueba): #{{{
260     log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
261         prueba.shortrepr())
262     try:
263         for cmd in self.comandos:
264             cmd.ejecutar(path, prueba)
265     except ExecutionFailure, e:
266         if self.rechazar_si_falla:
267             prueba.exito = False
268         if self.terminar_si_falla:
269             raise ExecutionError(e.comando, tarea)
270 TareaPrueba.ejecutar = ejecutar_tarea_prueba
271 #}}}
272
273 def ejecutar_comando_fuente(self, path, entrega): #{{{
274     log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
275         entrega.shortrepr())
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)
283     os.setegid(0)
284     try:
285         try:
286             proc = Popen(self.comando, **options)
287         finally:
288             os.setegid(egid) # Mortal de nuevo
289             os.seteuid(euid)
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)
293         raise
294     proc.wait()
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)
315 #    else:
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
321 #}}}
322
323 def ejecutar_comando_prueba(self, path, prueba): #{{{
324     log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
325         prueba.shortrepr())
326     rmtree(path)
327     os.mkdir(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
349 #    else:
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
355 #}}}
356