
CLOUD_NAMES vs CLOUD_NAME is a one character difference which may be easily overlooked. The split method will work even with only one item and no ','. And in Zuul we already have the convention of describing an input that accepts one or more items with a plural noun. Change-Id: I28b8b6be2b536d627bf77665b5c3e583000efbda
207 lines
6.0 KiB
Python
207 lines
6.0 KiB
Python
# Copyright 2020 BMW Group
|
|
#
|
|
# 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 logging
|
|
import openstack
|
|
import os
|
|
|
|
NOT_FOUND = b'''<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
|
<html><head>
|
|
<title>404 Not Found</title>
|
|
</head><body>
|
|
<h1>Not Found</h1>
|
|
<p>The requested URL was not found on this server.</p>
|
|
</body></html>
|
|
'''
|
|
|
|
METHOD_NOT_ALLOWED = b'''<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
|
<html><head>
|
|
<title>405 Method Not Allowed</title>
|
|
</head><body>
|
|
<h1>Method Not Allowed</h1>
|
|
<p>The requested method is not supported.</p>
|
|
</body></html>
|
|
'''
|
|
|
|
FORBIDDEN = b'''<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
|
<html><head>
|
|
<title>403 Forbidden</title>
|
|
</head><body>
|
|
<h1>Forbidden</h1>
|
|
<p>Overwriting files is not allowed.</p>
|
|
</body></html>
|
|
'''
|
|
|
|
BACKEND_CONNECTION = b'''<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
|
<html><head>
|
|
<title>503</title>
|
|
</head><body>
|
|
<h1>Backend problem</h1>
|
|
<p>Backend connection failed.</p>
|
|
</body></html>
|
|
'''
|
|
|
|
|
|
def redirect_directory(start_response, path):
|
|
# We need to redirect with the last element plus a terminating '/'
|
|
last_element = os.path.basename(path.rstrip('/'))
|
|
redirect = last_element + '/'
|
|
|
|
start_response(
|
|
'301 Moved Permanently',
|
|
[('Location', redirect)]
|
|
)
|
|
return iter([b''])
|
|
|
|
|
|
def data_generator(data):
|
|
chunk_size = 16384
|
|
buf = data.read(chunk_size)
|
|
while buf:
|
|
yield buf
|
|
buf = data.read(chunk_size)
|
|
|
|
|
|
def handle_get(start_response, clouds, container, path):
|
|
failures = []
|
|
for cloud in clouds:
|
|
with cloud.object_store.get(
|
|
'%s/%s' % (container, path),
|
|
stream=True) as response:
|
|
response_headers = {k: v for k, v in response.headers.items()}
|
|
|
|
if response.status_code == 404:
|
|
failures.append((404, ''))
|
|
continue
|
|
elif response.status_code != 200:
|
|
failures.append((response.status_code, response.reason))
|
|
continue
|
|
|
|
data = b'Status code %s' % str(response.status_code).encode()
|
|
start_response(
|
|
"{} {}".format(response.status_code, response.reason),
|
|
[('Content-Length', str(len(data)))]
|
|
)
|
|
yield data
|
|
return
|
|
|
|
if response_headers['Content-Type'] == 'application/directory':
|
|
# We got a directory so redirect it to path/index.html
|
|
return redirect_directory(start_response, path)
|
|
|
|
start_response(
|
|
"{} {}".format(response.status_code, response.reason),
|
|
response_headers.items())
|
|
|
|
# We want to forward the compressed data stream here so use the raw
|
|
# response stream.
|
|
while response.raw:
|
|
data = response.raw.read(16384)
|
|
yield data
|
|
if len(data) == 0:
|
|
break
|
|
return
|
|
|
|
# When hitting a special failure in one cloud return this,
|
|
# otherwise return 404
|
|
for status_code, reason in failures:
|
|
if status_code == 404:
|
|
# handle later
|
|
continue
|
|
|
|
# We hit a special failure, report this
|
|
data = b'Status code %s' % str(status_code).encode()
|
|
start_response(
|
|
"{} {}".format(status_code, reason),
|
|
[('Content-Length', str(len(data)))]
|
|
)
|
|
yield data
|
|
return
|
|
|
|
# No special failure, return 404
|
|
start_response(
|
|
'404 Not Found', []
|
|
)
|
|
yield NOT_FOUND
|
|
return
|
|
|
|
|
|
def swift_proxy(environ, start_response, clouds, container_prefix):
|
|
path = environ['PATH_INFO'].lstrip('/')
|
|
method = environ['REQUEST_METHOD']
|
|
|
|
# If the path ends with a / this is a directory and we want to deliver the
|
|
# index.html instead.
|
|
if path.endswith('/'):
|
|
path += 'index.html'
|
|
|
|
components = path.split('/', 1)
|
|
|
|
if not path:
|
|
start_response(
|
|
'404 Not Found', []
|
|
)
|
|
yield NOT_FOUND
|
|
return
|
|
|
|
if len(components) < 2:
|
|
# no container or path given, redirect to root index
|
|
return redirect_directory(start_response, path)
|
|
|
|
container = components[0]
|
|
path = components[1]
|
|
container = container_prefix + container
|
|
|
|
print('%s request %s/%s' % (method, container, path))
|
|
try:
|
|
if method == 'GET':
|
|
for chunk in handle_get(start_response, clouds, container, path):
|
|
yield chunk
|
|
else:
|
|
start_response(
|
|
'405 Method Not Allowed', []
|
|
)
|
|
yield METHOD_NOT_ALLOWED
|
|
except Exception as e:
|
|
start_response(
|
|
'503 Backend connection failed', []
|
|
)
|
|
yield '{}\n'.format(e).encode()
|
|
|
|
|
|
class CloudCache(object):
|
|
|
|
def __init__(self, app):
|
|
self.log = logging.getLogger('middleware')
|
|
self.app = app
|
|
|
|
cloud_names = os.environ['CLOUD_NAMES'].split(',')
|
|
|
|
self.clouds = []
|
|
for cloud_name in cloud_names:
|
|
self.log.info('Using cloud %s', cloud_name)
|
|
self.clouds.append(openstack.connect(cloud=cloud_name))
|
|
self.container_prefix = os.environ.get('CONTAINER_PREFIX', '')
|
|
if self.container_prefix:
|
|
self.log.info('Using container prefix %s',
|
|
self.container_prefix)
|
|
|
|
def __call__(self, environ, start_response):
|
|
for chunk in self.app(environ, start_response, self.clouds,
|
|
self.container_prefix):
|
|
yield chunk
|
|
|
|
|
|
proxy = CloudCache(swift_proxy)
|