Remove webapp

The webapp has been superseeded by zuul-web now so remove it
completely.

Change-Id: I8125a0d7f3aef8fa7982c75d4650776b6906a612
diff --git a/doc/source/admin/components.rst b/doc/source/admin/components.rst
index fa72c66..50afc92 100644
--- a/doc/source/admin/components.rst
+++ b/doc/source/admin/components.rst
@@ -243,23 +243,6 @@
 
       .. TODO: is this effectively required?
 
-.. attr:: webapp
-
-   .. attr:: listen_address
-      :default: all addresses
-
-      IP address or domain name on which to listen.
-
-   .. attr:: port
-      :default: 8001
-
-      Port on which the webapp is listening.
-
-   .. attr:: status_expiry
-      :default: 1
-
-      Zuul will cache the status.json file for this many seconds.
-
 .. attr:: scheduler
 
    .. attr:: command_socket
diff --git a/etc/zuul.conf-sample b/etc/zuul.conf-sample
index 3b1ad76..62b5086 100644
--- a/etc/zuul.conf-sample
+++ b/etc/zuul.conf-sample
@@ -41,10 +41,6 @@
 ;sql_connection_name=mydatabase
 status_url=https://zuul.example.com/status
 
-[webapp]
-listen_address=0.0.0.0
-port=8001
-
 [connection gerrit]
 driver=gerrit
 server=review.example.com
diff --git a/tests/base.py b/tests/base.py
index e7151df..4ba46a8 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -57,7 +57,6 @@
 import zuul.driver.gerrit.gerritconnection as gerritconnection
 import zuul.driver.github.githubconnection as githubconnection
 import zuul.scheduler
-import zuul.webapp
 import zuul.executor.server
 import zuul.executor.client
 import zuul.lib.connections
@@ -2009,9 +2008,6 @@
         self.sched = zuul.scheduler.Scheduler(self.config)
         self.sched._stats_interval = 1
 
-        self.webapp = zuul.webapp.WebApp(
-            self.sched, port=0, listen_address='127.0.0.1')
-
         self.event_queues = [
             self.sched.result_event_queue,
             self.sched.trigger_event_queue,
@@ -2050,7 +2046,6 @@
         self.sched.setZooKeeper(self.zk)
 
         self.sched.start()
-        self.webapp.start()
         self.executor_client.gearman.waitForServer()
         # Cleanups are run in reverse order
         self.addCleanup(self.assertCleanShutdown)
@@ -2315,8 +2310,6 @@
         self.sched.join()
         self.statsd.stop()
         self.statsd.join()
-        self.webapp.stop()
-        self.webapp.join()
         self.rpcclient.shutdown()
         self.gearman_server.shutdown()
         self.fake_nodepool.stop()
diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py
index 5db20b3..2b7d8e5 100755
--- a/tests/unit/test_scheduler.py
+++ b/tests/unit/test_scheduler.py
@@ -19,14 +19,12 @@
 import textwrap
 
 import os
-import re
 import shutil
 import time
 from unittest import skip
 
 import git
 import testtools
-import urllib
 
 import zuul.change_matcher
 from zuul.driver.gerrit import gerritreporter
@@ -2533,110 +2531,6 @@
         self.assertEqual(self.history[4].pipeline, 'check')
         self.assertEqual(self.history[5].pipeline, 'check')
 
-    def test_json_status(self):
-        "Test that we can retrieve JSON status info"
-        self.executor_server.hold_jobs_in_build = True
-        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
-        A.addApproval('Code-Review', 2)
-        self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
-        self.waitUntilSettled()
-
-        self.executor_server.release('project-merge')
-        self.waitUntilSettled()
-
-        port = self.webapp.server.socket.getsockname()[1]
-
-        req = urllib.request.Request(
-            "http://localhost:%s/tenant-one/status" % port)
-        f = urllib.request.urlopen(req)
-        headers = f.info()
-        self.assertIn('Content-Length', headers)
-        self.assertIn('Content-Type', headers)
-        self.assertIsNotNone(re.match('^application/json(; charset=UTF-8)?$',
-                                      headers['Content-Type']))
-        self.assertIn('Access-Control-Allow-Origin', headers)
-        self.assertIn('Cache-Control', headers)
-        self.assertIn('Last-Modified', headers)
-        self.assertIn('Expires', headers)
-        data = f.read().decode('utf8')
-
-        self.executor_server.hold_jobs_in_build = False
-        self.executor_server.release()
-        self.waitUntilSettled()
-
-        data = json.loads(data)
-        status_jobs = []
-        for p in data['pipelines']:
-            for q in p['change_queues']:
-                if p['name'] in ['gate', 'conflict']:
-                    self.assertEqual(q['window'], 20)
-                else:
-                    self.assertEqual(q['window'], 0)
-                for head in q['heads']:
-                    for change in head:
-                        self.assertTrue(change['active'])
-                        self.assertEqual(change['id'], '1,1')
-                        for job in change['jobs']:
-                            status_jobs.append(job)
-        self.assertEqual('project-merge', status_jobs[0]['name'])
-        # TODO(mordred) pull uuids from self.builds
-        self.assertEqual(
-            'stream.html?uuid={uuid}&logfile=console.log'.format(
-                uuid=status_jobs[0]['uuid']),
-            status_jobs[0]['url'])
-        self.assertEqual(
-            'finger://{hostname}/{uuid}'.format(
-                hostname=self.executor_server.hostname,
-                uuid=status_jobs[0]['uuid']),
-            status_jobs[0]['finger_url'])
-        # TOOD(mordred) configure a success-url on the base job
-        self.assertEqual(
-            'finger://{hostname}/{uuid}'.format(
-                hostname=self.executor_server.hostname,
-                uuid=status_jobs[0]['uuid']),
-            status_jobs[0]['report_url'])
-        self.assertEqual('project-test1', status_jobs[1]['name'])
-        self.assertEqual(
-            'stream.html?uuid={uuid}&logfile=console.log'.format(
-                uuid=status_jobs[1]['uuid']),
-            status_jobs[1]['url'])
-        self.assertEqual(
-            'finger://{hostname}/{uuid}'.format(
-                hostname=self.executor_server.hostname,
-                uuid=status_jobs[1]['uuid']),
-            status_jobs[1]['finger_url'])
-        self.assertEqual(
-            'finger://{hostname}/{uuid}'.format(
-                hostname=self.executor_server.hostname,
-                uuid=status_jobs[1]['uuid']),
-            status_jobs[1]['report_url'])
-
-        self.assertEqual('project-test2', status_jobs[2]['name'])
-        self.assertEqual(
-            'stream.html?uuid={uuid}&logfile=console.log'.format(
-                uuid=status_jobs[2]['uuid']),
-            status_jobs[2]['url'])
-        self.assertEqual(
-            'finger://{hostname}/{uuid}'.format(
-                hostname=self.executor_server.hostname,
-                uuid=status_jobs[2]['uuid']),
-            status_jobs[2]['finger_url'])
-        self.assertEqual(
-            'finger://{hostname}/{uuid}'.format(
-                hostname=self.executor_server.hostname,
-                uuid=status_jobs[2]['uuid']),
-            status_jobs[2]['report_url'])
-
-        # check job dependencies
-        self.assertIsNotNone(status_jobs[0]['dependencies'])
-        self.assertIsNotNone(status_jobs[1]['dependencies'])
-        self.assertIsNotNone(status_jobs[2]['dependencies'])
-        self.assertEqual(len(status_jobs[0]['dependencies']), 0)
-        self.assertEqual(len(status_jobs[1]['dependencies']), 1)
-        self.assertEqual(len(status_jobs[2]['dependencies']), 1)
-        self.assertIn('project-merge', status_jobs[1]['dependencies'])
-        self.assertIn('project-merge', status_jobs[2]['dependencies'])
-
     def test_reconfigure_merge(self):
         """Test that two reconfigure events are merged"""
 
@@ -3212,13 +3106,6 @@
 
         self.assertEqual(len(self.builds), 2)
 
-        port = self.webapp.server.socket.getsockname()[1]
-
-        req = urllib.request.Request(
-            "http://localhost:%s/tenant-one/status" % port)
-        f = urllib.request.urlopen(req)
-        data = f.read().decode('utf8')
-
         self.executor_server.hold_jobs_in_build = False
         # Stop queuing timer triggered jobs so that the assertions
         # below don't race against more jobs being queued.
@@ -3240,16 +3127,6 @@
                  ref='refs/heads/stable'),
         ], ordered=False)
 
-        data = json.loads(data)
-        status_jobs = set()
-        for p in data['pipelines']:
-            for q in p['change_queues']:
-                for head in q['heads']:
-                    for change in head:
-                        for job in change['jobs']:
-                            status_jobs.add(job['name'])
-        self.assertIn('project-bitrot', status_jobs)
-
     def test_idle(self):
         "Test that frequent periodic jobs work"
         # This test can not use simple_layout because it must start
diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py
index 6881a83..b5ebe9f 100644
--- a/tests/unit/test_web.py
+++ b/tests/unit/test_web.py
@@ -78,14 +78,105 @@
         super(TestWeb, self).tearDown()
 
     def test_web_status(self):
-        "Test that we can filter to only certain changes in the webapp."
+        "Test that we can retrieve JSON status info"
+        self.executor_server.hold_jobs_in_build = True
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        A.addApproval('Code-Review', 2)
+        self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
+        self.waitUntilSettled()
+
+        self.executor_server.release('project-merge')
+        self.waitUntilSettled()
 
         req = urllib.request.Request(
             "http://localhost:%s/tenant-one/status.json" % self.port)
         f = urllib.request.urlopen(req)
-        data = json.loads(f.read().decode('utf8'))
+        headers = f.info()
+        self.assertIn('Content-Length', headers)
+        self.assertIn('Content-Type', headers)
+        self.assertEqual(
+            'application/json; charset=utf-8', headers['Content-Type'])
+        self.assertIn('Access-Control-Allow-Origin', headers)
+        self.assertIn('Cache-Control', headers)
+        self.assertIn('Last-Modified', headers)
+        data = f.read().decode('utf8')
 
-        self.assertIn('pipelines', data)
+        self.executor_server.hold_jobs_in_build = False
+        self.executor_server.release()
+        self.waitUntilSettled()
+
+        data = json.loads(data)
+        status_jobs = []
+        for p in data['pipelines']:
+            for q in p['change_queues']:
+                if p['name'] in ['gate', 'conflict']:
+                    self.assertEqual(q['window'], 20)
+                else:
+                    self.assertEqual(q['window'], 0)
+                for head in q['heads']:
+                    for change in head:
+                        self.assertTrue(change['active'])
+                        self.assertIn(change['id'], ('1,1', '2,1', '3,1'))
+                        for job in change['jobs']:
+                            status_jobs.append(job)
+        self.assertEqual('project-merge', status_jobs[0]['name'])
+        # TODO(mordred) pull uuids from self.builds
+        self.assertEqual(
+            'stream.html?uuid={uuid}&logfile=console.log'.format(
+                uuid=status_jobs[0]['uuid']),
+            status_jobs[0]['url'])
+        self.assertEqual(
+            'finger://{hostname}/{uuid}'.format(
+                hostname=self.executor_server.hostname,
+                uuid=status_jobs[0]['uuid']),
+            status_jobs[0]['finger_url'])
+        # TOOD(mordred) configure a success-url on the base job
+        self.assertEqual(
+            'finger://{hostname}/{uuid}'.format(
+                hostname=self.executor_server.hostname,
+                uuid=status_jobs[0]['uuid']),
+            status_jobs[0]['report_url'])
+        self.assertEqual('project-test1', status_jobs[1]['name'])
+        self.assertEqual(
+            'stream.html?uuid={uuid}&logfile=console.log'.format(
+                uuid=status_jobs[1]['uuid']),
+            status_jobs[1]['url'])
+        self.assertEqual(
+            'finger://{hostname}/{uuid}'.format(
+                hostname=self.executor_server.hostname,
+                uuid=status_jobs[1]['uuid']),
+            status_jobs[1]['finger_url'])
+        self.assertEqual(
+            'finger://{hostname}/{uuid}'.format(
+                hostname=self.executor_server.hostname,
+                uuid=status_jobs[1]['uuid']),
+            status_jobs[1]['report_url'])
+
+        self.assertEqual('project-test2', status_jobs[2]['name'])
+        self.assertEqual(
+            'stream.html?uuid={uuid}&logfile=console.log'.format(
+                uuid=status_jobs[2]['uuid']),
+            status_jobs[2]['url'])
+        self.assertEqual(
+            'finger://{hostname}/{uuid}'.format(
+                hostname=self.executor_server.hostname,
+                uuid=status_jobs[2]['uuid']),
+            status_jobs[2]['finger_url'])
+        self.assertEqual(
+            'finger://{hostname}/{uuid}'.format(
+                hostname=self.executor_server.hostname,
+                uuid=status_jobs[2]['uuid']),
+            status_jobs[2]['report_url'])
+
+        # check job dependencies
+        self.assertIsNotNone(status_jobs[0]['dependencies'])
+        self.assertIsNotNone(status_jobs[1]['dependencies'])
+        self.assertIsNotNone(status_jobs[2]['dependencies'])
+        self.assertEqual(len(status_jobs[0]['dependencies']), 0)
+        self.assertEqual(len(status_jobs[1]['dependencies']), 1)
+        self.assertEqual(len(status_jobs[2]['dependencies']), 1)
+        self.assertIn('project-merge', status_jobs[1]['dependencies'])
+        self.assertIn('project-merge', status_jobs[2]['dependencies'])
 
     def test_web_bad_url(self):
         # do we 404 correctly
diff --git a/tests/unit/test_webapp.py b/tests/unit/test_webapp.py
deleted file mode 100644
index c06fc93..0000000
--- a/tests/unit/test_webapp.py
+++ /dev/null
@@ -1,119 +0,0 @@
-#!/usr/bin/env python
-
-# Copyright 2014 Hewlett-Packard Development Company, L.P.
-# Copyright 2014 Rackspace Australia
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-import os
-import json
-import urllib
-
-import webob
-
-from tests.base import ZuulTestCase, FIXTURE_DIR
-
-
-class TestWebapp(ZuulTestCase):
-    tenant_config_file = 'config/single-tenant/main.yaml'
-
-    def setUp(self):
-        super(TestWebapp, self).setUp()
-        self.executor_server.hold_jobs_in_build = True
-        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
-        A.addApproval('Code-Review', 2)
-        self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
-        B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
-        B.addApproval('Code-Review', 2)
-        self.fake_gerrit.addEvent(B.addApproval('Approved', 1))
-        self.waitUntilSettled()
-        self.port = self.webapp.server.socket.getsockname()[1]
-
-    def tearDown(self):
-        self.executor_server.hold_jobs_in_build = False
-        self.executor_server.release()
-        self.waitUntilSettled()
-        super(TestWebapp, self).tearDown()
-
-    def test_webapp_status(self):
-        "Test that we can filter to only certain changes in the webapp."
-
-        req = urllib.request.Request(
-            "http://localhost:%s/tenant-one/status" % self.port)
-        f = urllib.request.urlopen(req)
-        data = json.loads(f.read().decode('utf8'))
-
-        self.assertIn('pipelines', data)
-
-    def test_webapp_status_compat(self):
-        # testing compat with status.json
-        req = urllib.request.Request(
-            "http://localhost:%s/tenant-one/status.json" % self.port)
-        f = urllib.request.urlopen(req)
-        data = json.loads(f.read().decode('utf8'))
-
-        self.assertIn('pipelines', data)
-
-    def test_webapp_bad_url(self):
-        # do we 404 correctly
-        req = urllib.request.Request(
-            "http://localhost:%s/status/foo" % self.port)
-        self.assertRaises(urllib.error.HTTPError, urllib.request.urlopen, req)
-
-    def test_webapp_find_change(self):
-        # can we filter by change id
-        req = urllib.request.Request(
-            "http://localhost:%s/tenant-one/status/change/1,1" % self.port)
-        f = urllib.request.urlopen(req)
-        data = json.loads(f.read().decode('utf8'))
-
-        self.assertEqual(1, len(data), data)
-        self.assertEqual("org/project", data[0]['project'])
-
-        req = urllib.request.Request(
-            "http://localhost:%s/tenant-one/status/change/2,1" % self.port)
-        f = urllib.request.urlopen(req)
-        data = json.loads(f.read().decode('utf8'))
-
-        self.assertEqual(1, len(data), data)
-        self.assertEqual("org/project1", data[0]['project'], data)
-
-    def test_webapp_keys(self):
-        with open(os.path.join(FIXTURE_DIR, 'public.pem'), 'rb') as f:
-            public_pem = f.read()
-
-        req = urllib.request.Request(
-            "http://localhost:%s/tenant-one/keys/gerrit/org/project.pub" %
-            self.port)
-        f = urllib.request.urlopen(req)
-        self.assertEqual(f.read(), public_pem)
-
-    def test_webapp_custom_handler(self):
-        def custom_handler(path, tenant_name, request):
-            return webob.Response(body='ok')
-
-        self.webapp.register_path('/custom', custom_handler)
-        req = urllib.request.Request(
-            "http://localhost:%s/custom" % self.port)
-        f = urllib.request.urlopen(req)
-        self.assertEqual(b'ok', f.read())
-
-        self.webapp.unregister_path('/custom')
-        self.assertRaises(urllib.error.HTTPError, urllib.request.urlopen, req)
-
-    def test_webapp_404_on_unknown_tenant(self):
-        req = urllib.request.Request(
-            "http://localhost:{}/non-tenant/status.json".format(self.port))
-        e = self.assertRaises(
-            urllib.error.HTTPError, urllib.request.urlopen, req)
-        self.assertEqual(404, e.code)
diff --git a/zuul/cmd/scheduler.py b/zuul/cmd/scheduler.py
index a32d924..aeb40d9 100755
--- a/zuul/cmd/scheduler.py
+++ b/zuul/cmd/scheduler.py
@@ -118,7 +118,6 @@
         import zuul.executor.client
         import zuul.merger.client
         import zuul.nodepool
-        import zuul.webapp
         import zuul.zk
 
         if (self.config.has_option('gearman_server', 'start') and
@@ -142,15 +141,6 @@
 
         zookeeper.connect(zookeeper_hosts, timeout=zookeeper_timeout)
 
-        cache_expiry = get_default(self.config, 'webapp', 'status_expiry', 1)
-        listen_address = get_default(self.config, 'webapp', 'listen_address',
-                                     '0.0.0.0')
-        port = get_default(self.config, 'webapp', 'port', 8001)
-
-        webapp = zuul.webapp.WebApp(
-            self.sched, port=port, cache_expiry=cache_expiry,
-            listen_address=listen_address)
-
         self.configure_connections()
         self.sched.setExecutor(gearman)
         self.sched.setMerger(merger)
@@ -168,8 +158,6 @@
             # TODO(jeblair): If we had all threads marked as daemon,
             # we might be able to have a nicer way of exiting here.
             sys.exit(1)
-        self.log.info('Starting Webapp')
-        webapp.start()
 
         signal.signal(signal.SIGHUP, self.reconfigure_handler)
 
diff --git a/zuul/webapp.py b/zuul/webapp.py
deleted file mode 100644
index b5fdc0e..0000000
--- a/zuul/webapp.py
+++ /dev/null
@@ -1,200 +0,0 @@
-# Copyright 2012 Hewlett-Packard Development Company, L.P.
-# Copyright 2013 OpenStack Foundation
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-import copy
-import json
-import logging
-import re
-import threading
-import time
-from paste import httpserver
-import webob
-from webob import dec
-
-from zuul.lib import encryption
-
-"""Zuul main web app.
-
-Zuul supports HTTP requests directly against it for determining the
-change status. These responses are provided as json data structures.
-
-The supported urls are:
-
- - /status: return a complex data structure that represents the entire
-   queue / pipeline structure of the system
- - /status.json (backwards compatibility): same as /status
- - /status/change/X,Y: return status just for gerrit change X,Y
- - /keys/SOURCE/PROJECT.pub: return the public key for PROJECT
-
-When returning status for a single gerrit change you will get an
-array of changes, they will not include the queue structure.
-"""
-
-
-class WebApp(threading.Thread):
-    log = logging.getLogger("zuul.WebApp")
-    change_path_regexp = '/status/change/(.*)$'
-
-    def __init__(self, scheduler, port=8001, cache_expiry=1,
-                 listen_address='0.0.0.0'):
-        threading.Thread.__init__(self)
-        self.scheduler = scheduler
-        self.listen_address = listen_address
-        self.port = port
-        self.cache_expiry = cache_expiry
-        self.cache_time = 0
-        self.cache = {}
-        self.daemon = True
-        self.routes = {}
-        self._init_default_routes()
-        self.server = httpserver.serve(
-            dec.wsgify(self.app), host=self.listen_address, port=self.port,
-            start_loop=False)
-
-    def _init_default_routes(self):
-        self.register_path('/(status\.json|status)$', self.status)
-        self.register_path(self.change_path_regexp, self.change)
-
-    def run(self):
-        self.server.serve_forever()
-
-    def stop(self):
-        self.server.server_close()
-
-    def _changes_by_func(self, func, tenant_name):
-        """Filter changes by a user provided function.
-
-        In order to support arbitrary collection of subsets of changes
-        we provide a low level filtering mechanism that takes a
-        function which applies to changes. The output of this function
-        is a flattened list of those collected changes.
-        """
-        status = []
-        jsonstruct = json.loads(self.cache[tenant_name])
-        for pipeline in jsonstruct['pipelines']:
-            for change_queue in pipeline['change_queues']:
-                for head in change_queue['heads']:
-                    for change in head:
-                        if func(change):
-                            status.append(copy.deepcopy(change))
-        return json.dumps(status)
-
-    def _status_for_change(self, rev, tenant_name):
-        """Return the statuses for a particular change id X,Y."""
-        def func(change):
-            return change['id'] == rev
-        return self._changes_by_func(func, tenant_name)
-
-    def register_path(self, path, handler):
-        path_re = re.compile(path)
-        self.routes[path] = (path_re, handler)
-
-    def unregister_path(self, path):
-        if self.routes.get(path):
-            del self.routes[path]
-
-    def _handle_keys(self, request, path):
-        m = re.match('/keys/(.*?)/(.*?).pub', path)
-        if not m:
-            raise webob.exc.HTTPBadRequest()
-        source_name = m.group(1)
-        project_name = m.group(2)
-        source = self.scheduler.connections.getSource(source_name)
-        if not source:
-            raise webob.exc.HTTPNotFound(
-                detail="Cannot locate a source named %s" % source_name)
-        project = source.getProject(project_name)
-        if not project or not hasattr(project, 'public_key'):
-            raise webob.exc.HTTPNotFound(
-                detail="Cannot locate a project named %s" % project_name)
-
-        pem_public_key = encryption.serialize_rsa_public_key(
-            project.public_key)
-
-        response = webob.Response(body=pem_public_key,
-                                  content_type='text/plain')
-        return response.conditional_response_app
-
-    def app(self, request):
-        # Try registered paths without a tenant_name first
-        path = request.path
-        for path_re, handler in self.routes.values():
-            if path_re.match(path):
-                return handler(path, '', request)
-
-        # Now try with a tenant_name stripped
-        x, tenant_name, path = request.path.split('/', 2)
-        path = '/' + path
-        # Handle keys
-        if path.startswith('/keys'):
-            try:
-                return self._handle_keys(request, path)
-            except Exception as e:
-                self.log.exception("Issue with _handle_keys")
-                raise
-        for path_re, handler in self.routes.values():
-            if path_re.match(path):
-                return handler(path, tenant_name, request)
-        else:
-            raise webob.exc.HTTPNotFound()
-
-    def status(self, path, tenant_name, request):
-        def func():
-            return webob.Response(body=self.cache[tenant_name],
-                                  content_type='application/json',
-                                  charset='utf8')
-        if tenant_name not in self.scheduler.abide.tenants:
-            raise webob.exc.HTTPNotFound()
-        return self._response_with_status_cache(func, tenant_name)
-
-    def change(self, path, tenant_name, request):
-        def func():
-            m = re.match(self.change_path_regexp, path)
-            change_id = m.group(1)
-            status = self._status_for_change(change_id, tenant_name)
-            if status:
-                return webob.Response(body=status,
-                                      content_type='application/json',
-                                      charset='utf8')
-            else:
-                raise webob.exc.HTTPNotFound()
-        return self._response_with_status_cache(func, tenant_name)
-
-    def _refresh_status_cache(self, tenant_name):
-        if (tenant_name not in self.cache or
-            (time.time() - self.cache_time) > self.cache_expiry):
-            try:
-                self.cache[tenant_name] = self.scheduler.formatStatusJSON(
-                    tenant_name)
-                # Call time.time() again because formatting above may take
-                # longer than the cache timeout.
-                self.cache_time = time.time()
-            except Exception:
-                self.log.exception("Exception formatting status:")
-                raise
-
-    def _response_with_status_cache(self, func, tenant_name):
-        self._refresh_status_cache(tenant_name)
-
-        response = func()
-
-        response.headers['Access-Control-Allow-Origin'] = '*'
-
-        response.cache_control.public = True
-        response.cache_control.max_age = self.cache_expiry
-        response.last_modified = self.cache_time
-        response.expires = self.cache_time + self.cache_expiry
-
-        return response.conditional_response_app