Initial import of zuul storage proxy
Change-Id: I75f3910095b669e502127b3870dba9616da0b3c8
This commit is contained in:
parent
c6ec750c40
commit
bf54ed0b03
14
.zuul.yaml
Normal file
14
.zuul.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
- project:
|
||||
check:
|
||||
jobs:
|
||||
- tox-linters:
|
||||
vars:
|
||||
tox_install_bindep: false
|
||||
gate:
|
||||
jobs:
|
||||
- tox-linters:
|
||||
vars:
|
||||
tox_install_bindep: false
|
||||
release:
|
||||
jobs:
|
||||
- zuul-release-python
|
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM python:3.6-slim
|
||||
|
||||
RUN python -m venv /opt/swift-proxy
|
||||
|
||||
RUN mkdir -p /opt/swift-proxy-src
|
||||
|
||||
COPY . /opt/swift-proxy-src/
|
||||
|
||||
RUN PBR_VERSION=1.0.0 /opt/swift-proxy/bin/pip install /opt/swift-proxy-src
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD /opt/swift-proxy/bin/gunicorn -k eventlet -t 3600 --workers 10 swift_proxy:proxy
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
gunicorn[eventlet]
|
||||
openstacksdk
|
8
setup.cfg
Normal file
8
setup.cfg
Normal file
@ -0,0 +1,8 @@
|
||||
[metadata]
|
||||
name = zuul-storage-proxy
|
||||
requires-python = >=3.6
|
||||
requires-dist =
|
||||
setuptools
|
||||
|
||||
[pbr]
|
||||
warnerrors = True
|
23
setup.py
Normal file
23
setup.py
Normal file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr'],
|
||||
version='1.0.0',
|
||||
pbr=True)
|
1
test-requirements.txt
Normal file
1
test-requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
flake8
|
18
tox.ini
Normal file
18
tox.ini
Normal file
@ -0,0 +1,18 @@
|
||||
[tox]
|
||||
minversion = 1.6
|
||||
skipsdist = True
|
||||
envlist = linters
|
||||
|
||||
[testenv]
|
||||
basepython = python3
|
||||
|
||||
install_command = pip install {opts} {packages}
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:linters]
|
||||
commands = flake8 {posargs}
|
||||
|
||||
[flake8]
|
||||
show-source = True
|
||||
exclude = .venv*,.tox,dist,doc,build,*.egg,external
|
207
zuul_storage_proxy/__init__.py
Normal file
207
zuul_storage_proxy/__init__.py
Normal file
@ -0,0 +1,207 @@
|
||||
# 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 path inside tenant given, redirect to root index
|
||||
return redirect_directory(start_response, path)
|
||||
|
||||
tenant = components[0]
|
||||
path = components[1]
|
||||
container = '-'.join([container_prefix, tenant])
|
||||
|
||||
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
|
||||
|
||||
if 'CLOUD_NAME' in os.environ:
|
||||
cloud_names = [os.environ['CLOUD_NAME']]
|
||||
else:
|
||||
cloud_names = os.environ['CLOUD_NAMES'].split(',')
|
||||
|
||||
self.clouds = []
|
||||
for cloud_name in cloud_names:
|
||||
self.log.warning('Using cloud %s', cloud_name)
|
||||
self.clouds.append(openstack.connect(cloud=cloud_name))
|
||||
self.container_prefix = os.environ['CONTAINER_PREFIX']
|
||||
self.log.warning('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)
|
Loading…
Reference in New Issue
Block a user