Add Swift storage library

This commit is contained in:
Joseph Schorr 2015-05-21 15:22:59 -04:00
parent 417c77f4d9
commit 5845e37e32
9 changed files with 341 additions and 33 deletions

View file

@ -21,7 +21,6 @@ paramiko
xhtml2pdf xhtml2pdf
redis redis
hiredis hiredis
docker-py
flask-restful==0.2.12 flask-restful==0.2.12
jsonschema jsonschema
git+https://github.com/NateFerrero/oauth2lib.git git+https://github.com/NateFerrero/oauth2lib.git
@ -52,3 +51,5 @@ mock
psutil psutil
stringscore stringscore
mockldap mockldap
python-swiftclient
python-keystoneclient

View file

@ -1,68 +1,91 @@
APScheduler==3.0.1 APScheduler==3.0.3
Babel==1.3
Flask==0.10.1 Flask==0.10.1
Flask-Login==0.2.11 Flask-Login==0.2.11
Flask-Mail==0.9.1 Flask-Mail==0.9.1
Flask-Principal==0.4.0 Flask-Principal==0.4.0
Flask-RESTful==0.2.12 Flask-RESTful==0.2.12
Jinja2==2.7.3 Jinja2==2.7.3
LogentriesLogger==0.2.1 Logentries==0.7
Mako==1.0.0 Mako==1.0.1
MarkupSafe==0.23 MarkupSafe==0.23
Pillow==2.7.0 Pillow==2.8.1
PyMySQL==0.6.3 PyMySQL==0.6.6
PyPDF2==1.24 PyPDF2==1.24
PyYAML==3.11 PyYAML==3.11
SQLAlchemy==0.9.8 SQLAlchemy==1.0.3
WebOb==1.4 WebOb==1.4.1
Werkzeug==0.9.6 Werkzeug==0.10.4
aiowsgi==0.3 aiowsgi==0.5
alembic==0.7.4 alembic==0.7.5.post2
argparse==1.3.0
autobahn==0.9.3-3 autobahn==0.9.3-3
backports.ssl-match-hostname==3.4.0.2 backports.ssl-match-hostname==3.4.0.2
beautifulsoup4==4.3.2 beautifulsoup4==4.3.2
blinker==1.3 blinker==1.3
boto==2.35.1 boto==2.38.0
cachetools==1.0.0 cachetools==1.0.0
docker-py==0.7.1 certifi==2015.04.28
ecdsa==0.11 cffi==0.9.2
cryptography==0.8.2
ecdsa==0.13
enum34==1.0.4
funcparserlib==0.3.6
futures==2.2.0 futures==2.2.0
gevent==1.0.1 gevent==1.0.1
gipc==0.5.0 gipc==0.5.0
greenlet==0.4.5 greenlet==0.4.5
gunicorn==18.0 gunicorn==18.0
hiredis==0.1.5 hiredis==0.2.0
html5lib==0.999 html5lib==0.99999
iso8601==0.1.10
itsdangerous==0.24 itsdangerous==0.24
jsonschema==2.4.0 jsonschema==2.4.0
marisa-trie==0.7 marisa-trie==0.7.2
mixpanel-py==3.2.1 mixpanel-py==4.0.2
mock==1.0.1 mock==1.0.1
mockldap==0.2.4 mockldap==0.2.4
msgpack-python==0.4.6
netaddr==0.7.14
netifaces==0.10.4
oauthlib==0.7.2
oslo.config==1.11.0
oslo.i18n==1.6.0
oslo.serialization==1.5.0
oslo.utils==1.5.0
paramiko==1.15.2 paramiko==1.15.2
peewee==2.4.7 pbr==0.11.0
peewee==2.6.0
prettytable==0.7.2
psutil==2.2.1 psutil==2.2.1
psycopg2==2.5.4 psycopg2==2.6
py-bcrypt==0.4 py-bcrypt==0.4
pyOpenSSL==0.15.1
pyasn1==0.1.7
pycparser==2.12
pycrypto==2.6.1 pycrypto==2.6.1
python-dateutil==2.4.0 pygpgme==0.3
python-dateutil==2.4.2
python-keystoneclient==1.4.0
python-ldap==2.4.19 python-ldap==2.4.19
python-magic==0.4.6 python-magic==0.4.6
pygpgme==0.3 python-swiftclient==2.4.0
pytz==2014.10 pytz==2015.2
pyOpenSSL==0.14 raven==5.3.0
raven==5.1.1
redis==2.10.3 redis==2.10.3
reportlab==2.7 reportlab==2.7
requests==2.5.1 requests==2.6.2
requests-oauthlib==0.4.2 requests-oauthlib==0.4.2
simplejson==3.7.1
six==1.9.0 six==1.9.0
stevedore==1.4.0
stringscore==0.1.0 stringscore==0.1.0
stripe==1.20.1 stripe==1.22.2
trollius==1.0.4 trollius==1.0.4
tzlocal==1.1.2 tzlocal==1.1.3
urllib3==1.10.2 urllib3==1.10.3
waitress==0.8.9 waitress==0.8.9
websocket-client==0.23.0 websocket-client==0.30.0
wsgiref==0.1.2 wsgiref==0.1.2
xhtml2pdf==0.0.6 xhtml2pdf==0.0.6
git+https://github.com/DevTable/aniso8601-fake.git git+https://github.com/DevTable/aniso8601-fake.git

View file

@ -388,6 +388,29 @@ a:focus {
width: 400px; width: 400px;
} }
.config-map-field-element table {
margin-bottom: 10px;
}
.config-map-field-element .form-control-container {
border-top: 1px solid #eee;
padding-top: 10px;
}
.config-map-field-element .form-control-container select, .config-map-field-element .form-control-container input {
margin-bottom: 10px;
}
.config-map-field-element .empty {
color: #ccc;
margin-bottom: 10px;
display: block;
}
.config-map-field-element .item-title {
font-weight: bold;
}
.config-contact-field { .config-contact-field {
margin-bottom: 4px; margin-bottom: 4px;
} }

View file

@ -0,0 +1,20 @@
<div class="config-map-field-element">
<table class="table" ng-show="hasValues(binding)">
<tr class="item" ng-repeat="(key, value) in binding">
<td class="item-title">{{ key }}</td>
<td class="item-value">{{ value }}</td>
<td class="item-delete">
<a href="javascript:void(0)" ng-click="removeKey(key)">Remove</a>
</td>
</tr>
</table>
<span class="empty" ng-if="!hasValues(binding)">No entries defined</span>
<form class="form-control-container" ng-submit="addEntry()">
Add Key-Value:
<select ng-model="newKey">
<option ng-repeat="key in keys" value="{{ key }}">{{ key }}</option>
</select>
<input type="text" class="form-control" placeholder="Value" ng-model="newValue">
<button class="btn btn-default" style="display: inline-block">Add Entry</button>
</form>
</div>

View file

@ -184,6 +184,7 @@
<option value="S3Storage">Amazon S3</option> <option value="S3Storage">Amazon S3</option>
<option value="GoogleCloudStorage">Google Cloud Storage</option> <option value="GoogleCloudStorage">Google Cloud Storage</option>
<option value="RadosGWStorage">Ceph Object Gateway (RADOS)</option> <option value="RadosGWStorage">Ceph Object Gateway (RADOS)</option>
<option value="SwiftStorage">OpenStack Storage (Swift)</option>
</select> </select>
</td> </td>
</tr> </tr>
@ -192,10 +193,15 @@
<tr ng-repeat="field in STORAGE_CONFIG_FIELDS[config.DISTRIBUTED_STORAGE_CONFIG.local[0]]"> <tr ng-repeat="field in STORAGE_CONFIG_FIELDS[config.DISTRIBUTED_STORAGE_CONFIG.local[0]]">
<td>{{ field.title }}:</td> <td>{{ field.title }}:</td>
<td> <td>
<span class="config-map-field"
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]"
ng-if="field.kind == 'map'"
keys="field.keys"></span>
<span class="config-string-field" <span class="config-string-field"
binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]" binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]"
placeholder="{{ field.placeholder }}" placeholder="{{ field.placeholder }}"
ng-if="field.kind == 'text'"></span> ng-if="field.kind == 'text'"
is-optional="field.optional"></span>
<div class="co-checkbox" ng-if="field.kind == 'bool'"> <div class="co-checkbox" ng-if="field.kind == 'bool'">
<input id="dsc-{{ field.name }}" type="checkbox" <input id="dsc-{{ field.name }}" type="checkbox"
ng-model="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]"> ng-model="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]">

View file

@ -2,7 +2,7 @@
<form name="fieldform" novalidate> <form name="fieldform" novalidate>
<input type="text" class="form-control" placeholder="{{ placeholder || '' }}" <input type="text" class="form-control" placeholder="{{ placeholder || '' }}"
ng-model="binding" ng-trim="false" ng-minlength="1" ng-model="binding" ng-trim="false" ng-minlength="1"
ng-pattern="getRegexp(pattern)" required> ng-pattern="getRegexp(pattern)" ng-required="!isOptional">
<div class="alert alert-danger" ng-show="errorMessage"> <div class="alert alert-danger" ng-show="errorMessage">
{{ errorMessage }} {{ errorMessage }}
</div> </div>

View file

@ -78,6 +78,19 @@ angular.module("core-config-setup", ['angularFileUpload'])
{'name': 'secret_key', 'title': 'Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'}, {'name': 'secret_key', 'title': 'Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
{'name': 'bucket_name', 'title': 'Bucket Name', 'placeholder': 'my-cool-bucket', 'kind': 'text'}, {'name': 'bucket_name', 'title': 'Bucket Name', 'placeholder': 'my-cool-bucket', 'kind': 'text'},
{'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'} {'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'}
],
'SwiftStorage': [
{'name': 'auth_url', 'title': 'Swift Auth URL', 'placeholder': '', 'kind': 'text'},
{'name': 'swift_container', 'title': 'Swift Container Name', 'placeholder': 'mycontainer', 'kind': 'text'},
{'name': 'storage_path', 'title': 'Storage Path', 'placeholder': '/path/inside/container', 'kind': 'text'},
{'name': 'swift_user', 'title': 'Username', 'placeholder': 'accesskeyhere', 'kind': 'text'},
{'name': 'swift_password', 'title': 'Password/Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
{'name': 'ca_cert_path', 'title': 'CA Cert Filename', 'placeholder': 'conf/stack/swift.cert', 'kind': 'text', 'optional': true},
{'name': 'os_options', 'title': 'OS Options', 'kind': 'map',
'keys': ['tenant_id', 'auth_token', 'service_type', 'endpoint_type', 'tenant_name', 'object_storage_url', 'region_name']}
] ]
}; };
@ -760,6 +773,42 @@ angular.module("core-config-setup", ['angularFileUpload'])
return directiveDefinitionObject; return directiveDefinitionObject;
}) })
.directive('configMapField', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/config/config-map-field.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'binding': '=binding',
'keys': '=keys'
},
controller: function($scope, $element) {
$scope.newKey = null;
$scope.newValue = null;
$scope.hasValues = function(binding) {
return binding && Object.keys(binding).length;
};
$scope.removeKey = function(key) {
delete $scope.binding[key];
};
$scope.addEntry = function() {
if (!$scope.newKey || !$scope.newValue) { return; }
$scope.binding = $scope.binding || {};
$scope.binding[$scope.newKey] = $scope.newValue;
$scope.newKey = null;
$scope.newValue = null;
}
}
};
return directiveDefinitionObject;
})
.directive('configStringField', function () { .directive('configStringField', function () {
var directiveDefinitionObject = { var directiveDefinitionObject = {
priority: 0, priority: 0,
@ -772,7 +821,8 @@ angular.module("core-config-setup", ['angularFileUpload'])
'placeholder': '@placeholder', 'placeholder': '@placeholder',
'pattern': '@pattern', 'pattern': '@pattern',
'defaultValue': '@defaultValue', 'defaultValue': '@defaultValue',
'validator': '&validator' 'validator': '&validator',
'isOptional': '=isOptional'
}, },
controller: function($scope, $element) { controller: function($scope, $element) {
$scope.getRegexp = function(pattern) { $scope.getRegexp = function(pattern) {

View file

@ -2,6 +2,7 @@ from storage.local import LocalStorage
from storage.cloud import S3Storage, GoogleCloudStorage, RadosGWStorage from storage.cloud import S3Storage, GoogleCloudStorage, RadosGWStorage
from storage.fakestorage import FakeStorage from storage.fakestorage import FakeStorage
from storage.distributedstorage import DistributedStorage from storage.distributedstorage import DistributedStorage
from storage.swift import SwiftStorage
STORAGE_DRIVER_CLASSES = { STORAGE_DRIVER_CLASSES = {
@ -9,6 +10,7 @@ STORAGE_DRIVER_CLASSES = {
'S3Storage': S3Storage, 'S3Storage': S3Storage,
'GoogleCloudStorage': GoogleCloudStorage, 'GoogleCloudStorage': GoogleCloudStorage,
'RadosGWStorage': RadosGWStorage, 'RadosGWStorage': RadosGWStorage,
'SwiftStorage': SwiftStorage,
} }
def get_storage_driver(storage_params): def get_storage_driver(storage_params):

183
storage/swift.py Normal file
View file

@ -0,0 +1,183 @@
""" Swift storage driver. Based on: github.com/bacongobbler/docker-registry-driver-swift/ """
from swiftclient.client import Connection, ClientException
from storage.basestorage import BaseStorage
from random import SystemRandom
import string
import logging
logger = logging.getLogger(__name__)
class SwiftStorage(BaseStorage):
def __init__(self, swift_container, storage_path, auth_url, swift_user,
swift_password, auth_version=None, os_options=None, ca_cert_path=None):
self._swift_container = swift_container
self._storage_path = storage_path
self._auth_url = auth_url
self._ca_cert_path = ca_cert_path
self._swift_user = swift_user
self._swift_password = swift_password
self._auth_version = auth_version or 2
self._os_options = os_options or {}
self._initialized = False
self._swift_connection = None
def _initialize(self):
if self._initialized:
return
self._initialized = True
self._swift_connection = self._get_connection()
def _get_connection(self):
return Connection(
authurl=self._auth_url,
cacert=self._ca_cert_path,
user=self._swift_user,
key=self._swift_password,
auth_version=self._auth_version,
os_options=self._os_options)
def _get_relative_path(self, path):
if path.startswith(self._storage_path):
path = path[len(self._storage_path)]
if path.endswith('/'):
path = path[:-1]
return path
def _normalize_path(self, path=None):
path = self._storage_path + (path or '')
# Openstack does not like paths starting with '/' and we always normalize
# to remove trailing '/'
if path.startswith('/'):
path = path[1:]
if path.endswith('/'):
path = path[:-1]
return path
def _get_container(self, path):
self._initialize()
path = self._normalize_path(path)
if path and not path.endswith('/'):
path += '/'
try:
_, container = self._swift_connection.get_container(
container=self._swift_container,
prefix=path, delimiter='/')
return container
except:
logger.exception('Could not get container: %s', path)
raise IOError('Unknown path: %s' % path)
def _get_object(self, path, chunk_size=None):
self._initialize()
path = self._normalize_path(path)
try:
_, obj = self._swift_connection.get_object(self._swift_container, path,
resp_chunk_size=chunk_size)
return obj
except Exception:
logger.exception('Could not get object: %s', path)
raise IOError('Path %s not found' % path)
def _put_object(self, path, content, chunk=None, content_type=None, content_encoding=None):
self._initialize()
path = self._normalize_path(path)
headers = {}
if content_encoding is not None:
headers['Content-Encoding'] = content_encoding
try:
self._swift_connection.put_object(self._swift_container, path, content,
chunk_size=chunk, content_type=content_type,
headers=headers)
except ClientException:
raise
except Exception:
logger.exception('Could not put object: %s', path)
raise IOError("Could not put content: %s" % path)
def _head_object(self, path):
self._initialize()
path = self._normalize_path(path)
try:
return self._swift_connection.head_object(self._swift_container, path)
except Exception:
logger.exception('Could not head object: %s', path)
return None
def get_direct_download_url(self, path, expires_in=60, requires_cors=False):
if requires_cors:
return None
# TODO: http://docs.openstack.org/juno/config-reference/content/object-storage-tempurl.html
return None
def get_content(self, path):
return self._get_object(path)
def put_content(self, path, content):
self._put_object(path, content)
def stream_read(self, path):
for data in self._get_object(path, self.buffer_size):
yield data
def stream_read_file(self, path):
raise NotImplementedError
def stream_write(self, path, fp, content_type=None, content_encoding=None):
self._put_object(path, fp, self.buffer_size, content_type=content_type,
content_encoding=content_encoding)
def list_directory(self, path=None):
container = self._get_container(path)
if not container:
raise OSError('Unknown path: %s' % path)
for entry in container:
param = None
if 'name' in entry:
param = 'name'
elif 'subdir' in entry:
param = 'subdir'
else:
continue
yield self._get_relative_path(entry[param])
def exists(self, path):
return bool(self._head_object(path))
def remove(self, path):
self._initialize()
path = self._normalize_path(path)
try:
self._swift_connection.delete_object(self._swift_container, path)
except Exception:
raise IOError('Cannot delete path: %s' % path)
def _random_checksum(self, count):
chars = string.ascii_uppercase + string.digits
return ''.join(SystemRandom().choice(chars) for _ in range(count))
def get_checksum(self, path):
headers = self._head_object(path)
if not headers:
raise IOError('Cannot lookup path: %s' % path)
return headers.get('etag', '')[1:-1][:7] or self._random_checksum(7)