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
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
===============

View File

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

View File

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

View File

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

View File

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

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-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"

View File

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

View File

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

View File

@ -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',

View File

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

View File

@ -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()

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.