summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/source/configuration/settings.rst13
-rw-r--r--openstack_dashboard/api/rest/swift.py24
-rw-r--r--openstack_dashboard/api/swift.py23
-rw-r--r--openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.js47
-rw-r--r--openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.spec.js53
-rw-r--r--openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.html9
-rw-r--r--openstack_dashboard/defaults.py4
-rw-r--r--openstack_dashboard/static/app/core/openstack-service-api/swift.service.js22
-rw-r--r--openstack_dashboard/static/app/core/openstack-service-api/swift.service.spec.js18
-rw-r--r--openstack_dashboard/test/unit/api/rest/test_swift.py37
-rw-r--r--openstack_dashboard/test/unit/api/test_swift.py28
-rw-r--r--releasenotes/notes/swift-storage-policy-options-308087b16156a2cb.yaml6
12 files changed, 272 insertions, 12 deletions
diff --git a/doc/source/configuration/settings.rst b/doc/source/configuration/settings.rst
index 921063034..66fcd8fcd 100644
--- a/doc/source/configuration/settings.rst
+++ b/doc/source/configuration/settings.rst
@@ -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
===============
diff --git a/openstack_dashboard/api/rest/swift.py b/openstack_dashboard/api/rest/swift.py
index 457084e41..e71655ed2 100644
--- a/openstack_dashboard/api/rest/swift.py
+++ b/openstack_dashboard/api/rest/swift.py
@@ -41,6 +41,27 @@ class Info(generic.View):
@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"""
url_regex = r'swift/containers/$'
@@ -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,
diff --git a/openstack_dashboard/api/swift.py b/openstack_dashboard/api/swift.py
index 0fad7f97c..1d289e6d7 100644
--- a/openstack_dashboard/api/swift.py
+++ b/openstack_dashboard/api/swift.py
@@ -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)
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.js b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.js
index afbe1ffd6..9c6a60615 100644
--- a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.js
+++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.js
@@ -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 = [
@@ -208,6 +245,11 @@
}
},
{
+ key: 'policy',
+ type: 'select',
+ titleMap: getPolicyOptions()
+ },
+ {
key: 'public',
type: 'radiobuttons',
disableSuccessState: true,
@@ -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
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.spec.js b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.spec.js
index c8e9e73c0..023064a73 100644
--- a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.spec.js
+++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.controller.spec.js
@@ -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;
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.html b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.html
index 704ee0e44..329f11419 100644
--- a/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.html
+++ b/openstack_dashboard/dashboards/project/static/dashboard/project/containers/containers.html
@@ -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"
diff --git a/openstack_dashboard/defaults.py b/openstack_dashboard/defaults.py
index c81c3ab80..0d1ddf3b6 100644
--- a/openstack_dashboard/defaults.py
+++ b/openstack_dashboard/defaults.py
@@ -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
diff --git a/openstack_dashboard/static/app/core/openstack-service-api/swift.service.js b/openstack_dashboard/static/app/core/openstack-service-api/swift.service.js
index 221d3c606..458bff487 100644
--- a/openstack_dashboard/static/app/core/openstack-service-api/swift.service.js
+++ b/openstack_dashboard/static/app/core/openstack-service-api/swift.service.js
@@ -47,6 +47,7 @@
getObjectDetails:getObjectDetails,
getObjects: getObjects,
getObjectURL: getObjectURL,
+ getPolicyDetails: getPolicyDetails,
setContainerAccess: setContainerAccess,
uploadObject: uploadObject
};
@@ -90,6 +91,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
* Get the list of containers for this account
@@ -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;
diff --git a/openstack_dashboard/static/app/core/openstack-service-api/swift.service.spec.js b/openstack_dashboard/static/app/core/openstack-service-api/swift.service.spec.js
index e9500bc44..7649a7217 100644
--- a/openstack_dashboard/static/app/core/openstack-service-api/swift.service.spec.js
+++ b/openstack_dashboard/static/app/core/openstack-service-api/swift.service.spec.js
@@ -61,28 +61,34 @@
testInput: [ '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' ]
+ testInput: [ 'new-spam', false, 'nz--o1--mr-r3' ]
},
{
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: true, storage_policy: 'nz--o1--mr-r3'},
error: 'Unable to create the container.',
- testInput: [ 'new-spam', true ]
+ testInput: [ 'new-spam', true, 'nz--o1--mr-r3' ]
},
{
func: 'deleteContainer',
diff --git a/openstack_dashboard/test/unit/api/rest/test_swift.py b/openstack_dashboard/test/unit/api/rest/test_swift.py
index f6ac16f8e..3ac4e6027 100644
--- a/openstack_dashboard/test/unit/api/rest/test_swift.py
+++ b/openstack_dashboard/test/unit/api/rest/test_swift.py
@@ -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
#
diff --git a/openstack_dashboard/test/unit/api/test_swift.py b/openstack_dashboard/test/unit/api/test_swift.py
index 3e47643a2..a91e0249a 100644
--- a/openstack_dashboard/test/unit/api/test_swift.py
+++ b/openstack_dashboard/test/unit/api/test_swift.py
@@ -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()
diff --git a/releasenotes/notes/swift-storage-policy-options-308087b16156a2cb.yaml b/releasenotes/notes/swift-storage-policy-options-308087b16156a2cb.yaml
new file mode 100644
index 000000000..0562f3e36
--- /dev/null
+++ b/releasenotes/notes/swift-storage-policy-options-308087b16156a2cb.yaml
@@ -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.