]> git.llucax.com Git - z.facultad/75.52/sercom.git/blob - sercom/tester.py
No importar símbolos de subprocess globalmente.
[z.facultad/75.52/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 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
14 import logging
15
16 log = logging.getLogger('sercom.tester')
17
18 error_interno = _(u'\n**Error interno al preparar la entrega.**')
19
20 class UserInfo(object): #{{{
21     def __init__(self, user):
22         try:
23             info = pwd.getpwnam(user)
24         except:
25             info = pwd.get(int(user))
26         self.user = info[0]
27         self.uid = info[2]
28         self.gid = info[3]
29         self.name = info[4]
30         self.home = info[5]
31         self.shell = info[6]
32         self.group = grp.getgrgid(self.gid)[0]
33 #}}}
34
35 user_info = UserInfo(config.get('sercom.tester.user', 65534))
36
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)
43
44     The arguments are the same as for the Popen constructor.  Example:
45
46     check_call(["ls", "-l"])
47     """
48     retcode = sp.call(*popenargs, **kwargs)
49     cmd = kwargs.get("args")
50     if cmd is None:
51         cmd = popenargs[0]
52     if retcode:
53         raise sp.CalledProcessError(retcode, cmd)
54     return retcode
55 sp.check_call = check_call
56 #}}}
57
58 #{{{ Excepciones
59
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
66         self.cmd = cmd
67     def __str__(self):
68         return ("Command '%s' returned non-zero exit status %d"
69             % (self.cmd, self.returncode))
70 sp.CalledProcessError = CalledProcessError
71 #}}}
72
73 class Error(StandardError): pass
74
75 class ExecutionFailure(Error, RuntimeError): pass
76
77 class RsyncError(Error, EnvironmentError): pass
78
79 #}}}
80
81 def unzip(bytes, dst): # {{{
82     log.debug(_(u'Intentando descomprimir en %s'), dst)
83     if bytes is None:
84         return
85     zfile = ZipFile(StringIO(bytes), 'r')
86     for f in zfile.namelist():
87         if f.endswith(os.sep):
88             log.debug(_(u'Creando directorio %s'), f)
89             os.mkdir(join(dst, f))
90         else:
91             log.debug(_(u'Descomprimiendo archivo %s'), f)
92             file(join(dst, f), 'w').write(zfile.read(f))
93 #}}}
94
95 class SecureProcess(object): #{{{
96     default = dict(
97         max_tiempo_cpu      = 120,
98         max_memoria         = 16,
99         max_tam_archivo     = 5,
100         max_cant_archivos   = 5,
101         max_cant_procesos   = 0,
102         max_locks_memoria   = 0,
103     )
104     uid = config.get('sercom.tester.chroot.user', 65534)
105     MB = 1048576
106     # XXX probar! make de un solo archivo lleva nproc=100 y nofile=15
107     def __init__(self, comando, chroot, cwd):
108             self.comando = comando
109             self.chroot = chroot
110             self.cwd = cwd
111     def __getattr__(self, name):
112         if getattr(self.comando, name) is not None:
113             return getattr(self.comando, name)
114         return config.get('sercom.tester.limits.' + name, self.default[name])
115     def __call__(self):
116         x2 = lambda x: (x, x)
117         os.chroot(self.chroot)
118         os.chdir(self.cwd)
119         uinfo = UserInfo(self.uid)
120         os.setgid(uinfo.gid)
121         os.setuid(uinfo.uid) # Somos mortales irreversiblemente
122         rsrc.setrlimit(rsrc.RLIMIT_CPU, x2(self.max_tiempo_cpu))
123         rsrc.setrlimit(rsrc.RLIMIT_AS, x2(self.max_memoria*self.MB))
124         rsrc.setrlimit(rsrc.RLIMIT_FSIZE, x2(self.max_tam_archivo*self.MB)) # XXX calcular en base a archivos esperados?
125         rsrc.setrlimit(rsrc.RLIMIT_NOFILE, x2(self.max_cant_archivos)) #XXX Obtener de archivos esperados?
126         rsrc.setrlimit(rsrc.RLIMIT_NPROC, x2(self.max_cant_procesos))
127         rsrc.setrlimit(rsrc.RLIMIT_MEMLOCK, x2(self.max_locks_memoria))
128         rsrc.setrlimit(rsrc.RLIMIT_CORE, x2(0))
129         log.debug('Proceso segurizado: chroot=%s, cwd=%s, user=%s(%s), '
130             'group=%s(%s), cpu=%s, as=%sMiB, fsize=%sMiB, nofile=%s, nproc=%s, '
131             'memlock=%s', self.chroot, self.cwd, uinfo.user, uinfo.uid,
132             uinfo.group, uinfo.gid, self.max_tiempo_cpu, self.max_memoria,
133             self.max_tam_archivo, self.max_cant_archivos,
134             self.max_cant_procesos, self.max_locks_memoria)
135         # Tratamos de forzar un sync para que entre al sleep del padre FIXME
136         import time
137         time.sleep(0)
138 #}}}
139
140 class Tester(object): #{{{
141
142     def __init__(self, name, path, home, queue): #{{{ y properties
143         self.name = name
144         self.path = path
145         self.home = home
146         self.queue = queue
147         # Ahora somos mortales (oid mortales)
148         log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
149             user_info.user, user_info.group, user_info.uid, user_info.gid)
150         os.setegid(user_info.gid)
151         os.seteuid(user_info.uid)
152
153     @property
154     def build_path(self):
155         return join(self.chroot, self.home, 'build')
156
157     @property
158     def test_path(self):
159         return join(self.chroot, self.home, 'test')
160
161     @property
162     def chroot(self):
163         return join(self.path, 'chroot_' + self.name)
164     #}}}
165
166     @property
167     def orig_chroot(self):
168         return join(self.path, 'chroot')
169
170     def run(self): #{{{
171         entrega_id = self.queue.get() # blocking
172         while entrega_id is not None:
173             entrega = Entrega.get(entrega_id)
174             log.debug(_(u'Nueva entrega para probar en tester %s: %s'),
175                 self.name, entrega)
176             self.test(entrega)
177             log.debug(_(u'Fin de pruebas de: %s'), entrega)
178             entrega_id = self.queue.get() # blocking
179     #}}}
180
181     def test(self, entrega): #{{{
182         log.debug(_(u'Tester.test(entrega=%s)'), entrega)
183         entrega.inicio_tareas = datetime.now()
184         try:
185             try:
186                 self.setup_chroot(entrega)
187                 self.ejecutar_tareas_fuente(entrega)
188                 self.ejecutar_tareas_prueba(entrega)
189                 self.clean_chroot(entrega)
190             except ExecutionFailure, e:
191                 entrega.correcta = False
192                 log.info(_(u'Entrega incorrecta: %s'), entrega)
193             except Exception, e:
194                 if isinstance(e, SystemExit): raise
195                 entrega.observaciones += error_interno
196                 log.exception(_(u'Hubo una excepción inesperada: %s'), e)
197             except:
198                 entrega.observaciones += error_interno
199                 log.exception(_(u'Hubo una excepción inesperada desconocida'))
200             else:
201                 entrega.correcta = True
202                 log.debug(_(u'Entrega correcta: %s'), entrega)
203         finally:
204             entrega.fin_tareas = datetime.now()
205     #}}}
206
207     def setup_chroot(self, entrega): #{{{ y clean_chroot()
208         log.debug(_(u'Tester.setup_chroot(entrega=%s)'), entrega.shortrepr())
209         rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
210             '--archive', '--acls', '--delete-during', '--force', # TODO config
211             join(self.orig_chroot, ''), self.chroot)
212         log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
213         os.seteuid(0) # Dios! (para chroot)
214         os.setegid(0)
215         try:
216             sp.check_call(rsync)
217         finally:
218             log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
219                 user_info.user, user_info.group, user_info.uid, user_info.gid)
220             os.setegid(user_info.gid) # Mortal de nuevo
221             os.seteuid(user_info.uid)
222         unzip(entrega.archivos, self.build_path)
223
224     def clean_chroot(self, entrega):
225         log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
226         pass # Se limpia con el próximo rsync
227     #}}}
228
229     def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
230         log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
231             entrega.shortrepr())
232         tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
233                     if isinstance(t, TareaFuente)]
234         for tarea in tareas:
235             tarea.ejecutar(self.build_path, entrega)
236
237     def ejecutar_tareas_prueba(self, entrega):
238         log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
239             entrega.shortrepr())
240         for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
241             caso.ejecutar(self.test_path, entrega)
242     #}}}
243
244 #}}}
245
246 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
247     log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
248         entrega.shortrepr())
249     tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
250                 if isinstance(t, TareaPrueba)]
251     prueba = entrega.add_prueba(self)
252     try:
253         try:
254             for tarea in tareas:
255                 tarea.ejecutar(path, prueba)
256         except ExecutionFailure, e:
257             prueba.exito = False
258             if self.rechazar_si_falla:
259                 entrega.exito = False
260             if self.terminar_si_falla:
261                 raise ExecutionError(e.comando, e.tarea, prueba)
262         else:
263             prueba.exito = True
264     finally:
265         prueba.fin = datetime.now()
266 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
267 #}}}
268
269 def ejecutar_tarea_fuente(self, path, entrega): #{{{
270     log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
271         entrega.shortrepr())
272     try:
273         for cmd in self.comandos:
274             cmd.ejecutar(path, entrega)
275     except ExecutionFailure, e:
276         if self.rechazar_si_falla:
277             entrega.exito = False
278         if self.terminar_si_falla:
279             raise ExecutionError(e.comando, tarea)
280 TareaFuente.ejecutar = ejecutar_tarea_fuente
281 #}}}
282
283 def ejecutar_tarea_prueba(self, path, prueba): #{{{
284     log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
285         prueba.shortrepr())
286     try:
287         for cmd in self.comandos:
288             cmd.ejecutar(path, prueba)
289     except ExecutionFailure, e:
290         if self.rechazar_si_falla:
291             prueba.exito = False
292         if self.terminar_si_falla:
293             raise ExecutionError(e.comando, tarea)
294 TareaPrueba.ejecutar = ejecutar_tarea_prueba
295 #}}}
296
297 def ejecutar_comando_fuente(self, path, entrega): #{{{
298     log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
299         entrega.shortrepr())
300     unzip(self.archivos_entrada, path) # TODO try/except
301     comando_ejecutado = entrega.add_comando_ejecutado(self)
302     # Abro archivos para fds básicos (FIXME)
303     options = dict(
304         close_fds=True,
305         stdin=None,
306         stdout=None,
307         stderr=None,
308         shell=True,
309         preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build')
310     )
311     log.debug(_(u'Ejecutando como root: %s'), self.comando)
312     os.seteuid(0) # Dios! (para chroot)
313     os.setegid(0)
314     try:
315         try:
316             proc = sp.Popen(self.comando, **options)
317         finally:
318             log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
319                 user_info.user, user_info.group, user_info.uid, user_info.gid)
320             os.setegid(user_info.gid) # Mortal de nuevo
321             os.seteuid(user_info.uid)
322     except Exception, e: # FIXME poner en el manejo de exceptiones estandar
323         if hasattr(e, 'child_traceback'):
324             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
325         raise
326     proc.wait()
327     comando_ejecutado.fin = datetime.now()
328 #    if no_anda_ejecucion: # TODO
329 #        comando_ejecutado.exito = False
330 #        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
331 #        if self.rechazar_si_falla:
332 #            entrega.exito = False
333 #        if self.terminar_si_falla: # TODO
334 #            raise ExecutionFailure(self)
335     # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
336     # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
337     # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
338 #    for archivo in self.archivos_salida:
339 #        pass # TODO hacer diff
340 #    if archivos_mal: # TODO
341 #        comando_ejecutado.exito = False
342 #        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
343 #        if self.rechazar_si_falla:
344 #            entrega.exito = False
345 #        if self.terminar_si_falla: # TODO
346 #            raise ExecutionFailure(self)
347 #    else:
348 #        comando_ejecutado.exito = True
349 #        comando_ejecutado.observaciones += 'xxx OK' # TODO
350     comando_ejecutado.exito = True
351     comando_ejecutado.observaciones += 'xxx OK' # TODO
352 ComandoFuente.ejecutar = ejecutar_comando_fuente
353 #}}}
354
355 def ejecutar_comando_prueba(self, path, prueba): #{{{
356     log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
357         prueba.shortrepr())
358     rmtree(path)
359     os.mkdir(path)
360     unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
361     unzip(self.archivos_entrada, path) # TODO try/except
362     comando_ejecutado = prueba.add_comando_ejecutado(self)
363     # TODO ejecutar en chroot (path)
364     comando_ejecutado.fin = datetime.now()
365 #    if no_anda_ejecucion: # TODO
366 #        comando_ejecutado.exito = False
367 #        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
368 #        if self.rechazar_si_falla:
369 #            entrega.exito = False
370 #        if self.terminar_si_falla: # TODO
371 #            raise ExecutionFailure(self) # TODO info de error
372 #    for archivo in self.archivos_salida:
373 #        pass # TODO hacer diff
374 #    if archivos_mal: # TODO
375 #        comando_ejecutado.exito = False
376 #        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
377 #        if self.rechazar_si_falla:
378 #            entrega.exito = False
379 #        if self.terminar_si_falla: # TODO
380 #            raise ExecutionFailure(comando=self) # TODO info de error
381 #    else:
382 #        comando_ejecutado.exito = True
383 #        comando_ejecutado.observaciones += 'xxx OK' # TODO
384     comando_ejecutado.exito = True
385     comando_ejecutado.observaciones += 'xxx OK' # TODO
386 ComandoPrueba.ejecutar = ejecutar_comando_prueba
387 #}}}
388