Commits

dready committed 9e12519

New model fields, email notification, worker failure handling

* New FabricRecipe fields: notify, schedule, repos, projdir
* New Build fields: executed_datetime and created_datetime
* email build results to address in FabricRecipe.notify
* settings.FF_EMAIL_BUILD_RESULTS used to turn on/off above (default: on)
* worker exports slug, repos, projdir fields to fabfile
* worker considers job succeeded if no exception caught, not when stderr is tru-ish

  • Participants
  • Parent commits d0454cc

Comments (0)

Files changed (6)

src/factory/models.py

 from django.db import models
 from django.conf import settings
 from factory.storage import FileSystemStorageUuidName
+from factory import signals
+from datetime import datetime
 
 # Create your models here.
 class FabfileRecipe(models.Model):
     name = models.CharField(max_length=255)
     slug = models.SlugField()
+    notify = models.EmailField(blank=True)
+    schedule = models.BooleanField() # or a charfield that lets you specify a job ID that matches the cronjob that will call schedule.py
     file = models.FileField(upload_to='factory/fabfiles')
+
+    repos = models.CharField(max_length=255, blank=True,
+                             help_text=u'URL of git repository to clone')
+    projdir = models.CharField(max_length=255,
+                               help_text=u'directory to cd into after clone, '
+                                         u'including the name of the work tree')
+
+    # default task
+    #task = models.CharField(max_length=255)
+    # proj repository (for "standard" fabfile)
+    #url = models.URLField(verify_exists=False, max_length=200, blank=True)
     
     def __unicode__(self):
         return self.name
 
-    
+
 class Build(models.Model):
     name = models.CharField(max_length=255)
     task = models.CharField(max_length=255,
                             help_text='Space separated list of tasks')
     fabfile_recipe = models.ForeignKey(FabfileRecipe)
+
+    # we can perhaps use this for the git revision to use?
     revision = models.CharField(max_length=255, null=True, blank=True)
+
     executed = models.BooleanField()
+    executed_datetime = models.DateTimeField(blank=True, null=True)
     success = models.BooleanField()
     environement = models.TextField(null=True, blank=True)
     output = models.TextField(null=True, blank=True)
     error = models.TextField(null=True, blank=True)
-    
+    created_datetime = models.DateTimeField(auto_now_add=True, auto_now=True)
+
+    #execution_time = models.IntegerField()
+    #started_datetime = models.DateTimeField(blank=True, null=True)
+    # from above (both reported by worker), we can find out the queued time
+
     def __unicode__(self):
         return self.name
+
+    def save(self, force_insert=False, force_update=False, **kwargs):
+        if self.executed:
+            self.executed_datetime = datetime.now()
+        return super(Build, self).save(force_insert, force_update, **kwargs)
+
     def make_build_package(self):
         """
         This method should build a compressed package containing the task runner,
             return os.path.basename(tarfile_name)
         finally:
             out_tarfile.close()
-            
-            
+
+
     def get_build_package_url(self):
         filename = self.make_build_package()
         return "%s/%s" %(settings.BUILD_URL, filename)
 
+
+
+signals.connect(Build)

src/factory/signals.py

+from django.db.models.signals import pre_save
+from django.core.mail import send_mail
+from django.conf import settings
+
+def send_build_email(instance, **kwargs):
+    if not getattr(settings, 'FF_EMAIL_BUILD_RESULTS', False):
+        return
+
+    if not instance.executed:
+        return
+
+    if not instance.fabfile_recipe.notify:
+        return
+
+    subject = '%s Build Results (%s)' % (instance, "success" if instance.success else "failure")
+    msg = """
+    Tasks: %(task)s
+    Recipe: %(recipe)s
+    ------------------------------------------------
+    Output
+    ------------------------------------------------
+    %(output)s
+
+
+    ------------------------------------------------
+    Errors
+    ------------------------------------------------
+    %(error)s
+    """ % dict(task=instance.task,
+               recipe=instance.fabfile_recipe,
+               output=instance.output,
+               error=instance.error)
+
+    send_mail(subject, msg, settings.SERVER_EMAIL,
+              [instance.fabfile_recipe.notify], fail_silently=False)
+
+
+
+
+def connect(build_model):
+    pre_save.connect(send_build_email, sender=build_model)

src/factory/views.py

         oldest_build_not_executed = qs[0]
         d={"name":oldest_build_not_executed.name,
            'task':oldest_build_not_executed.task,
+           'slug':oldest_build_not_executed.fabfile_recipe.slug,
+           'repos':oldest_build_not_executed.fabfile_recipe.repos,
+           'projdir':oldest_build_not_executed.fabfile_recipe.projdir,
            "post_back_url":"http://%s%s" %(site.domain,reverse("build_update",
                                kwargs={'object_id':oldest_build_not_executed.id})),
            "build_package_url":"http://%s%s" %(site.domain,

src/project/settings.py

     from project.local_settings import *
 except:
     pass
+
+
+FF_EMAIL_BUILD_RESULTS = True

src/worker/__init__.py

 class WorkerError(Exception):
     def __init__(self, value):
         self.value = value
+
     def __str__(self):
         return str(self.value)
 
 
 class Worker(object):
     def __init__(self, name, task, post_back_url, build_package_url,
+                 slug, repos, projdir,
                  kitchen_path):
         self.name = name
         self.post_back_url = post_back_url
         self.kitchen_path = kitchen_path
         self.filename = None
         self.task = task 
+        self.slug = slug
+        self.repos = repos
+        self.projdir = projdir
         self.executed = False
         self.success = False
         self.revision = None
         self.output = None
         self.error = None
+
+
     def download_build_package(self):
         logging.debug("try to download the build package : %s" %
                       self.build_package_url)
                                                     self.filename.split(".")[0]))
         local_tar_file.close()
 
+
     def execute_task(self):
         #a bit of hackery there to import this particular fabfile
         logging.debug("Worker try to execute the task : %s" %
                             "fabfile.py")
         file_path = os.path.join(self.kitchen_path, file)
         # We should collect this output
-        self.output, self.error = self._execute_task_from_fabfile(file_path,
-                                                                  self.task) 
+        self.output, self.error, self.success = (
+            self._execute_task_from_fabfile(file_path,
+                                            self.task,
+                                            self.slug,
+                                            self.repos,
+                                            self.projdir))
         logging.debug('output : %s' %self.output)
         logging.debug('error : %s' %self.error)
-        if self.error:
-            self.success = False
+        if self.success:
+            logging.debug("Succeed to execute the task")
+        else:
             logging.debug("Fail to execute the task")
-        else:
-            self.success = True
-            logging.debug("Succeed to execute the task")
+
+
     def post_result(self):
         values = {
             "name": self.name,
         request = urllib2.Request(self.post_back_url, data)
         fd=urllib2.urlopen(request)
         data=fd.read()
-        
+ 
+
     def clean(self):
         logging.debug('Clean the kitchen')
         for f in glob(os.path.join(self.kitchen_path,
                 os.remove(f)
             elif os.path.isdir(f):
                 rmtree(f)
+
+
     @staticmethod
-    def _execute_task_from_fabfile(fabfile_path, task):
-        
+    def _execute_task_from_fabfile(fabfile_path, task, slug, repos, projdir):
+        from fabric.api import env
+        print "***** repos=%s" % repos
+        env['ff_slug'] = slug
+        env['ff_repos'] = repos
+        env['ff_projdir'] = projdir
+
         fabfile = load_source("fabfile",
                     fabfile_path)
         logging.debug("fabfile_path : %s" %fabfile_path)
             error = StringIO()
             sys.stdout = output
             sys.stderr = error
+            failed = False
             # execute the task
             try:
                 task()
             except SystemExit, e:
                 logging.error("%s %s" %(e.__class__(), str(e)))
+                failed = True
+            except Exception, e:
+                logging.error("%s %s" %(e.__class__(), str(e)))
+                return (output.getvalue(), error.getvalue() + '\n' + str(e), False)
             finally:
                 sys.stdout = sys.__stdout__
                 sys.stderr = sys.__stderr__
                 os.chdir(cwd)
-            return (output.getvalue(), error.getvalue())
+            return (output.getvalue(), error.getvalue(), not failed)
         else:
-            raise WorkerError("No task %s in fabfile %s" %
-                                            (task, fabfile_path))    
+            return ('', "No task %s in fabfile %s" % (task, fabfile_path), False)

src/worker/run_worker.py

         worker = Worker(name=worker_dict['name'], task=worker_dict['task'],
                     post_back_url=worker_dict['post_back_url'],
                     build_package_url=worker_dict['build_package_url'],
+                    slug=worker_dict['slug'],
+                    repos=worker_dict['repos'],
+                    projdir=worker_dict['projdir'],
                     kitchen_path=kitchen_path)
         return worker
     else: