James E. Blair | 1f4c2bb | 2013-04-26 08:40:46 -0700 | [diff] [blame] | 1 | # 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 Dague | a8311bf | 2014-09-30 06:28:26 -0400 | [diff] [blame] | 16 | import copy |
| 17 | import json |
James E. Blair | 1f4c2bb | 2013-04-26 08:40:46 -0700 | [diff] [blame] | 18 | import logging |
Sean Dague | a8311bf | 2014-09-30 06:28:26 -0400 | [diff] [blame] | 19 | import re |
James E. Blair | 1f4c2bb | 2013-04-26 08:40:46 -0700 | [diff] [blame] | 20 | import threading |
Clark Boylan | e0b4bdb | 2014-06-03 17:01:25 -0700 | [diff] [blame] | 21 | import time |
James E. Blair | 1f4c2bb | 2013-04-26 08:40:46 -0700 | [diff] [blame] | 22 | from paste import httpserver |
Yuriy Taraday | a6d452f | 2014-04-16 12:36:20 +0400 | [diff] [blame] | 23 | import webob |
| 24 | from webob import dec |
James E. Blair | bf1a4f2 | 2017-03-17 10:59:37 -0700 | [diff] [blame] | 25 | |
| 26 | from zuul.lib import encryption |
James E. Blair | 1f4c2bb | 2013-04-26 08:40:46 -0700 | [diff] [blame] | 27 | |
Sean Dague | a8311bf | 2014-09-30 06:28:26 -0400 | [diff] [blame] | 28 | """Zuul main web app. |
| 29 | |
| 30 | Zuul supports HTTP requests directly against it for determining the |
| 31 | change status. These responses are provided as json data structures. |
| 32 | |
| 33 | The 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. Blair | c49e5e7 | 2017-03-16 14:56:32 -0700 | [diff] [blame] | 39 | - /keys/SOURCE/PROJECT.pub: return the public key for PROJECT |
Sean Dague | a8311bf | 2014-09-30 06:28:26 -0400 | [diff] [blame] | 40 | |
| 41 | When returning status for a single gerrit change you will get an |
| 42 | array of changes, they will not include the queue structure. |
| 43 | """ |
| 44 | |
James E. Blair | 1f4c2bb | 2013-04-26 08:40:46 -0700 | [diff] [blame] | 45 | |
| 46 | class WebApp(threading.Thread): |
| 47 | log = logging.getLogger("zuul.WebApp") |
Gregory Haynes | 4fc1254 | 2015-04-22 20:38:06 -0700 | [diff] [blame] | 48 | change_path_regexp = '/status/change/(.*)$' |
James E. Blair | 1f4c2bb | 2013-04-26 08:40:46 -0700 | [diff] [blame] | 49 | |
Paul Belanger | 88ef0ea | 2015-12-23 11:57:02 -0500 | [diff] [blame] | 50 | def __init__(self, scheduler, port=8001, cache_expiry=1, |
| 51 | listen_address='0.0.0.0'): |
James E. Blair | 1f4c2bb | 2013-04-26 08:40:46 -0700 | [diff] [blame] | 52 | threading.Thread.__init__(self) |
| 53 | self.scheduler = scheduler |
Paul Belanger | 88ef0ea | 2015-12-23 11:57:02 -0500 | [diff] [blame] | 54 | self.listen_address = listen_address |
James E. Blair | 1843a55 | 2013-07-03 14:19:52 -0700 | [diff] [blame] | 55 | self.port = port |
Clark Boylan | e0b4bdb | 2014-06-03 17:01:25 -0700 | [diff] [blame] | 56 | self.cache_expiry = cache_expiry |
| 57 | self.cache_time = 0 |
Paul Belanger | 6349d15 | 2016-10-30 16:21:17 -0400 | [diff] [blame] | 58 | self.cache = {} |
James E. Blair | f31b343 | 2014-03-11 15:35:00 -0700 | [diff] [blame] | 59 | self.daemon = True |
Jan Hruban | 7083edd | 2015-08-21 14:00:54 +0200 | [diff] [blame] | 60 | self.routes = {} |
| 61 | self._init_default_routes() |
Paul Belanger | 88ef0ea | 2015-12-23 11:57:02 -0500 | [diff] [blame] | 62 | self.server = httpserver.serve( |
| 63 | dec.wsgify(self.app), host=self.listen_address, port=self.port, |
| 64 | start_loop=False) |
James E. Blair | f31b343 | 2014-03-11 15:35:00 -0700 | [diff] [blame] | 65 | |
Jan Hruban | 7083edd | 2015-08-21 14:00:54 +0200 | [diff] [blame] | 66 | 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. Blair | f31b343 | 2014-03-11 15:35:00 -0700 | [diff] [blame] | 70 | def run(self): |
James E. Blair | 1f4c2bb | 2013-04-26 08:40:46 -0700 | [diff] [blame] | 71 | self.server.serve_forever() |
| 72 | |
| 73 | def stop(self): |
| 74 | self.server.server_close() |
| 75 | |
Clint Byrum | 77c184a | 2016-12-20 12:27:21 -0800 | [diff] [blame] | 76 | def _changes_by_func(self, func, tenant_name): |
Sean Dague | a8311bf | 2014-09-30 06:28:26 -0400 | [diff] [blame] | 77 | """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 Byrum | 77c184a | 2016-12-20 12:27:21 -0800 | [diff] [blame] | 85 | jsonstruct = json.loads(self.cache[tenant_name]) |
Sean Dague | a8311bf | 2014-09-30 06:28:26 -0400 | [diff] [blame] | 86 | 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 Byrum | 77c184a | 2016-12-20 12:27:21 -0800 | [diff] [blame] | 94 | def _status_for_change(self, rev, tenant_name): |
Sean Dague | a8311bf | 2014-09-30 06:28:26 -0400 | [diff] [blame] | 95 | """Return the statuses for a particular change id X,Y.""" |
| 96 | def func(change): |
| 97 | return change['id'] == rev |
Clint Byrum | 77c184a | 2016-12-20 12:27:21 -0800 | [diff] [blame] | 98 | return self._changes_by_func(func, tenant_name) |
Sean Dague | a8311bf | 2014-09-30 06:28:26 -0400 | [diff] [blame] | 99 | |
Jan Hruban | 7083edd | 2015-08-21 14:00:54 +0200 | [diff] [blame] | 100 | 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 Dague | a8311bf | 2014-09-30 06:28:26 -0400 | [diff] [blame] | 107 | |
James E. Blair | c49e5e7 | 2017-03-16 14:56:32 -0700 | [diff] [blame] | 108 | 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. Blair | bf1a4f2 | 2017-03-17 10:59:37 -0700 | [diff] [blame] | 121 | pem_public_key = encryption.serialize_rsa_public_key( |
| 122 | project.public_key) |
James E. Blair | c49e5e7 | 2017-03-16 14:56:32 -0700 | [diff] [blame] | 123 | |
| 124 | response = webob.Response(body=pem_public_key, |
| 125 | content_type='text/plain') |
| 126 | return response.conditional_response_app |
| 127 | |
Yuriy Taraday | a6d452f | 2014-04-16 12:36:20 +0400 | [diff] [blame] | 128 | def app(self, request): |
Jan Hruban | 7083edd | 2015-08-21 14:00:54 +0200 | [diff] [blame] | 129 | # Try registered paths without a tenant_name first |
| 130 | path = request.path |
Clint Byrum | e0093db | 2017-05-10 20:53:43 -0700 | [diff] [blame] | 131 | for path_re, handler in self.routes.values(): |
Jan Hruban | 7083edd | 2015-08-21 14:00:54 +0200 | [diff] [blame] | 132 | if path_re.match(path): |
| 133 | return handler(path, '', request) |
| 134 | |
| 135 | # Now try with a tenant_name stripped |
James E. Blair | f0a12e7 | 2017-08-01 16:36:43 -0700 | [diff] [blame] | 136 | x, tenant_name, path = request.path.split('/', 2) |
| 137 | path = '/' + path |
Jan Hruban | 7083edd | 2015-08-21 14:00:54 +0200 | [diff] [blame] | 138 | # Handle keys |
James E. Blair | c49e5e7 | 2017-03-16 14:56:32 -0700 | [diff] [blame] | 139 | if path.startswith('/keys'): |
Monty Taylor | ac37ff5 | 2017-08-01 18:24:08 -0500 | [diff] [blame] | 140 | try: |
| 141 | return self._handle_keys(request, path) |
| 142 | except Exception as e: |
| 143 | self.log.exception("Issue with _handle_keys") |
| 144 | raise |
Clint Byrum | e0093db | 2017-05-10 20:53:43 -0700 | [diff] [blame] | 145 | for path_re, handler in self.routes.values(): |
Jan Hruban | 7083edd | 2015-08-21 14:00:54 +0200 | [diff] [blame] | 146 | if path_re.match(path): |
| 147 | return handler(path, tenant_name, request) |
| 148 | else: |
Yuriy Taraday | a6d452f | 2014-04-16 12:36:20 +0400 | [diff] [blame] | 149 | raise webob.exc.HTTPNotFound() |
Sean Dague | a8311bf | 2014-09-30 06:28:26 -0400 | [diff] [blame] | 150 | |
Jan Hruban | 7083edd | 2015-08-21 14:00:54 +0200 | [diff] [blame] | 151 | def status(self, path, tenant_name, request): |
| 152 | def func(): |
| 153 | return webob.Response(body=self.cache[tenant_name], |
Clint Byrum | 1e477c9 | 2017-05-10 20:53:54 -0700 | [diff] [blame] | 154 | content_type='application/json', |
| 155 | charset='utf8') |
Clint Byrum | 4c37760 | 2017-08-17 09:54:08 -0700 | [diff] [blame] | 156 | if tenant_name not in self.scheduler.abide.tenants: |
| 157 | raise webob.exc.HTTPNotFound() |
Jan Hruban | 7083edd | 2015-08-21 14:00:54 +0200 | [diff] [blame] | 158 | 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 Byrum | 1e477c9 | 2017-05-10 20:53:54 -0700 | [diff] [blame] | 167 | content_type='application/json', |
| 168 | charset='utf8') |
Jan Hruban | 7083edd | 2015-08-21 14:00:54 +0200 | [diff] [blame] | 169 | 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 Belanger | 6349d15 | 2016-10-30 16:21:17 -0400 | [diff] [blame] | 174 | if (tenant_name not in self.cache or |
Clark Boylan | e0b4bdb | 2014-06-03 17:01:25 -0700 | [diff] [blame] | 175 | (time.time() - self.cache_time) > self.cache_expiry): |
| 176 | try: |
Paul Belanger | 6349d15 | 2016-10-30 16:21:17 -0400 | [diff] [blame] | 177 | self.cache[tenant_name] = self.scheduler.formatStatusJSON( |
| 178 | tenant_name) |
Clark Boylan | e0b4bdb | 2014-06-03 17:01:25 -0700 | [diff] [blame] | 179 | # 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 Dague | a8311bf | 2014-09-30 06:28:26 -0400 | [diff] [blame] | 185 | |
Jan Hruban | 7083edd | 2015-08-21 14:00:54 +0200 | [diff] [blame] | 186 | def _response_with_status_cache(self, func, tenant_name): |
| 187 | self._refresh_status_cache(tenant_name) |
| 188 | |
| 189 | response = func() |
Sean Dague | a8311bf | 2014-09-30 06:28:26 -0400 | [diff] [blame] | 190 | |
Yuriy Taraday | a6d452f | 2014-04-16 12:36:20 +0400 | [diff] [blame] | 191 | response.headers['Access-Control-Allow-Origin'] = '*' |
Timo Tijhof | 0ebd293 | 2015-04-02 12:11:21 +0100 | [diff] [blame] | 192 | |
| 193 | response.cache_control.public = True |
| 194 | response.cache_control.max_age = self.cache_expiry |
Clark Boylan | aa4f2e7 | 2014-06-03 21:22:40 -0700 | [diff] [blame] | 195 | response.last_modified = self.cache_time |
Timo Tijhof | 0ebd293 | 2015-04-02 12:11:21 +0100 | [diff] [blame] | 196 | response.expires = self.cache_time + self.cache_expiry |
| 197 | |
| 198 | return response.conditional_response_app |