Add JSON status endpoint.

Can be used for nifty ajax-style status pages.

Add optional description field to pipeline.

Change-Id: If5db3f6945f65f038833cbf9c783de5ffef63b49
Reviewed-on: https://review.openstack.org/18579
Reviewed-by: Jeremy Stanley <fungi@yuggoth.org>
Approved: James E. Blair <corvus@inaugust.com>
Tested-by: Jenkins
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index 961c964..25c2087 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -166,6 +166,10 @@
   This is used later in the project definition to indicate what jobs
   should be run for events in the pipeline.
 
+**description**
+  This is an optional field that may be used to provide a textual
+  description of the pipeline.
+
 **manager**
   There are currently two schemes for managing pipelines:
 
diff --git a/zuul/launcher/jenkins.py b/zuul/launcher/jenkins.py
index 8feea02..1841a92 100644
--- a/zuul/launcher/jenkins.py
+++ b/zuul/launcher/jenkins.py
@@ -49,9 +49,9 @@
 
     def app(self, environ, start_response):
         request = Request(environ)
-        start_response('200 OK', [('content-type', 'text/html')])
         if request.path == '/jenkins_endpoint':
             self.jenkins_endpoint(request)
+            start_response('200 OK', [('content-type', 'text/html')])
             return ['Zuul good.']
         elif request.path == '/status':
             try:
@@ -59,8 +59,19 @@
             except:
                 self.log.exception("Exception formatting status:")
                 raise
+            start_response('200 OK', [('content-type', 'text/html')])
+            return [ret]
+        elif request.path == '/status.json':
+            try:
+                ret = self.jenkins.sched.formatStatusJSON()
+            except:
+                self.log.exception("Exception formatting status:")
+                raise
+            start_response('200 OK', [('content-type', 'application/json'),
+                                      ('Access-Control-Allow-Origin', '*')])
             return [ret]
         else:
+            start_response('200 OK', [('content-type', 'text/html')])
             return ['Zuul good.']
 
     def jenkins_endpoint(self, request):
diff --git a/zuul/model.py b/zuul/model.py
index 57810b9..aa55561 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -27,6 +27,7 @@
     """A top-level pipeline such as check, gate, post, etc."""
     def __init__(self, name):
         self.name = name
+        self.description = None
         self.job_trees = {}  # project -> JobTree
         self.manager = None
         self.queues = []
@@ -186,6 +187,24 @@
                 ret += self.formatStatus(head, html=True)
         return ret
 
+    def formatStatusJSON(self):
+        j_pipeline = dict(name=self.name,
+                          description=self.description)
+        j_queues = []
+        j_pipeline['change_queues'] = j_queues
+        for queue in self.queues:
+            j_queue = dict(name=queue.name)
+            j_queues.append(j_queue)
+            j_queue['heads'] = []
+            for head in queue.getHeads():
+                j_changes = []
+                c = head
+                while c:
+                    j_changes.append(self.formatChangeJSON(c))
+                    c = c.change_behind
+                j_queue['heads'].append(j_changes)
+        return j_pipeline
+
     def formatStatus(self, changeish, indent=0, html=False):
         indent_str = ' ' * indent
         ret = ''
@@ -224,6 +243,29 @@
             ret += self.formatStatus(changeish.change_behind, indent + 2, html)
         return ret
 
+    def formatChangeJSON(self, changeish):
+        ret = {}
+        if hasattr(changeish, 'url') and changeish.url is not None:
+            ret['url'] = changeish.url
+        ret['id'] = changeish._id()
+        ret['project'] = changeish.project.name
+        ret['jobs'] = []
+        for job in self.getJobs(changeish):
+            build = changeish.current_build_set.getBuild(job.name)
+            if build:
+                result = build.result
+                url = build.url
+            else:
+                result = None
+                url = None
+            ret['jobs'].append(
+                dict(
+                    name=job.name,
+                    url=url,
+                    result=result,
+                    voting=job.voting))
+        return ret
+
 
 class ChangeQueue(object):
     """DependentPipelines have multiple parallel queues shared by
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index d7fd544..aa3f8ab 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import json
 import logging
 import os
 import pickle
@@ -82,6 +83,7 @@
 
         for conf_pipeline in data.get('pipelines', []):
             pipeline = Pipeline(conf_pipeline['name'])
+            pipeline.description = conf_pipeline.get('description')
             manager = globals()[conf_pipeline['manager']](self, pipeline)
             pipeline.setManager(manager)
 
@@ -407,6 +409,27 @@
         ret += '</pre></html>'
         return ret
 
+    def formatStatusJSON(self):
+        data = {}
+        if self._pause:
+            ret = '<p><b>Queue only mode:</b> preparing to '
+            if self._reconfigure:
+                ret += 'reconfigure'
+            if self._exit:
+                ret += 'exit'
+            ret += ', queue length: %s' % self.trigger_event_queue.qsize()
+            ret += '</p>'
+            data['message'] = ret
+
+        pipelines = []
+        data['pipelines'] = pipelines
+        keys = self.pipelines.keys()
+        keys.sort()
+        for key in keys:
+            pipeline = self.pipelines[key]
+            pipelines.append(pipeline.formatStatusJSON())
+        return json.dumps(data)
+
 
 class BasePipelineManager(object):
     log = logging.getLogger("zuul.BasePipelineManager")