blob: 134fb3c1ae50a9b5ae79c38d4e123d308a279e8d [file] [log] [blame]
James E. Blair1f4c2bb2013-04-26 08:40:46 -07001# Copyright 2012 Hewlett-Packard Development Company, L.P.
2# Copyright 2013 OpenStack Foundation
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
Sean Daguea8311bf2014-09-30 06:28:26 -040016import copy
17import json
James E. Blair1f4c2bb2013-04-26 08:40:46 -070018import logging
Sean Daguea8311bf2014-09-30 06:28:26 -040019import re
James E. Blair1f4c2bb2013-04-26 08:40:46 -070020import threading
Clark Boylane0b4bdb2014-06-03 17:01:25 -070021import time
James E. Blair1f4c2bb2013-04-26 08:40:46 -070022from paste import httpserver
Yuriy Taradaya6d452f2014-04-16 12:36:20 +040023import webob
24from webob import dec
James E. Blairbf1a4f22017-03-17 10:59:37 -070025
26from zuul.lib import encryption
James E. Blair1f4c2bb2013-04-26 08:40:46 -070027
Sean Daguea8311bf2014-09-30 06:28:26 -040028"""Zuul main web app.
29
30Zuul supports HTTP requests directly against it for determining the
31change status. These responses are provided as json data structures.
32
33The supported urls are:
34
35 - /status: return a complex data structure that represents the entire
36 queue / pipeline structure of the system
37 - /status.json (backwards compatibility): same as /status
38 - /status/change/X,Y: return status just for gerrit change X,Y
James E. Blairc49e5e72017-03-16 14:56:32 -070039 - /keys/SOURCE/PROJECT.pub: return the public key for PROJECT
Sean Daguea8311bf2014-09-30 06:28:26 -040040
41When returning status for a single gerrit change you will get an
42array of changes, they will not include the queue structure.
43"""
44
James E. Blair1f4c2bb2013-04-26 08:40:46 -070045
46class WebApp(threading.Thread):
47 log = logging.getLogger("zuul.WebApp")
Gregory Haynes4fc12542015-04-22 20:38:06 -070048 change_path_regexp = '/status/change/(.*)$'
James E. Blair1f4c2bb2013-04-26 08:40:46 -070049
Paul Belanger88ef0ea2015-12-23 11:57:02 -050050 def __init__(self, scheduler, port=8001, cache_expiry=1,
51 listen_address='0.0.0.0'):
James E. Blair1f4c2bb2013-04-26 08:40:46 -070052 threading.Thread.__init__(self)
53 self.scheduler = scheduler
Paul Belanger88ef0ea2015-12-23 11:57:02 -050054 self.listen_address = listen_address
James E. Blair1843a552013-07-03 14:19:52 -070055 self.port = port
Clark Boylane0b4bdb2014-06-03 17:01:25 -070056 self.cache_expiry = cache_expiry
57 self.cache_time = 0
Paul Belanger6349d152016-10-30 16:21:17 -040058 self.cache = {}
James E. Blairf31b3432014-03-11 15:35:00 -070059 self.daemon = True
Jan Hruban7083edd2015-08-21 14:00:54 +020060 self.routes = {}
61 self._init_default_routes()
Paul Belanger88ef0ea2015-12-23 11:57:02 -050062 self.server = httpserver.serve(
63 dec.wsgify(self.app), host=self.listen_address, port=self.port,
64 start_loop=False)
James E. Blairf31b3432014-03-11 15:35:00 -070065
Jan Hruban7083edd2015-08-21 14:00:54 +020066 def _init_default_routes(self):
67 self.register_path('/(status\.json|status)$', self.status)
68 self.register_path(self.change_path_regexp, self.change)
69
James E. Blairf31b3432014-03-11 15:35:00 -070070 def run(self):
James E. Blair1f4c2bb2013-04-26 08:40:46 -070071 self.server.serve_forever()
72
73 def stop(self):
74 self.server.server_close()
75
Clint Byrum77c184a2016-12-20 12:27:21 -080076 def _changes_by_func(self, func, tenant_name):
Sean Daguea8311bf2014-09-30 06:28:26 -040077 """Filter changes by a user provided function.
78
79 In order to support arbitrary collection of subsets of changes
80 we provide a low level filtering mechanism that takes a
81 function which applies to changes. The output of this function
82 is a flattened list of those collected changes.
83 """
84 status = []
Clint Byrum77c184a2016-12-20 12:27:21 -080085 jsonstruct = json.loads(self.cache[tenant_name])
Sean Daguea8311bf2014-09-30 06:28:26 -040086 for pipeline in jsonstruct['pipelines']:
87 for change_queue in pipeline['change_queues']:
88 for head in change_queue['heads']:
89 for change in head:
90 if func(change):
91 status.append(copy.deepcopy(change))
92 return json.dumps(status)
93
Clint Byrum77c184a2016-12-20 12:27:21 -080094 def _status_for_change(self, rev, tenant_name):
Sean Daguea8311bf2014-09-30 06:28:26 -040095 """Return the statuses for a particular change id X,Y."""
96 def func(change):
97 return change['id'] == rev
Clint Byrum77c184a2016-12-20 12:27:21 -080098 return self._changes_by_func(func, tenant_name)
Sean Daguea8311bf2014-09-30 06:28:26 -040099
Jan Hruban7083edd2015-08-21 14:00:54 +0200100 def register_path(self, path, handler):
101 path_re = re.compile(path)
102 self.routes[path] = (path_re, handler)
103
104 def unregister_path(self, path):
105 if self.routes.get(path):
106 del self.routes[path]
Sean Daguea8311bf2014-09-30 06:28:26 -0400107
James E. Blairc49e5e72017-03-16 14:56:32 -0700108 def _handle_keys(self, request, path):
109 m = re.match('/keys/(.*?)/(.*?).pub', path)
110 if not m:
111 raise webob.exc.HTTPNotFound()
112 source_name = m.group(1)
113 project_name = m.group(2)
114 source = self.scheduler.connections.getSource(source_name)
115 if not source:
116 raise webob.exc.HTTPNotFound()
117 project = source.getProject(project_name)
118 if not project:
119 raise webob.exc.HTTPNotFound()
120
James E. Blairbf1a4f22017-03-17 10:59:37 -0700121 pem_public_key = encryption.serialize_rsa_public_key(
122 project.public_key)
James E. Blairc49e5e72017-03-16 14:56:32 -0700123
124 response = webob.Response(body=pem_public_key,
125 content_type='text/plain')
126 return response.conditional_response_app
127
Yuriy Taradaya6d452f2014-04-16 12:36:20 +0400128 def app(self, request):
Jan Hruban7083edd2015-08-21 14:00:54 +0200129 # Try registered paths without a tenant_name first
130 path = request.path
Clint Byrume0093db2017-05-10 20:53:43 -0700131 for path_re, handler in self.routes.values():
Jan Hruban7083edd2015-08-21 14:00:54 +0200132 if path_re.match(path):
133 return handler(path, '', request)
134
135 # Now try with a tenant_name stripped
James E. Blairf0a12e72017-08-01 16:36:43 -0700136 x, tenant_name, path = request.path.split('/', 2)
137 path = '/' + path
Jan Hruban7083edd2015-08-21 14:00:54 +0200138 # Handle keys
James E. Blairc49e5e72017-03-16 14:56:32 -0700139 if path.startswith('/keys'):
Monty Taylorac37ff52017-08-01 18:24:08 -0500140 try:
141 return self._handle_keys(request, path)
142 except Exception as e:
143 self.log.exception("Issue with _handle_keys")
144 raise
Clint Byrume0093db2017-05-10 20:53:43 -0700145 for path_re, handler in self.routes.values():
Jan Hruban7083edd2015-08-21 14:00:54 +0200146 if path_re.match(path):
147 return handler(path, tenant_name, request)
148 else:
Yuriy Taradaya6d452f2014-04-16 12:36:20 +0400149 raise webob.exc.HTTPNotFound()
Sean Daguea8311bf2014-09-30 06:28:26 -0400150
Jan Hruban7083edd2015-08-21 14:00:54 +0200151 def status(self, path, tenant_name, request):
152 def func():
153 return webob.Response(body=self.cache[tenant_name],
Clint Byrum1e477c92017-05-10 20:53:54 -0700154 content_type='application/json',
155 charset='utf8')
Clint Byrum4c377602017-08-17 09:54:08 -0700156 if tenant_name not in self.scheduler.abide.tenants:
157 raise webob.exc.HTTPNotFound()
Jan Hruban7083edd2015-08-21 14:00:54 +0200158 return self._response_with_status_cache(func, tenant_name)
159
160 def change(self, path, tenant_name, request):
161 def func():
162 m = re.match(self.change_path_regexp, path)
163 change_id = m.group(1)
164 status = self._status_for_change(change_id, tenant_name)
165 if status:
166 return webob.Response(body=status,
Clint Byrum1e477c92017-05-10 20:53:54 -0700167 content_type='application/json',
168 charset='utf8')
Jan Hruban7083edd2015-08-21 14:00:54 +0200169 else:
170 raise webob.exc.HTTPNotFound()
171 return self._response_with_status_cache(func, tenant_name)
172
173 def _refresh_status_cache(self, tenant_name):
Paul Belanger6349d152016-10-30 16:21:17 -0400174 if (tenant_name not in self.cache or
Clark Boylane0b4bdb2014-06-03 17:01:25 -0700175 (time.time() - self.cache_time) > self.cache_expiry):
176 try:
Paul Belanger6349d152016-10-30 16:21:17 -0400177 self.cache[tenant_name] = self.scheduler.formatStatusJSON(
178 tenant_name)
Clark Boylane0b4bdb2014-06-03 17:01:25 -0700179 # Call time.time() again because formatting above may take
180 # longer than the cache timeout.
181 self.cache_time = time.time()
182 except:
183 self.log.exception("Exception formatting status:")
184 raise
Sean Daguea8311bf2014-09-30 06:28:26 -0400185
Jan Hruban7083edd2015-08-21 14:00:54 +0200186 def _response_with_status_cache(self, func, tenant_name):
187 self._refresh_status_cache(tenant_name)
188
189 response = func()
Sean Daguea8311bf2014-09-30 06:28:26 -0400190
Yuriy Taradaya6d452f2014-04-16 12:36:20 +0400191 response.headers['Access-Control-Allow-Origin'] = '*'
Timo Tijhof0ebd2932015-04-02 12:11:21 +0100192
193 response.cache_control.public = True
194 response.cache_control.max_age = self.cache_expiry
Clark Boylanaa4f2e72014-06-03 21:22:40 -0700195 response.last_modified = self.cache_time
Timo Tijhof0ebd2932015-04-02 12:11:21 +0100196 response.expires = self.cache_time + self.cache_expiry
197
198 return response.conditional_response_app