Merge "Add support for multiple swift storage policies"

This commit is contained in:
Zuul 2020-04-17 10:06:59 +00:00 committed by Gerrit Code Review
commit b62c49acf5
12 changed files with 276 additions and 16 deletions

View File

@ -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 exact number depends on your connection speed), otherwise you may encounter
socket timeout. The default value is 524288 bytes (or 512 Kilobytes). 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 Django Settings
=============== ===============

View File

@ -40,6 +40,27 @@ class Info(generic.View):
return {'info': capabilities} 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 @urls.register
class Containers(generic.View): class Containers(generic.View):
"""API for swift container listing for an account""" """API for swift container listing for an account"""
@ -83,6 +104,9 @@ class Container(generic.View):
if 'is_public' in request.DATA: if 'is_public' in request.DATA:
metadata['is_public'] = request.DATA['is_public'] 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 # This will raise an exception if the container already exists
try: try:
api.swift.swift_create_container(request, container, api.swift.swift_create_container(request, container,

View File

@ -104,6 +104,13 @@ def _objectify(items, container_name):
return objects 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): def _metadata_to_header(metadata):
headers = {} headers = {}
public = metadata.get('is_public') public = metadata.get('is_public')
@ -114,6 +121,10 @@ def _metadata_to_header(metadata):
elif public is False: elif public is False:
headers['x-container-read'] = "" headers['x-container-read'] = ""
storage_policy = metadata.get("storage_policy")
if storage_policy:
headers["x-storage-policy"] = storage_policy
return headers return headers
@ -175,6 +186,10 @@ def swift_get_container(request, container_name, with_data=True):
timestamp = None timestamp = None
is_public = False is_public = False
public_url = None public_url = None
storage_policy = headers.get("x-storage-policy")
storage_policy_display_name = \
get_storage_policy_display_name(storage_policy)
try: try:
is_public = GLOBAL_READ_ACL in headers.get('x-container-read', '') is_public = GLOBAL_READ_ACL in headers.get('x-container-read', '')
if is_public: if is_public:
@ -194,8 +209,16 @@ def swift_get_container(request, container_name, with_data=True):
'timestamp': timestamp, 'timestamp': timestamp,
'data': data, 'data': data,
'is_public': is_public, 'is_public': is_public,
'storage_policy': {
"name": storage_policy,
},
'public_url': public_url, '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) return Container(container_info)

View File

@ -59,6 +59,9 @@
$location, $location,
$q) { $q) {
var ctrl = this; var ctrl = this;
ctrl.defaultPolicy = '';
ctrl.policies = []; // on ctrl scope to be included in tests
ctrl.policyOptions = [];
ctrl.model = containersModel; ctrl.model = containersModel;
ctrl.model.initialize(); ctrl.model.initialize();
ctrl.baseRoute = baseRoute; ctrl.baseRoute = baseRoute;
@ -85,6 +88,7 @@
ctrl.createContainer = createContainer; ctrl.createContainer = createContainer;
ctrl.createContainerAction = createContainerAction; ctrl.createContainerAction = createContainerAction;
ctrl.selectContainer = selectContainer; ctrl.selectContainer = selectContainer;
ctrl.setDefaultPolicyAndOptions = setDefaultPolicyAndOptions;
////////// //////////
function checkContainerNameConflict(containerName) { 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 = { var createContainerSchema = {
type: 'object', type: 'object',
properties: { properties: {
@ -178,6 +210,11 @@
pattern: '^[^/]+$', pattern: '^[^/]+$',
description: gettext('Container name must not contain "/".') description: gettext('Container name must not contain "/".')
}, },
policy: {
title: gettext('Storage Policy'),
type: 'string',
default: ctrl.defaultPolicy
},
public: { public: {
title: gettext('Container Access'), title: gettext('Container Access'),
type: 'boolean', type: 'boolean',
@ -186,7 +223,7 @@
'gain access to your objects in the container.') 'gain access to your objects in the container.')
} }
}, },
required: ['name'] required: ['name', 'policy']
}; };
var createContainerForm = [ var createContainerForm = [
@ -207,6 +244,11 @@
exists: checkContainerNameConflict exists: checkContainerNameConflict
} }
}, },
{
key: 'policy',
type: 'select',
titleMap: getPolicyOptions()
},
{ {
key: 'public', key: 'public',
type: 'radiobuttons', type: 'radiobuttons',
@ -232,13 +274,14 @@
size: 'md', size: 'md',
helpUrl: basePath + 'create-container.help.html' helpUrl: basePath + 'create-container.help.html'
}; };
config.schema.properties.policy.default = ctrl.defaultPolicy;
return modalFormService.open(config).then(function then() { return modalFormService.open(config).then(function then() {
return ctrl.createContainerAction(model); return ctrl.createContainerAction(model);
}); });
} }
function 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() { function success() {
toastService.add('success', interpolate( toastService.add('success', interpolate(
gettext('Container %(name)s created.'), model, true gettext('Container %(name)s created.'), model, true

View File

@ -44,16 +44,17 @@
} }
}; };
var $q, scope, $location, $rootScope, controller, var $q, scope, $location, $httpBackend, $rootScope, controller,
modalFormService, simpleModal, swiftAPI, toast; modalFormService, simpleModal, swiftAPI, toast;
beforeEach(module('horizon.dashboard.project.containers', function($provide) { beforeEach(module('horizon.dashboard.project.containers', function($provide) {
$provide.value('horizon.dashboard.project.containers.containers-model', fakeModel); $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'); controller = $injector.get('$controller');
$q = _$q_; $q = _$q_;
$httpBackend = _$httpBackend_;
$location = $injector.get('$location'); $location = $injector.get('$location');
$rootScope = _$rootScope_; $rootScope = _$rootScope_;
scope = $rootScope.$new(); scope = $rootScope.$new();
@ -105,6 +106,7 @@
ctrl.toggleAccess(container); ctrl.toggleAccess(container);
expect(swiftAPI.setContainerAccess).toHaveBeenCalledWith('spam', true); expect(swiftAPI.setContainerAccess).toHaveBeenCalledWith('spam', true);
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
deferred.resolve(); deferred.resolve();
$rootScope.$apply(); $rootScope.$apply();
@ -121,6 +123,7 @@
ctrl.toggleAccess(container); ctrl.toggleAccess(container);
expect(swiftAPI.setContainerAccess).toHaveBeenCalledWith('spam', false); expect(swiftAPI.setContainerAccess).toHaveBeenCalledWith('spam', false);
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
deferred.resolve(); deferred.resolve();
$rootScope.$apply(); $rootScope.$apply();
@ -146,6 +149,7 @@
expect(spec.body).toBeDefined(); expect(spec.body).toBeDefined();
expect(spec.submit).toBeDefined(); expect(spec.submit).toBeDefined();
expect(spec.cancel).toBeDefined(); expect(spec.cancel).toBeDefined();
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
// when the modal is resolved, make sure delete is called // when the modal is resolved, make sure delete is called
deferred.resolve(); deferred.resolve();
@ -158,10 +162,12 @@
var deferred = $q.defer(); var deferred = $q.defer();
spyOn(swiftAPI, 'deleteContainer').and.returnValue(deferred.promise); spyOn(swiftAPI, 'deleteContainer').and.returnValue(deferred.promise);
spyOn($location, 'path'); spyOn($location, 'path');
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
var ctrl = createController(); var ctrl = createController();
ctrl.model.container = {name: 'one'}; ctrl.model.container = {name: 'one'};
createController().deleteContainerAction(fakeModel.containers[1]); createController().deleteContainerAction(fakeModel.containers[1]);
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
deferred.resolve(); deferred.resolve();
$rootScope.$apply(); $rootScope.$apply();
@ -183,6 +189,8 @@
ctrl.model.container = {name: 'two'}; ctrl.model.container = {name: 'two'};
ctrl.deleteContainerAction(fakeModel.containers[1]); ctrl.deleteContainerAction(fakeModel.containers[1]);
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
deferred.resolve(); deferred.resolve();
$rootScope.$apply(); $rootScope.$apply();
expect($location.path).toHaveBeenCalledWith('base ham'); expect($location.path).toHaveBeenCalledWith('base ham');
@ -203,12 +211,49 @@
expect(config.schema).toBeDefined(); expect(config.schema).toBeDefined();
expect(config.form).toBeDefined(); expect(config.form).toBeDefined();
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
// when the modal is resolved, make sure create is called // when the modal is resolved, make sure create is called
deferred.resolve(); deferred.resolve();
$rootScope.$apply(); $rootScope.$apply();
expect(ctrl.createContainerAction).toHaveBeenCalledWith({public: false}); 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() { it('should check for container existence - with presence', function test() {
var deferred = $q.defer(); var deferred = $q.defer();
spyOn(swiftAPI, 'getContainer').and.returnValue(deferred.promise); spyOn(swiftAPI, 'getContainer').and.returnValue(deferred.promise);
@ -221,6 +266,7 @@
d.then(function result() { resolved = true; }, function () { rejected = true; }); d.then(function result() { resolved = true; }, function () { rejected = true; });
expect(swiftAPI.getContainer).toHaveBeenCalledWith('spam', true); expect(swiftAPI.getContainer).toHaveBeenCalledWith('spam', true);
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
// we found something // we found something
deferred.resolve(); deferred.resolve();
@ -240,6 +286,7 @@
d.then(function result() { resolved = true; }, function () { rejected = true; }); d.then(function result() { resolved = true; }, function () { rejected = true; });
expect(swiftAPI.getContainer).toHaveBeenCalledWith('spam', true); expect(swiftAPI.getContainer).toHaveBeenCalledWith('spam', true);
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
// we did not find something // we did not find something
deferred.reject(); deferred.reject();
@ -261,6 +308,7 @@
spyOn(swiftAPI, 'createContainer').and.returnValue(deferred.promise); spyOn(swiftAPI, 'createContainer').and.returnValue(deferred.promise);
createController().createContainerAction({name: 'spam', public: true}); createController().createContainerAction({name: 'spam', public: true});
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
deferred.resolve(); deferred.resolve();
$rootScope.$apply(); $rootScope.$apply();
@ -271,6 +319,7 @@
it('should call getContainers when filters change', function test() { it('should call getContainers when filters change', function test() {
spyOn(fakeModel, 'getContainers').and.callThrough(); spyOn(fakeModel, 'getContainers').and.callThrough();
$httpBackend.expectGET('/dashboard/api/swift/policies/').respond({});
var ctrl = createController(); var ctrl = createController();
ctrl.filterEventTrigeredBySearchBar = true; ctrl.filterEventTrigeredBySearchBar = true;
scope.cc = ctrl; scope.cc = ctrl;

View File

@ -55,6 +55,15 @@
<span class="hz-object-label col-lg-7 col-md-12" translate>Date Created</span> <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> <span class="hz-object-val col-lg-5 col-md-12">{$ container.timestamp | date $}</span>
</li> </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"> <li class="hz-object-link row">
<div class="themable-checkbox col-lg-7 col-md-12"> <div class="themable-checkbox col-lg-7 col-md-12">
<input type="checkbox" id="id_access" ng-model="container.is_public" <input type="checkbox" id="id_access" ng-model="container.is_public"

View File

@ -322,6 +322,10 @@ SHOW_OPENSTACK_CLOUDS_YAML = True
# The size of chunk in bytes for downloading objects from Swift # The size of chunk in bytes for downloading objects from Swift
SWIFT_FILE_TRANSFER_CHUNK_SIZE = 512 * 1024 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 # NOTE: The default value of USER_MENU_LINKS will be set after loading
# local_settings if it is not configured. # local_settings if it is not configured.
USER_MENU_LINKS = None USER_MENU_LINKS = None

View File

@ -47,6 +47,7 @@
getObjectDetails:getObjectDetails, getObjectDetails:getObjectDetails,
getObjects: getObjects, getObjects: getObjects,
getObjectURL: getObjectURL, getObjectURL: getObjectURL,
getPolicyDetails: getPolicyDetails,
setContainerAccess: setContainerAccess, setContainerAccess: setContainerAccess,
uploadObject: uploadObject 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 * @name getContainers
* @description * @description
@ -135,8 +153,8 @@
* @returns {Object} The result of the creation call * @returns {Object} The result of the creation call
* *
*/ */
function createContainer(container, isPublic) { function createContainer(container, isPublic, policy) {
var data = {is_public: false}; var data = {is_public: false, storage_policy: policy};
if (isPublic) { if (isPublic) {
data.is_public = true; data.is_public = true;

View File

@ -61,28 +61,34 @@
testInput: [ 'spam' ] testInput: [ 'spam' ]
}, },
{ {
func: 'createContainer', func: 'getPolicyDetails',
method: 'post', method: 'get',
path: '/api/swift/containers/new-spam/metadata/', path: '/api/swift/policies/',
data: {is_public: false}, error: 'Unable to fetch the policy details.'
error: 'Unable to create the container.',
testInput: [ 'new-spam' ]
}, },
{ {
func: 'createContainer', func: 'createContainer',
method: 'post', method: 'post',
path: '/api/swift/containers/new-spam/metadata/', 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.', error: 'Unable to create the container.',
testInput: [ 'new-spam', false ] testInput: [ 'new-spam', false, 'nz--o1--mr-r3' ]
}, },
{ {
func: 'createContainer', func: 'createContainer',
method: 'post', method: 'post',
path: '/api/swift/containers/new-spam/metadata/', 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.', 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', func: 'deleteContainer',

View File

@ -34,6 +34,43 @@ class SwiftRestTestCase(test.TestCase):
response.json) response.json)
self.mock_swift_get_capabilities.assert_called_once_with(request) 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 # Containers
# #

View File

@ -95,6 +95,34 @@ class SwiftApiTests(test.APIMockTestCase):
swift_api.head_container.assert_called_once_with(container.name) 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): def test_swift_update_container(self, mock_swiftclient):
metadata = {'is_public': True} metadata = {'is_public': True}
container = self.containers.first() container = self.containers.first()

View File

@ -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.