Merge "Add support for multiple swift storage policies"
This commit is contained in:
commit
b62c49acf5
@ -2339,6 +2339,19 @@ from Swift. Do not make it very large (higher than several dozens of Megabytes,
|
||||
exact number depends on your connection speed), otherwise you may encounter
|
||||
socket timeout. The default value is 524288 bytes (or 512 Kilobytes).
|
||||
|
||||
|
||||
SWIFT_STORAGE_POLICY_DISPLAY_NAMES
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 18.3.0(Ussuri)
|
||||
|
||||
Default: ``{}``
|
||||
|
||||
A dictionary mapping from the swift storage policy name to an alternate,
|
||||
user friendly display name which will be rendered on the dashboard. If
|
||||
no display is specified for a storage policy, the storage
|
||||
policy name will be used verbatim.
|
||||
|
||||
Django Settings
|
||||
===============
|
||||
|
||||
|
@ -40,6 +40,27 @@ class Info(generic.View):
|
||||
return {'info': capabilities}
|
||||
|
||||
|
||||
@urls.register
|
||||
class Policies(generic.View):
|
||||
"""API for information about available container storage policies"""
|
||||
url_regex = r'swift/policies/$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request):
|
||||
"""List available container storage policies"""
|
||||
|
||||
capabilities = api.swift.swift_get_capabilities(request)
|
||||
policies = capabilities['swift']['policies']
|
||||
|
||||
for policy in policies:
|
||||
display_name = \
|
||||
api.swift.get_storage_policy_display_name(policy['name'])
|
||||
if display_name:
|
||||
policy["display_name"] = display_name
|
||||
|
||||
return {'policies': policies}
|
||||
|
||||
|
||||
@urls.register
|
||||
class Containers(generic.View):
|
||||
"""API for swift container listing for an account"""
|
||||
@ -83,6 +104,9 @@ class Container(generic.View):
|
||||
if 'is_public' in request.DATA:
|
||||
metadata['is_public'] = request.DATA['is_public']
|
||||
|
||||
if 'storage_policy' in request.DATA:
|
||||
metadata['storage_policy'] = request.DATA['storage_policy']
|
||||
|
||||
# This will raise an exception if the container already exists
|
||||
try:
|
||||
api.swift.swift_create_container(request, container,
|
||||
|
@ -104,6 +104,13 @@ def _objectify(items, container_name):
|
||||
return objects
|
||||
|
||||
|
||||
def get_storage_policy_display_name(name):
|
||||
"""Gets the user friendly display name for a storage policy"""
|
||||
|
||||
display_names = settings.SWIFT_STORAGE_POLICY_DISPLAY_NAMES
|
||||
return display_names.get(name)
|
||||
|
||||
|
||||
def _metadata_to_header(metadata):
|
||||
headers = {}
|
||||
public = metadata.get('is_public')
|
||||
@ -114,6 +121,10 @@ def _metadata_to_header(metadata):
|
||||
elif public is False:
|
||||
headers['x-container-read'] = ""
|
||||
|
||||
storage_policy = metadata.get("storage_policy")
|
||||
if storage_policy:
|
||||
headers["x-storage-policy"] = storage_policy
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
@ -175,6 +186,10 @@ def swift_get_container(request, container_name, with_data=True):
|
||||
timestamp = None
|
||||
is_public = False
|
||||
public_url = None
|
||||
storage_policy = headers.get("x-storage-policy")
|
||||
storage_policy_display_name = \
|
||||
get_storage_policy_display_name(storage_policy)
|
||||
|
||||
try:
|
||||
is_public = GLOBAL_READ_ACL in headers.get('x-container-read', '')
|
||||
if is_public:
|
||||
@ -194,8 +209,16 @@ def swift_get_container(request, container_name, with_data=True):
|
||||
'timestamp': timestamp,
|
||||
'data': data,
|
||||
'is_public': is_public,
|
||||
'storage_policy': {
|
||||
"name": storage_policy,
|
||||
},
|
||||
'public_url': public_url,
|
||||
}
|
||||
|
||||
if storage_policy_display_name:
|
||||
container_info['storage_policy']['display_name'] = \
|
||||
get_storage_policy_display_name(storage_policy)
|
||||
|
||||
return Container(container_info)
|
||||
|
||||
|
||||
|
@ -59,6 +59,9 @@
|
||||
$location,
|
||||
$q) {
|
||||
var ctrl = this;
|
||||
ctrl.defaultPolicy = '';
|
||||
ctrl.policies = []; // on ctrl scope to be included in tests
|
||||
ctrl.policyOptions = [];
|
||||
ctrl.model = containersModel;
|
||||
ctrl.model.initialize();
|
||||
ctrl.baseRoute = baseRoute;
|
||||
@ -85,6 +88,7 @@
|
||||
ctrl.createContainer = createContainer;
|
||||
ctrl.createContainerAction = createContainerAction;
|
||||
ctrl.selectContainer = selectContainer;
|
||||
ctrl.setDefaultPolicyAndOptions = setDefaultPolicyAndOptions;
|
||||
|
||||
//////////
|
||||
function checkContainerNameConflict(containerName) {
|
||||
@ -169,6 +173,34 @@
|
||||
});
|
||||
}
|
||||
|
||||
function getPolicyOptions() {
|
||||
// get the details for available storage policies
|
||||
swiftAPI.getPolicyDetails().then(setDefaultPolicyAndOptions);
|
||||
return ctrl.policyOptions;
|
||||
}
|
||||
|
||||
function setDefaultPolicyAndOptions(data) {
|
||||
ctrl.policies = data.data.policies;
|
||||
ctrl.defaultPolicy = ctrl.policies[0].name; // set the first option as default policy
|
||||
angular.forEach(ctrl.policies, function(policy) {
|
||||
// set the correct default policy as per the API data
|
||||
if (policy.default) {
|
||||
ctrl.defaultPolicy = policy.name;
|
||||
}
|
||||
|
||||
var displayName = policy.name;
|
||||
|
||||
if (policy.display_name) {
|
||||
displayName = policy.display_name + ' (' + policy.name + ')';
|
||||
}
|
||||
|
||||
ctrl.policyOptions.push({
|
||||
value: policy.name,
|
||||
name: displayName
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var createContainerSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@ -178,6 +210,11 @@
|
||||
pattern: '^[^/]+$',
|
||||
description: gettext('Container name must not contain "/".')
|
||||
},
|
||||
policy: {
|
||||
title: gettext('Storage Policy'),
|
||||
type: 'string',
|
||||
default: ctrl.defaultPolicy
|
||||
},
|
||||
public: {
|
||||
title: gettext('Container Access'),
|
||||
type: 'boolean',
|
||||
@ -186,7 +223,7 @@
|
||||
'gain access to your objects in the container.')
|
||||
}
|
||||
},
|
||||
required: ['name']
|
||||
required: ['name', 'policy']
|
||||
};
|
||||
|
||||
var createContainerForm = [
|
||||
@ -207,6 +244,11 @@
|
||||
exists: checkContainerNameConflict
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'policy',
|
||||
type: 'select',
|
||||
titleMap: getPolicyOptions()
|
||||
},
|
||||
{
|
||||
key: 'public',
|
||||
type: 'radiobuttons',
|
||||
@ -232,13 +274,14 @@
|
||||
size: 'md',
|
||||
helpUrl: basePath + 'create-container.help.html'
|
||||
};
|
||||
config.schema.properties.policy.default = ctrl.defaultPolicy;
|
||||
return modalFormService.open(config).then(function then() {
|
||||
return ctrl.createContainerAction(model);
|
||||
});
|
||||
}
|
||||
|
||||
function createContainerAction(model) {
|
||||
return swiftAPI.createContainer(model.name, model.public).then(
|
||||
return swiftAPI.createContainer(model.name, model.public, model.policy).then(
|
||||
function success() {
|
||||
toastService.add('success', interpolate(
|
||||
gettext('Container %(name)s created.'), model, true
|
||||
|
@ -44,16 +44,17 @@
|
||||
}
|
||||
};
|
||||
|
||||
var $q, scope, $location, $rootScope, controller,
|
||||
var $q, scope, $location, $httpBackend, $rootScope, controller,
|
||||
modalFormService, simpleModal, swiftAPI, toast;
|
||||
|
||||
beforeEach(module('horizon.dashboard.project.containers', function($provide) {
|
||||
$provide.value('horizon.dashboard.project.containers.containers-model', fakeModel);
|
||||
}));
|
||||
|
||||
beforeEach(inject(function ($injector, _$q_, _$rootScope_) {
|
||||
beforeEach(inject(function ($injector, _$httpBackend_, _$q_, _$rootScope_) {
|
||||
controller = $injector.get('$controller');
|
||||
$q = _$q_;
|
||||
$httpBackend = _$httpBackend_;
|
||||
$location = $injector.get('$location');
|
||||
$rootScope = _$rootScope_;
|
||||
scope = $rootScope.$new();
|
||||
@ -105,6 +106,7 @@
|
||||
ctrl.toggleAccess(container);
|
||||
|
||||
expect(swiftAPI.setContainerAccess).toHaveBeenCalledWith('spam', true);
|
||||
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
|
||||
|
||||
deferred.resolve();
|
||||
$rootScope.$apply();
|
||||
@ -121,6 +123,7 @@
|
||||
ctrl.toggleAccess(container);
|
||||
|
||||
expect(swiftAPI.setContainerAccess).toHaveBeenCalledWith('spam', false);
|
||||
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
|
||||
|
||||
deferred.resolve();
|
||||
$rootScope.$apply();
|
||||
@ -146,6 +149,7 @@
|
||||
expect(spec.body).toBeDefined();
|
||||
expect(spec.submit).toBeDefined();
|
||||
expect(spec.cancel).toBeDefined();
|
||||
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
|
||||
|
||||
// when the modal is resolved, make sure delete is called
|
||||
deferred.resolve();
|
||||
@ -158,10 +162,12 @@
|
||||
var deferred = $q.defer();
|
||||
spyOn(swiftAPI, 'deleteContainer').and.returnValue(deferred.promise);
|
||||
spyOn($location, 'path');
|
||||
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
|
||||
|
||||
var ctrl = createController();
|
||||
ctrl.model.container = {name: 'one'};
|
||||
createController().deleteContainerAction(fakeModel.containers[1]);
|
||||
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
|
||||
|
||||
deferred.resolve();
|
||||
$rootScope.$apply();
|
||||
@ -183,6 +189,8 @@
|
||||
ctrl.model.container = {name: 'two'};
|
||||
ctrl.deleteContainerAction(fakeModel.containers[1]);
|
||||
|
||||
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
|
||||
|
||||
deferred.resolve();
|
||||
$rootScope.$apply();
|
||||
expect($location.path).toHaveBeenCalledWith('base ham');
|
||||
@ -203,12 +211,49 @@
|
||||
expect(config.schema).toBeDefined();
|
||||
expect(config.form).toBeDefined();
|
||||
|
||||
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
|
||||
|
||||
// when the modal is resolved, make sure create is called
|
||||
deferred.resolve();
|
||||
$rootScope.$apply();
|
||||
expect(ctrl.createContainerAction).toHaveBeenCalledWith({public: false});
|
||||
});
|
||||
|
||||
it('should preselect default policy in create container dialog', function test() {
|
||||
var deferred = $q.defer();
|
||||
spyOn(modalFormService, 'open').and.returnValue(deferred.promise);
|
||||
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond();
|
||||
|
||||
var ctrl = createController();
|
||||
var policyOptions = {
|
||||
data: {
|
||||
policies: [
|
||||
{
|
||||
name: 'nz--o1--mr-r3'
|
||||
},
|
||||
{
|
||||
display_name: 'Single Region nz-por-1',
|
||||
default: true,
|
||||
name: 'nz-por-1--o1--sr-r3'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
ctrl.setDefaultPolicyAndOptions(policyOptions);
|
||||
|
||||
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond();
|
||||
|
||||
ctrl.createContainer();
|
||||
expect(modalFormService.open).toHaveBeenCalled();
|
||||
var config = modalFormService.open.calls.mostRecent().args[0];
|
||||
expect(config.schema.properties.policy.default).toBe(
|
||||
'nz-por-1--o1--sr-r3'
|
||||
);
|
||||
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
it('should check for container existence - with presence', function test() {
|
||||
var deferred = $q.defer();
|
||||
spyOn(swiftAPI, 'getContainer').and.returnValue(deferred.promise);
|
||||
@ -221,6 +266,7 @@
|
||||
d.then(function result() { resolved = true; }, function () { rejected = true; });
|
||||
|
||||
expect(swiftAPI.getContainer).toHaveBeenCalledWith('spam', true);
|
||||
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
|
||||
|
||||
// we found something
|
||||
deferred.resolve();
|
||||
@ -240,6 +286,7 @@
|
||||
d.then(function result() { resolved = true; }, function () { rejected = true; });
|
||||
|
||||
expect(swiftAPI.getContainer).toHaveBeenCalledWith('spam', true);
|
||||
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
|
||||
|
||||
// we did not find something
|
||||
deferred.reject();
|
||||
@ -261,6 +308,7 @@
|
||||
spyOn(swiftAPI, 'createContainer').and.returnValue(deferred.promise);
|
||||
|
||||
createController().createContainerAction({name: 'spam', public: true});
|
||||
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
|
||||
|
||||
deferred.resolve();
|
||||
$rootScope.$apply();
|
||||
@ -271,6 +319,7 @@
|
||||
|
||||
it('should call getContainers when filters change', function test() {
|
||||
spyOn(fakeModel, 'getContainers').and.callThrough();
|
||||
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
|
||||
var ctrl = createController();
|
||||
ctrl.filterEventTrigeredBySearchBar = true;
|
||||
scope.cc = ctrl;
|
||||
|
@ -55,6 +55,15 @@
|
||||
<span class="hz-object-label col-lg-7 col-md-12" translate>Date Created</span>
|
||||
<span class="hz-object-val col-lg-5 col-md-12">{$ container.timestamp | date $}</span>
|
||||
</li>
|
||||
<li ng-if="container.storage_policy.display_name" class="hz-object-policy row">
|
||||
<span class="hz-object-label col-lg-7 col-md-12" translate>Storage Policy</span>
|
||||
<span class="hz-object-val col-lg-5 col-md-12">{$ container.storage_policy.display_name $}</span>
|
||||
<span class="hz-object-val col-lg-offset-7 col-lg-5 col-md-12">({$ container.storage_policy.name $})</span>
|
||||
</li>
|
||||
<li ng-if="!container.storage_policy.display_name" class="hz-object-policy row">
|
||||
<span class="hz-object-label col-lg-7 col-md-12" translate>Storage Policy</span>
|
||||
<span class="hz-object-val col-lg-5 col-md-12">{$ container.storage_policy.name $}</span>
|
||||
</li>
|
||||
<li class="hz-object-link row">
|
||||
<div class="themable-checkbox col-lg-7 col-md-12">
|
||||
<input type="checkbox" id="id_access" ng-model="container.is_public"
|
||||
|
@ -322,6 +322,10 @@ SHOW_OPENSTACK_CLOUDS_YAML = True
|
||||
# The size of chunk in bytes for downloading objects from Swift
|
||||
SWIFT_FILE_TRANSFER_CHUNK_SIZE = 512 * 1024
|
||||
|
||||
# Mapping from actual storage policy name to user friendly
|
||||
# name to be rendered.
|
||||
SWIFT_STORAGE_POLICY_DISPLAY_NAMES = {}
|
||||
|
||||
# NOTE: The default value of USER_MENU_LINKS will be set after loading
|
||||
# local_settings if it is not configured.
|
||||
USER_MENU_LINKS = None
|
||||
|
@ -47,6 +47,7 @@
|
||||
getObjectDetails:getObjectDetails,
|
||||
getObjects: getObjects,
|
||||
getObjectURL: getObjectURL,
|
||||
getPolicyDetails: getPolicyDetails,
|
||||
setContainerAccess: setContainerAccess,
|
||||
uploadObject: uploadObject
|
||||
};
|
||||
@ -89,6 +90,23 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @name getPolicyDetails
|
||||
* @description
|
||||
* Fetch all the storage policy details with display names and storage values.
|
||||
*
|
||||
* @returns {Object} The result of the object passed to the Swift /policies call.
|
||||
*
|
||||
*/
|
||||
function getPolicyDetails() {
|
||||
return apiService.get('/api/swift/policies/').error(function() {
|
||||
toastService.add(
|
||||
'error',
|
||||
gettext('Unable to fetch the policy details.')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @name getContainers
|
||||
* @description
|
||||
@ -135,8 +153,8 @@
|
||||
* @returns {Object} The result of the creation call
|
||||
*
|
||||
*/
|
||||
function createContainer(container, isPublic) {
|
||||
var data = {is_public: false};
|
||||
function createContainer(container, isPublic, policy) {
|
||||
var data = {is_public: false, storage_policy: policy};
|
||||
|
||||
if (isPublic) {
|
||||
data.is_public = true;
|
||||
|
@ -61,28 +61,34 @@
|
||||
testInput: [ 'spam' ]
|
||||
},
|
||||
{
|
||||
func: 'createContainer',
|
||||
method: 'post',
|
||||
path: '/api/swift/containers/new-spam/metadata/',
|
||||
data: {is_public: false},
|
||||
error: 'Unable to create the container.',
|
||||
testInput: [ 'new-spam' ]
|
||||
func: 'getPolicyDetails',
|
||||
method: 'get',
|
||||
path: '/api/swift/policies/',
|
||||
error: 'Unable to fetch the policy details.'
|
||||
},
|
||||
{
|
||||
func: 'createContainer',
|
||||
method: 'post',
|
||||
path: '/api/swift/containers/new-spam/metadata/',
|
||||
data: {is_public: false},
|
||||
data: {is_public: false, storage_policy: 'nz--o1--mr-r3'},
|
||||
error: 'Unable to create the container.',
|
||||
testInput: [ 'new-spam', false ]
|
||||
testInput: [ 'new-spam', false, 'nz--o1--mr-r3' ]
|
||||
},
|
||||
{
|
||||
func: 'createContainer',
|
||||
method: 'post',
|
||||
path: '/api/swift/containers/new-spam/metadata/',
|
||||
data: {is_public: true},
|
||||
data: {is_public: false, storage_policy: 'nz--o1--mr-r3'},
|
||||
error: 'Unable to create the container.',
|
||||
testInput: [ 'new-spam', true ]
|
||||
testInput: [ 'new-spam', false, 'nz--o1--mr-r3' ]
|
||||
},
|
||||
{
|
||||
func: 'createContainer',
|
||||
method: 'post',
|
||||
path: '/api/swift/containers/new-spam/metadata/',
|
||||
data: {is_public: true, storage_policy: 'nz--o1--mr-r3'},
|
||||
error: 'Unable to create the container.',
|
||||
testInput: [ 'new-spam', true, 'nz--o1--mr-r3' ]
|
||||
},
|
||||
{
|
||||
func: 'deleteContainer',
|
||||
|
@ -34,6 +34,43 @@ class SwiftRestTestCase(test.TestCase):
|
||||
response.json)
|
||||
self.mock_swift_get_capabilities.assert_called_once_with(request)
|
||||
|
||||
@test.create_mocks({api.swift: ['swift_get_capabilities',
|
||||
'get_storage_policy_display_name']})
|
||||
def test_policies_get(self):
|
||||
request = self.mock_rest_request()
|
||||
self.mock_get_storage_policy_display_name.side_effect = [
|
||||
"Multi Region", None]
|
||||
self.mock_swift_get_capabilities.return_value = {
|
||||
'swift': {
|
||||
'policies': [
|
||||
{
|
||||
"aliases": "nz--o1--mr-r3",
|
||||
"default": True,
|
||||
"name": "nz--o1--mr-r3"
|
||||
}, {
|
||||
"aliases": "another-policy",
|
||||
"name": "some-other-policy"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
response = swift.Policies().get(request)
|
||||
self.assertStatusCode(response, 200)
|
||||
self.assertEqual({
|
||||
'policies': [
|
||||
{
|
||||
"aliases": "nz--o1--mr-r3",
|
||||
"default": True,
|
||||
"name": "nz--o1--mr-r3",
|
||||
"display_name": "Multi Region"
|
||||
},
|
||||
{
|
||||
"aliases": "another-policy",
|
||||
"name": "some-other-policy"
|
||||
}
|
||||
]}, response.json)
|
||||
self.mock_swift_get_capabilities.assert_called_once_with(request)
|
||||
|
||||
#
|
||||
# Containers
|
||||
#
|
||||
|
@ -95,6 +95,34 @@ class SwiftApiTests(test.APIMockTestCase):
|
||||
|
||||
swift_api.head_container.assert_called_once_with(container.name)
|
||||
|
||||
def test_swift_create_container_with_storage_policy(self, mock_swiftclient):
|
||||
metadata = {'is_public': True, 'storage_policy': 'nz-o1-mr-r3'}
|
||||
container = self.containers.first()
|
||||
swift_api = mock_swiftclient.return_value
|
||||
swift_api.head_container.return_value = container
|
||||
|
||||
headers = api.swift._metadata_to_header(metadata=(metadata))
|
||||
self.assertEqual(headers["x-storage-policy"], 'nz-o1-mr-r3')
|
||||
|
||||
with self.assertRaises(exceptions.AlreadyExists):
|
||||
api.swift.swift_create_container(self.request,
|
||||
container.name,
|
||||
metadata=(metadata))
|
||||
|
||||
swift_api.head_container.assert_called_once_with(container.name)
|
||||
|
||||
def test_metadata_to_headers(self, mock_swiftclient):
|
||||
metadata = {'is_public': True, 'storage_policy': 'nz-o1-mr-r3'}
|
||||
headers = api.swift._metadata_to_header(metadata=(metadata))
|
||||
self.assertEqual(headers["x-storage-policy"], 'nz-o1-mr-r3')
|
||||
self.assertEqual(headers["x-container-read"], '.r:*,.rlistings')
|
||||
|
||||
def test_metadata_to_headers_without_metadata(self, mock_swiftclient):
|
||||
metadata = {}
|
||||
headers = api.swift._metadata_to_header(metadata=(metadata))
|
||||
self.assertNotIn("x-storage-policy", headers)
|
||||
self.assertNotIn("x-container-read", headers)
|
||||
|
||||
def test_swift_update_container(self, mock_swiftclient):
|
||||
metadata = {'is_public': True}
|
||||
container = self.containers.first()
|
||||
|
@ -0,0 +1,6 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds options to gui to allow user to select which storage policy container
|
||||
will use and displays the container's storage policy in the container
|
||||
information.
|
Loading…
Reference in New Issue
Block a user