diff --git a/doc/source/configuration/settings.rst b/doc/source/configuration/settings.rst index 921063034d..66fcd8fcdb 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 457084e41e..e71655ed28 100644 --- a/openstack_dashboard/api/rest/swift.py +++ b/openstack_dashboard/api/rest/swift.py @@ -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, diff --git a/openstack_dashboard/api/swift.py b/openstack_dashboard/api/swift.py index 0fad7f97c5..1d289e6d70 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 afbe1ffd6b..9c6a606152 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 = [ @@ -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 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 c8e9e73c0a..023064a73b 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 704ee0e44f..329f11419c 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 @@ Date Created {$ container.timestamp | date $} +
  • + Storage Policy + {$ container.storage_policy.display_name $} + ({$ container.storage_policy.name $}) +
  • +
  • + Storage Policy + {$ container.storage_policy.name $} +