[Launch Instance Fix] Settings for volume name

This patch provides the rest API needed to
get settings that allow conditionally
displaying the volume device name
and admin password.

Examples:

settingsService.ifEnabled('setting').then(doThis);
settingsService.ifEnabled('setting', 'expected', 'default').then(doThis, elseThis);

Change-Id: I8d16f4b974786157c5aa562e2675e6e60aabc412
Partial-Bug: #1439906
Partial-Bug: #1439905
This commit is contained in:
Travis Tripp 2015-04-02 23:49:29 -06:00
parent d7abcbfeec
commit 3a59bec6a7
7 changed files with 572 additions and 14 deletions

View File

@ -1,18 +1,19 @@
/*
Copyright 2015, Rackspace, US, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
* Copyright 2015 IBM Corp
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
'use strict';
@ -61,10 +62,252 @@ limitations under the License.
horizon.alert('error', gettext('Unable to retrieve admin configuration.'));
});
};
}
// Register it with the API module so that anybody using the
// API module will have access to the Config APIs.
angular.module('hz.api')
.service('configAPI', ['apiService', ConfigAPI]);
/**
* @ngdoc service
* @name hz.api.settingsService
* @description
* Provides utilities to the cached settings data. This helps
* with asynchronous data loading.
*
* The cache in current horizon (Kilo non-single page app) only has a
* lifetime of the current page. The cache is reloaded every time you change
* panels. It also happens when you change the region selector at the top
* of the page, and when you log back in.
*
* So, at least for now, this seems to be a reliable way that will
* make only a single request to get user information for a
* particular page or modal. Making this a service allows it to be injected
* and used transparently where needed without making every single use of it
* pass it through as an argument.
*/
function settingsService($q, apiService) {
var service = {};
/**
* @name hz.api.configAPI.getSettings
* @description
* Gets all the allowed settings
*
* Returns an object with settings.
*/
service.getSettings = function (suppressError) {
function onError() {
var message = gettext('Unable to retrieve settings.');
if (!suppressError && horizon.alert) {
horizon.alert('error', message);
}
return message;
}
// The below ensures that errors are handled like other
// service errors (for better or worse), but when successful
// unwraps the success result data for direct consumption.
return apiService.get('/api/settings/', {cache: true})
.error(onError)
.then(function (response) {
return response.data;
});
};
/**
* @name hz.api.settingsService.getSetting
* @description
* This retrieves a specific setting.
*
* If the setting isn't found, it will return undefined unless a default
* is specified. In that case, the default will be returned.
*
* @param {string} setting The path to the setting to get.
*
* local_settings.py allows you to create settings such as:
*
* OPENSTACK_HYPERVISOR_FEATURES = {
* 'can_set_mount_point': True,
* 'can_set_password': False,
* }
*
* To access a specific setting, use a simplified path where a . (dot)
* separates elements in the path. So in the above example, the paths
* would be:
*
* OPENSTACK_HYPERVISOR_FEATURES.can_set_mount_point
* OPENSTACK_HYPERVISOR_FEATURES.can_set_password
*
* @param {object=} defaultSetting If the requested setting does not exist,
* the defaultSetting will be returned. This is optional.
*
* @example
*
* Using the OPENSTACK_HYPERVISOR_FEATURES mentioned above, the following
* would call doSomething and pass the setting value into doSomething.
*
```js
settingsService.getSetting('OPENSTACK_HYPERVISOR_FEATURES.can_set_mount_point')
.then(doSomething);
```
*/
service.getSetting = function (path, defaultSetting) {
var deferred = $q.defer(),
pathElements = path.split("."),
settingAtRequestedPath;
function onSettingsLoaded(settings) {
// This recursively traverses the object hierarchy until either all the
// path elements are traversed or until the next element in the path
// does not have the requested child object.
settingAtRequestedPath = pathElements.reduce(
function (setting, nextPathElement) {
return setting ? setting[nextPathElement] : undefined;
}, settings);
if (typeof settingAtRequestedPath === "undefined" &&
(typeof defaultSetting !== "undefined")) {
settingAtRequestedPath = defaultSetting;
}
deferred.resolve(settingAtRequestedPath);
}
function onSettingsFailure(message) {
deferred.reject(message);
}
service.getSettings()
.then(onSettingsLoaded, onSettingsFailure);
return deferred.promise;
};
/**
* @name hz.api.settingsService.ifEnabled
* @description
* Checks if the desired setting is enabled. This returns a promise.
* If the setting is enabled, the promise will be resolved.
* If it is not enabled, the promise will be rejected. Use it like you
* would normal promises.
*
* @param {string} setting The path to the setting to check.
* local_settings.py allows you to create settings such as:
*
* OPENSTACK_HYPERVISOR_FEATURES = {
* 'can_set_mount_point': True,
* 'can_set_password': False,
* }
*
* To access a specific setting, use a simplified path where a . (dot)
* separates elements in the path. So in the above example, the paths
* would be:
*
* OPENSTACK_HYPERVISOR_FEATURES.can_set_mount_point
* OPENSTACK_HYPERVISOR_FEATURES.can_set_password
*
* @param (object=} expected Used to determine if the setting is
* enabled. The actual setting will be evaluated against the expected
* value using angular.equals(). If they are equal, then it will be
* considered enabled. This is optional and defaults to True.
*
* @param {object=} defaultSetting If the requested setting does not exist,
* the defaultSetting will be used for evaluation. This is optional. If
* not specified and the setting is not specified, then the setting will
* not be considered to be enabled.
*
* @example
* Simple true / false example:
*
* Using the OPENSTACK_HYPERVISOR_FEATURES mentioned above, the following
* would call the "setMountPoint" function only if
* OPENSTACK_HYPERVISOR_FEATURES.can_set_mount_point is set to true.
*
```js
settingsService.ifEnabled('OPENSTACK_HYPERVISOR_FEATURES.can_set_mount_point')
.then(setMountPoint);
```
*
* Evaluating other types of settings:
*
* local_settings.py allows you optionally set the enabled OpenStack
* Service API versions with the following setting:
*
* OPENSTACK_API_VERSIONS = {
* "data-processing": 1.1,
* "identity": 3,
* "volume": 2,
* }
*
* The above is a nested object structure. The simplified path to the
* volume service version is OPENSTACK_API_VERSIONS.volume
*
* It is not uncommon for different OpenStack deployments to have
* different versions of the service enabled for various reasons.
*
* So, now, assume that if version 2 of the volume service (Cinder) is
* enabled that you want to do something. If it isn't, then you will do
* something else.
*
* Assume doSomethingIfVersion2 is a function you want to call if version 2
* is enabled.
*
* Assume doSomethingElse is a function that does something else if
* version 2 is not enabled (optional)
*
```js
settingsService.ifEnabled('OPENSTACK_API_VERSIONS.volume', 2)
.then(doSomethingIfVersion2, doSomethingElse);
```
*
* Now assume that if nothing is set in local_settings, that you want to
* treat the result as if version 1 is enabled (default when nothing set).
*
```js
settingsService.ifEnabled('OPENSTACK_API_VERSIONS.volume', 2, 1)
.then(doSomethingIfVersion2, doSomethingElse);
```
*/
service.ifEnabled = function (setting, expected, defaultSetting) {
var deferred = $q.defer();
// If expected is not defined, we default to expecting the setting
// to be 'true' in order for it to be considered enabled.
expected = (typeof expected === "undefined") ? true : expected;
function onSettingLoaded(setting) {
if (angular.equals(expected, setting)) {
deferred.resolve();
} else {
deferred.reject(interpolate(
gettext('Setting is not enabled: %(setting)s'),
{setting: setting},
true));
}
deferred.resolve(setting);
}
function onSettingFailure(message) {
deferred.reject(message);
}
service.getSetting(setting, defaultSetting)
.then(onSettingLoaded, onSettingFailure);
return deferred.promise;
};
return service;
}
angular.module('hz.api')
.factory('settingsService', ['$q', 'apiService', settingsService]);
}());

View File

@ -0,0 +1,253 @@
/*
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*global angular,describe,it,expect,inject,module,beforeEach,afterEach*/
(function () {
'use strict';
describe('settingsService', function () {
var settingsService,
$httpBackend,
responseMockOpts = {succeed: true},
testData = {
isTrue: true,
isFalse: false,
versions: {one: 1, two: 2},
deep: {nest: { foo: 'bar' } },
isNull: null
};
beforeEach(module('hz.api'));
beforeEach(inject(function (_$httpBackend_, _settingsService_) {
responseMockOpts.succeed = true;
settingsService = _settingsService_;
$httpBackend = _$httpBackend_;
$httpBackend.whenGET('/api/settings/').respond(
function () {
return responseMockOpts.succeed ?
[200, testData, {}] : [500, 'Fail', {}];
});
$httpBackend.expectGET('/api/settings/');
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
describe('getSettings', function () {
it('should return all settings', function () {
settingsService.getSettings().then(
function (actual) {
expect(actual).toEqual(testData);
}
);
$httpBackend.flush();
});
it('should fail when error response', function () {
responseMockOpts.succeed = false;
settingsService.getSettings().then(
function (actual) {
fail('Should not have succeeded: ' + angular.toJson(actual));
},
function (actual) {
expect(actual).toBeDefined();
}
);
$httpBackend.flush();
});
});
describe('getSetting', function () {
it('nested deep object is found', function () {
settingsService.getSetting('deep.nest.foo')
.then(function (actual) {
expect(actual).toEqual('bar');
});
$httpBackend.flush();
});
it("is undefined when doesn't exist", function () {
settingsService.getSetting('will.not.exist')
.then(function (actual) {
expect(actual).toBeUndefined();
});
$httpBackend.flush();
});
it("default is returned when doesn't exist", function () {
var actual;
settingsService.getSetting('will.not.exist', 'hello')
.then(function (actual) {
expect(actual).toEqual('hello');
});
$httpBackend.flush();
});
it('should return true', function () {
settingsService.getSetting('isTrue')
.then(function (actual) {
expect(actual).toEqual(true);
});
$httpBackend.flush();
});
it('should fail when error response', function () {
responseMockOpts.succeed = false;
settingsService.getSetting('isTrue').then(
function (actual) {
fail('Should not have succeeded: ' + angular.toJson(actual));
},
function (actual) {
expect(actual).toBeDefined();
}
);
$httpBackend.flush();
});
});
describe('ifEnabled', function () {
var expectedResult = {};
var enabled = function () {
expectedResult.enabled = true;
};
var notEnabled = function () {
expectedResult.enabled = false;
};
beforeEach(inject(function () {
expectedResult = {enabled: null};
}));
function meetsExpectations(expected) {
$httpBackend.flush();
expect(expectedResult.enabled).toBe(expected);
}
it('should fail when error response', function () {
responseMockOpts.succeed = false;
settingsService.ifEnabled('isTrue').then(
function (actual) {
fail('Should not have succeeded: ' + angular.toJson(actual));
},
function (actual) {
expect(actual).toBeDefined();
}
);
$httpBackend.flush();
});
it('boolean is enabled when true', function () {
settingsService.ifEnabled('isTrue').then(enabled, notEnabled);
meetsExpectations(true);
});
it('boolean is enabled when true expected', function () {
settingsService.ifEnabled('isTrue', true).then(enabled, notEnabled);
meetsExpectations(true);
});
it('boolean is not enabled when false expected', function () {
settingsService.ifEnabled('isTrue', false).then(enabled, notEnabled);
meetsExpectations(false);
});
it('boolean is not enabled when false', function () {
settingsService.ifEnabled('isFalse').then(enabled, notEnabled);
meetsExpectations(false);
});
it('boolean is enabled when false expected', function () {
settingsService.ifEnabled('isFalse', false).then(enabled, notEnabled);
meetsExpectations(true);
});
it('nested object is enabled when expected', function () {
settingsService.ifEnabled('versions.one', 1).then(enabled, notEnabled);
meetsExpectations(true);
});
it('nested object is not enabled', function () {
settingsService.ifEnabled('versions.two', 1).then(enabled, notEnabled);
meetsExpectations(false);
});
it('nested object is not enabled when not found', function () {
settingsService.ifEnabled('no-exist.two', 1).then(enabled, notEnabled);
meetsExpectations(false);
});
it('nested deep object is enabled when expected', function () {
settingsService.ifEnabled('deep.nest.foo', 'bar').then(enabled, notEnabled);
meetsExpectations(true);
});
it('nested deep object is not enabled when not expected', function () {
settingsService.ifEnabled('deep.nest.foo', 'wrong').then(enabled, notEnabled);
meetsExpectations(false);
});
it('null is not enabled', function () {
settingsService.ifEnabled('isNull').then(enabled, notEnabled);
meetsExpectations(false);
});
it('null is enabled when expected', function () {
settingsService.ifEnabled('isNull', null).then(enabled, notEnabled);
meetsExpectations(true);
});
it('true is enabled when not found and true default', function () {
settingsService.ifEnabled('nonExistent', true, true).then(enabled, notEnabled);
meetsExpectations(true);
});
it('true is not enabled when not found and false default', function () {
settingsService.ifEnabled('nonExistent', true, false).then(enabled, notEnabled);
meetsExpectations(false);
});
it('true is not enabled when not found and no default', function () {
settingsService.ifEnabled('nonExistent', true).then(enabled, notEnabled);
meetsExpectations(false);
});
it('false is enabled when not found and expected w/ default', function () {
settingsService.ifEnabled('nonExistent', false, false).then(enabled, notEnabled);
meetsExpectations(true);
});
it('bar is enabled when expected not found and bar default', function () {
settingsService.ifEnabled('nonExistent', 'bar', 'bar').then(enabled, notEnabled);
meetsExpectations(true);
});
it('bar is not enabled when expected not found and not default', function () {
settingsService.ifEnabled('nonExistent', 'bar', 'foo').then(enabled, notEnabled);
meetsExpectations(false);
});
});
});
})();

View File

@ -22,6 +22,7 @@ class ServicesTests(test.JasmineTests):
'horizon/js/angular/services/horizon.utils.js',
'horizon/js/angular/hz.api.module.js',
'horizon/js/angular/services/hz.api.service.js',
'horizon/js/angular/services/hz.api.config.js',
'angular/widget.module.js',
'angular/action-list/action-list.js',
'angular/action-list/button-tooltip.js',
@ -47,6 +48,7 @@ class ServicesTests(test.JasmineTests):
]
specs = [
'horizon/js/angular/services/hz.api.service.spec.js',
'horizon/js/angular/services/hz.api.config.spec.js',
'horizon/tests/jasmine/utils.spec.js',
'angular/action-list/action-list.spec.js',
'angular/bind-scope/bind-scope.spec.js',

View File

@ -1,4 +1,5 @@
# Copyright 2015 IBM Corp.
# Copyright 2015, Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -12,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from django.conf import settings
from django.views import generic
from horizon import conf
@ -25,6 +27,14 @@ admin_configs = ['ajax_queue_limit', 'ajax_poll_interval',
'user_home', 'help_url',
'password_autocomplete', 'disable_password_reveal']
# settings that we allow to be retrieved via REST API
# these settings are available to the client and are not secured.
# *** THEY SHOULD BE TREATED WITH EXTREME CAUTION ***
settings_required = getattr(settings, 'REST_API_REQUIRED_SETTINGS', [])
settings_additional = getattr(settings, 'REST_API_ADDITIONAL_SETTINGS', [])
settings_allowed = settings_required + settings_additional
# properties we know are user config
# this is a white list of keys under HORIZON_CONFIG in settings.pys
# that we want to pass onto client
@ -70,3 +80,18 @@ class AdminConfigs(generic.View):
for key in admin_configs:
config[key] = conf.HORIZON_CONFIG.get(key, None)
return config
@urls.register
class Settings(generic.View):
"""API for retrieving settings.
This API returns read-only settings values.
This configuration object can be fetched as needed.
Examples of settings: OPENSTACK_HYPERVISOR_FEATURES
"""
url_regex = r'settings/$'
@rest_utils.ajax()
def get(self, request):
return {k: getattr(settings, k, None) for k in settings_allowed}

View File

@ -614,3 +614,24 @@ SECURITY_GROUP_RULES = {
# auth_token middleware are using. Allowed values are the
# algorithms supported by Python's hashlib library.
#OPENSTACK_TOKEN_HASH_ALGORITHM = 'md5'
# AngularJS requires some settings to be made available to
# the client side. Some settings are required by in-tree / built-in horizon
# features. These settings must be added to REST_API_REQUIRED_SETTINGS in the
# form of ['SETTING_1','SETTING_2'], etc.
#
# You may remove settings from this list for security purposes, but do so at
# the risk of breaking a built-in horizon feature. These settings are required
# for horizon to function properly. Only remove them if you know what you
# are doing. These settings may in the future be moved to be defined within
# the enabled panel configuration.
# You should not add settings to this list for out of tree extensions.
# See: https://wiki.openstack.org/wiki/Horizon/RESTAPI
REST_API_REQUIRED_SETTINGS = ['OPENSTACK_HYPERVISOR_FEATURES']
# Additional settings can be made available to the client side for
# extensibility by specifying them in REST_API_ADDITIONAL_SETTINGS
# !! Please use extreme caution as the settings are transferred via HTTP/S
# and are not encrypted on the browser. This is an experimental API and
# may be deprecated in the future without notice.
#REST_API_ADDITIONAL_SETTINGS = []

View File

@ -51,6 +51,14 @@ class ConfigRestTestCase(test.TestCase):
self.assertStatusCode(response, 200)
self.assertContains(response.content, content)
def test_settings_config_get(self):
request = self.mock_rest_request()
response = config.Settings().get(request)
self.assertStatusCode(response, 200)
self.assertContains(response.content, "REST_API_SETTING_1")
self.assertContains(response.content, "REST_API_SETTING_2")
self.assertNotContains(response.content, "REST_API_SECURITY")
def test_ignore_list(self):
ignore_config = {"password_validator": "someobject"}
content = '"password_validator": "someobject"'

View File

@ -205,3 +205,9 @@ POLICY_FILES = {
# The openstack_auth.user.Token object isn't JSON-serializable ATM
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
REST_API_SETTING_1 = 'foo'
REST_API_SETTING_2 = 'bar'
REST_API_SECURITY = 'SECURITY'
REST_API_REQUIRED_SETTINGS = ['REST_API_SETTING_1']
REST_API_ADDITIONAL_SETTINGS = ['REST_API_SETTING_2']