]> git.llucax.com Git - software/sercom.git/blob - sercom/tester.py
Asociar el stdin al subproceso.
[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 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, 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.
86     """
87     log.debug(_(u'Intentando descomprimir'))
88     if bytes is None:
89         return
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)
95             os.mkdir(dst)
96         else:
97             log.debug(_(u'Descomprimiendo archivo "%s" en "%s"'), f, dst)
98             file(dst, 'w').write(zfile.read(f))
99     zfile.close()
100 #}}}
101
102 class SecureProcess(object): #{{{
103     default = dict(
104         max_tiempo_cpu      = 120,
105         max_memoria         = 16,
106         max_tam_archivo     = 5,
107         max_cant_archivos   = 5,
108         max_cant_procesos   = 0,
109         max_locks_memoria   = 0,
110     )
111     uid = config.get('sercom.tester.chroot.user', 65534)
112     MB = 1048576
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
116         self.chroot = chroot
117         self.cwd = cwd
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])
127     def __call__(self):
128         x2 = lambda x: (x, x)
129         os.chroot(self.chroot)
130         os.chdir(self.cwd)
131         uinfo = UserInfo(self.uid)
132         os.setgid(uinfo.gid)
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
142         import time
143         time.sleep(0)
144 #}}}
145
146 class Tester(object): #{{{
147
148     def __init__(self, name, path, home, queue): #{{{ y properties
149         self.name = name
150         self.path = path
151         self.home = home
152         self.queue = queue
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)
158
159     @property
160     def build_path(self):
161         return join(self.chroot, self.home, 'build')
162
163     @property
164     def test_path(self):
165         return join(self.chroot, self.home, 'test')
166
167     @property
168     def chroot(self):
169         return join(self.path, 'chroot_' + self.name)
170
171     @property
172     def orig_chroot(self):
173         return join(self.path, 'chroot')
174     #}}}
175
176     def run(self): #{{{
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'),
181                 self.name, entrega)
182             self.test(entrega)
183             log.debug(_(u'Fin de pruebas de: %s'), entrega)
184             entrega_id = self.queue.get() # blocking
185     #}}}
186
187     def test(self, entrega): #{{{
188         log.debug(_(u'Tester.test(entrega=%s)'), entrega)
189         entrega.inicio_tareas = datetime.now()
190         try:
191             try:
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)
199             except Exception, e:
200                 if isinstance(e, SystemExit): raise
201                 entrega.observaciones += error_interno
202                 log.exception(_(u'Hubo una excepción inesperada: %s'), e)
203             except:
204                 entrega.observaciones += error_interno
205                 log.exception(_(u'Hubo una excepción inesperada desconocida'))
206             else:
207                 entrega.correcta = True
208                 log.debug(_(u'Entrega correcta: %s'), entrega)
209         finally:
210             entrega.fin_tareas = datetime.now()
211     #}}}
212
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)
220         os.setegid(0)
221         try:
222             sp.check_call(rsync)
223         finally:
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)
229
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
233     #}}}
234
235     def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
236         log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
237             entrega.shortrepr())
238         tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
239                     if isinstance(t, TareaFuente)]
240         for tarea in tareas:
241             tarea.ejecutar(self.build_path, entrega)
242
243     def ejecutar_tareas_prueba(self, entrega):
244         log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
245             entrega.shortrepr())
246         for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
247             caso.ejecutar(self.test_path, entrega)
248     #}}}
249
250 #}}}
251
252 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
253     log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
254         entrega.shortrepr())
255     tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
256                 if isinstance(t, TareaPrueba)]
257     prueba = entrega.add_prueba(self)
258     try:
259         try:
260             for tarea in tareas:
261                 tarea.ejecutar(path, prueba)
262         except ExecutionFailure, e:
263             prueba.exito = False
264             if self.rechazar_si_falla:
265                 entrega.exito = False
266             if self.terminar_si_falla:
267                 raise ExecutionError(e.comando, e.tarea, prueba)
268         else:
269             prueba.exito = True
270     finally:
271         prueba.fin = datetime.now()
272 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
273 #}}}
274
275 def ejecutar_tarea_fuente(self, path, entrega): #{{{
276     log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
277         entrega.shortrepr())
278     try:
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
287 #}}}
288
289 def ejecutar_tarea_prueba(self, path, prueba): #{{{
290     log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
291         prueba.shortrepr())
292     try:
293         for cmd in self.comandos:
294             cmd.ejecutar(path, prueba)
295     except ExecutionFailure, e:
296         if self.rechazar_si_falla:
297             prueba.exito = False
298         if self.terminar_si_falla:
299             raise ExecutionError(e.comando, tarea)
300 TareaPrueba.ejecutar = ejecutar_tarea_prueba
301 #}}}
302
303 def ejecutar_comando_fuente(self, path, entrega): #{{{
304     log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
305         entrega.shortrepr())
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
309     options = dict(
310         close_fds=True,
311         stdin=None,
312         shell=True,
313         preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build')
314     )
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
321     else:
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)
330     os.setegid(0)
331     try:
332         try:
333             proc = sp.Popen(self.comando, **options)
334         finally:
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)
339     except Exception, e:
340         if hasattr(e, 'child_traceback'):
341             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
342         raise
343     proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
344     comando_ejecutado.fin = datetime.now()
345     buffer = StringIO()
346     zip = ZipFile(buffer, 'w')
347     if self.guardar_stdouterr:
348         zip.write('/tmp/sercom.tester.%s.stdouterr'
349             % comando_ejecutado.id, '__stdouterr__')
350     else:
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__')
357     zip.close()
358     comando_ejecutado.archivos_guardados = buffer.getvalue()
359
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)
379 #    else:
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
385 #}}}
386
387 def ejecutar_comando_prueba(self, path, prueba): #{{{
388     log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
389         prueba.shortrepr())
390     rmtree(path)
391     os.mkdir(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
413 #    else:
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
419 #}}}
420