Support scheduler hints when launching instance

This adds the Scheduler Hints step to the angular Launch Instance
workflow and allows specifying arbitrary hints when launching an
instance. It also displays available hints from the glance metadata
definitions catalog OS::Nova::Server namespace that use the
"scheduler_hints" properties target.

Implements: blueprint add-scheduler-hints
Change-Id: Ic33c31e645f45b7a4cbdf13e9a115c96399d5e32
This commit is contained in:
Justin Pomeroy 2016-01-26 10:28:57 -06:00 committed by Travis Tripp
parent 23d5b3aa32
commit 4254165250
17 changed files with 319 additions and 49 deletions

View File

@ -621,7 +621,8 @@ edited.
Default:: Default::
{ {
"config_drive": False "config_drive": False,
"enable_scheduler_hints": True
} }
A dictionary of settings which can be used to provide the default values for A dictionary of settings which can be used to provide the default values for
@ -630,6 +631,8 @@ properties found in the Launch Instance modal.
The ``config_drive`` setting specifies the default value for the Configuration The ``config_drive`` setting specifies the default value for the Configuration
Drive property. Drive property.
The ``enable_scheduler_hints`` setting specifies whether or not Scheduler Hints
can be provided when launching an instance.
``LAUNCH_INSTANCE_NG_ENABLED`` ``LAUNCH_INSTANCE_NG_ENABLED``
------------------------------ ------------------------------

View File

@ -646,7 +646,8 @@ def server_create(request, name, image, flavor, key_name, user_data,
security_groups, block_device_mapping=None, security_groups, block_device_mapping=None,
block_device_mapping_v2=None, nics=None, block_device_mapping_v2=None, nics=None,
availability_zone=None, instance_count=1, admin_pass=None, availability_zone=None, instance_count=1, admin_pass=None,
disk_config=None, config_drive=None, meta=None): disk_config=None, config_drive=None, meta=None,
scheduler_hints=None):
return Server(novaclient(request).servers.create( return Server(novaclient(request).servers.create(
name, image, flavor, userdata=user_data, name, image, flavor, userdata=user_data,
security_groups=security_groups, security_groups=security_groups,
@ -655,7 +656,7 @@ def server_create(request, name, image, flavor, key_name, user_data,
nics=nics, availability_zone=availability_zone, nics=nics, availability_zone=availability_zone,
min_count=instance_count, admin_pass=admin_pass, min_count=instance_count, admin_pass=admin_pass,
disk_config=disk_config, config_drive=config_drive, disk_config=disk_config, config_drive=config_drive,
meta=meta), request) meta=meta, scheduler_hints=scheduler_hints), request)
def server_delete(request, instance): def server_delete(request, instance):

View File

@ -184,7 +184,7 @@ class Servers(generic.View):
_optional_create = [ _optional_create = [
'block_device_mapping', 'block_device_mapping_v2', 'nics', 'meta', 'block_device_mapping', 'block_device_mapping_v2', 'nics', 'meta',
'availability_zone', 'instance_count', 'admin_pass', 'disk_config', 'availability_zone', 'instance_count', 'admin_pass', 'disk_config',
'config_drive' 'config_drive', 'scheduler_hints'
] ]
@rest_utils.ajax() @rest_utils.ajax()

View File

@ -29,7 +29,9 @@
'horizon.app.core.openstack-service-api.settings', 'horizon.app.core.openstack-service-api.settings',
'horizon.dashboard.project.workflow.launch-instance.boot-source-types', 'horizon.dashboard.project.workflow.launch-instance.boot-source-types',
'horizon.dashboard.project.workflow.launch-instance.non_bootable_image_types', 'horizon.dashboard.project.workflow.launch-instance.non_bootable_image_types',
'horizon.framework.widgets.toast.service' 'horizon.framework.widgets.toast.service',
'horizon.app.core.openstack-service-api.policy',
'horizon.dashboard.project.workflow.launch-instance.step-policy'
]; ];
/** /**
@ -69,7 +71,9 @@
settings, settings,
bootSourceTypes, bootSourceTypes,
nonBootableImageTypes, nonBootableImageTypes,
toast toast,
policy,
stepPolicy
) { ) {
var initPromise; var initPromise;
@ -125,7 +129,8 @@
flavor: null, flavor: null,
image: null, image: null,
volume: null, volume: null,
instance: null instance: null,
hints: null
}, },
networks: [], networks: [],
ports: [], ports: [],
@ -137,6 +142,7 @@
volumes: [], volumes: [],
volumeSnapshots: [], volumeSnapshots: [],
metadataTree: null, metadataTree: null,
hintsTree: null,
/** /**
* api methods for UI controllers * api methods for UI controllers
@ -270,6 +276,7 @@
setFinalSpecPorts(finalSpec); setFinalSpecPorts(finalSpec);
setFinalSpecKeyPairs(finalSpec); setFinalSpecKeyPairs(finalSpec);
setFinalSpecSecurityGroups(finalSpec); setFinalSpecSecurityGroups(finalSpec);
setFinalSpecSchedulerHints(finalSpec);
setFinalSpecMetadata(finalSpec); setFinalSpecMetadata(finalSpec);
return novaAPI.createServer(finalSpec).then(successMessage); return novaAPI.createServer(finalSpec).then(successMessage);
@ -592,6 +599,20 @@
angular.extend(model.novaLimits, data.data); angular.extend(model.novaLimits, data.data);
} }
// Scheduler hints
function setFinalSpecSchedulerHints(finalSpec) {
if (model.hintsTree) {
var hints = model.hintsTree.getExisting();
if (!angular.equals({}, hints)) {
angular.forEach(hints, function(value, key) {
hints[key] = value + '';
});
finalSpec.scheduler_hints = hints;
}
}
}
// Instance metadata // Instance metadata
function setFinalSpecMetadata(finalSpec) { function setFinalSpecMetadata(finalSpec) {
@ -608,10 +629,10 @@
// Metadata Definitions // Metadata Definitions
/* /**
* Metadata definitions provide supplemental information in source image * Metadata definitions provide supplemental information in source image detail
* detail rows and are used on the metadata tab for adding metadata to the * rows and are used on the metadata tab for adding metadata to the instance and
* instance. * on the scheduler hints tab.
*/ */
function getMetadataDefinitions() { function getMetadataDefinitions() {
// Metadata definitions often apply to multiple resource types. It is optimal to make a // Metadata definitions often apply to multiple resource types. It is optimal to make a
@ -625,6 +646,14 @@
}; };
angular.forEach(resourceTypes, applyForResourceType); angular.forEach(resourceTypes, applyForResourceType);
// Need to check setting and policy for scheduler hints
$q.all([
settings.ifEnabled('LAUNCH_INSTANCE_DEFAULTS.enable_scheduler_hints', true, true),
policy.ifAllowed(stepPolicy.schedulerHints)
]).then(function getSchedulerHints() {
applyForResourceType(['OS::Nova::Server', 'scheduler_hints'], 'hints');
});
} }
function applyForResourceType(resourceType, key) { function applyForResourceType(resourceType, key) {

View File

@ -19,7 +19,7 @@
describe('Launch Instance Model', function() { describe('Launch Instance Model', function() {
describe('launchInstanceModel Factory', function() { describe('launchInstanceModel Factory', function() {
var model, scope, settings, $q; var model, scope, settings, $q, glance;
var cinderEnabled = false; var cinderEnabled = false;
var neutronEnabled = false; var neutronEnabled = false;
var novaExtensionsEnabled = false; var novaExtensionsEnabled = false;
@ -187,6 +187,16 @@
} }
}); });
$provide.value('horizon.app.core.openstack-service-api.policy', {
ifAllowed: function() {
var deferred = $q.defer();
deferred.resolve();
return deferred.promise;
}
});
$provide.value('horizon.app.core.openstack-service-api.novaExtensions', { $provide.value('horizon.app.core.openstack-service-api.novaExtensions', {
ifNameEnabled: function() { ifNameEnabled: function() {
var deferred = $q.defer(); var deferred = $q.defer();
@ -207,6 +217,27 @@
deferred.resolve(settings[setting]); deferred.resolve(settings[setting]);
return deferred.promise;
},
ifEnabled: function(setting) {
var deferred = $q.defer();
var keys = setting.split('.');
var index = 0;
var value = settings;
while (angular.isObject(value) && index < keys.length) {
value = value[keys[index]];
index++;
}
// NOTE: This does not work for the general case of ifEnabled, only for what
// we need it for at the moment (only explicit false rejects the promise).
if (value === false) {
deferred.reject();
} else {
deferred.resolve();
}
return deferred.promise; return deferred.promise;
} }
}); });
@ -216,10 +247,12 @@
}); });
})); }));
beforeEach(inject(function(launchInstanceModel, $rootScope, _$q_) { beforeEach(inject(function($injector) {
model = launchInstanceModel; model = $injector.get('launchInstanceModel');
$q = _$q_; $q = $injector.get('$q');
scope = $rootScope.$new(); scope = $injector.get('$rootScope').$new();
glance = $injector.get('horizon.app.core.openstack-service-api.glance');
spyOn(glance, 'getNamespaces').and.callThrough();
})); }));
describe('Initial object (pre-initialize)', function() { describe('Initial object (pre-initialize)', function() {
@ -252,7 +285,8 @@
expect(model.metadataDefs.image).toBeNull(); expect(model.metadataDefs.image).toBeNull();
expect(model.metadataDefs.volume).toBeNull(); expect(model.metadataDefs.volume).toBeNull();
expect(model.metadataDefs.instance).toBeNull(); expect(model.metadataDefs.instance).toBeNull();
expect(Object.keys(model.metadataDefs).length).toBe(4); expect(model.metadataDefs.hints).toBeNull();
expect(Object.keys(model.metadataDefs).length).toBe(5);
}); });
it('defaults "allow create volume from image" to false', function() { it('defaults "allow create volume from image" to false', function() {
@ -271,6 +305,10 @@
expect(model.metadataTree).toBe(null); expect(model.metadataTree).toBe(null);
}); });
it('defaults "hintsTree" to null', function() {
expect(model.hintsTree).toBe(null);
});
it('initializes "nova limits" to empty object', function() { it('initializes "nova limits" to empty object', function() {
expect(model.novaLimits).toEqual({}); expect(model.novaLimits).toEqual({});
}); });
@ -387,6 +425,19 @@
scope.$apply(); scope.$apply();
expect(model.ports.length).toBe(1); expect(model.ports.length).toBe(1);
}); });
it('should make 5 requests for namespaces', function() {
model.initialize(true);
scope.$apply();
expect(glance.getNamespaces.calls.count()).toBe(5);
});
it('should not request scheduler hints if scheduler hints disabled', function() {
settings.LAUNCH_INSTANCE_DEFAULTS.enable_scheduler_hints = false;
model.initialize(true);
scope.$apply();
expect(glance.getNamespaces.calls.count()).toBe(4);
});
}); });
describe('Post Initialization Model - Initializing', function() { describe('Post Initialization Model - Initializing', function() {
@ -472,7 +523,7 @@
}); });
describe('Create Instance', function() { describe('Create Instance', function() {
var metadata; var metadata, hints;
beforeEach(function() { beforeEach(function() {
// initialize some data // initialize some data
@ -495,6 +546,13 @@
return metadata; return metadata;
} }
}; };
hints = {'group': 'group1'};
model.hintsTree = {
getExisting: function() {
return hints;
}
};
}); });
it('should set final spec in format required by Nova (Neutron disabled)', function() { it('should set final spec in format required by Nova (Neutron disabled)', function() {
@ -649,6 +707,23 @@
expect(finalSpec.meta).toBe(metadata); expect(finalSpec.meta).toBe(metadata);
}); });
it('should not have scheduler_hints property if no scheduler hints specified', function() {
hints = {};
var finalSpec = model.createInstance();
expect(finalSpec.scheduler_hints).toBeUndefined();
model.hintsTree = null;
finalSpec = model.createInstance();
expect(finalSpec.scheduler_hints).toBeUndefined();
});
it('should have scheduler_hints property if scheduler hints specified', function() {
var finalSpec = model.createInstance();
expect(finalSpec.scheduler_hints).toBe(hints);
});
}); });
}); });
}); });

View File

@ -22,10 +22,11 @@
launchInstanceWorkflow.$inject = [ launchInstanceWorkflow.$inject = [
'horizon.dashboard.project.workflow.launch-instance.basePath', 'horizon.dashboard.project.workflow.launch-instance.basePath',
'horizon.dashboard.project.workflow.launch-instance.step-policy',
'horizon.app.core.workflow.factory' 'horizon.app.core.workflow.factory'
]; ];
function launchInstanceWorkflow(basePath, dashboardWorkflow) { function launchInstanceWorkflow(basePath, stepPolicy, dashboardWorkflow) {
return dashboardWorkflow({ return dashboardWorkflow({
title: gettext('Launch Instance'), title: gettext('Launch Instance'),
@ -89,6 +90,16 @@
formName: 'launchInstanceConfigurationForm' formName: 'launchInstanceConfigurationForm'
}, },
{ {
id: 'hints',
title: gettext('Scheduler Hints'),
templateUrl: basePath + 'scheduler-hints/scheduler-hints.html',
helpUrl: basePath + 'scheduler-hints/scheduler-hints.help.html',
formName: 'launchInstanceSchedulerHintsForm',
policy: stepPolicy.schedulerHints,
setting: 'LAUNCH_INSTANCE_DEFAULTS.enable_scheduler_hints'
},
{
id: 'metadata',
title: gettext('Metadata'), title: gettext('Metadata'),
templateUrl: basePath + 'metadata/metadata.html', templateUrl: basePath + 'metadata/metadata.html',
helpUrl: basePath + 'metadata/metadata.help.html', helpUrl: basePath + 'metadata/metadata.help.html',

View File

@ -17,28 +17,19 @@
'use strict'; 'use strict';
describe('horizon.dashboard.project.workflow.launch-instance.workflow tests', function () { describe('horizon.dashboard.project.workflow.launch-instance.workflow tests', function () {
var launchInstanceWorkflow; var launchInstanceWorkflow, stepPolicy;
beforeEach(module('horizon.app.core')); beforeEach(module('horizon.app.core'));
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.framework.widgets.toast'));
beforeEach(module('horizon.dashboard.project')); beforeEach(module('horizon.dashboard.project'));
beforeEach(module(function($provide) {
// Need to mock horizon.framework.workflow from 'horizon'
var workflow = function(spec, decorators) {
angular.forEach(decorators, function(decorator) {
decorator(spec);
});
return spec;
};
$provide.value('horizon.app.core.openstack-service-api.serviceCatalog', {
ifTypeEnabled: angular.noop
});
$provide.value('horizon.framework.util.workflow.service', workflow);
}));
beforeEach(inject(function ($injector) { beforeEach(inject(function ($injector) {
launchInstanceWorkflow = $injector.get( launchInstanceWorkflow = $injector.get(
'horizon.dashboard.project.workflow.launch-instance.workflow' 'horizon.dashboard.project.workflow.launch-instance.workflow'
); );
stepPolicy = $injector.get('horizon.dashboard.project.workflow.launch-instance.step-policy');
})); }));
it('should be defined', function () { it('should be defined', function () {
@ -49,9 +40,9 @@
expect(launchInstanceWorkflow.title).toBeDefined(); expect(launchInstanceWorkflow.title).toBeDefined();
}); });
it('should have the nine steps defined', function () { it('should have 10 steps defined', function () {
expect(launchInstanceWorkflow.steps).toBeDefined(); expect(launchInstanceWorkflow.steps).toBeDefined();
expect(launchInstanceWorkflow.steps.length).toBe(9); expect(launchInstanceWorkflow.steps.length).toBe(10);
var forms = [ var forms = [
'launchInstanceDetailsForm', 'launchInstanceDetailsForm',
@ -62,6 +53,7 @@
'launchInstanceAccessAndSecurityForm', 'launchInstanceAccessAndSecurityForm',
'launchInstanceKeypairForm', 'launchInstanceKeypairForm',
'launchInstanceConfigurationForm', 'launchInstanceConfigurationForm',
'launchInstanceSchedulerHintsForm',
'launchInstanceMetadataForm' 'launchInstanceMetadataForm'
]; ];
@ -77,6 +69,10 @@
it('specifies that the network port step requires the network service type', function() { it('specifies that the network port step requires the network service type', function() {
expect(launchInstanceWorkflow.steps[4].requiredServiceTypes).toEqual(['network']); expect(launchInstanceWorkflow.steps[4].requiredServiceTypes).toEqual(['network']);
}); });
it('has a policy rule for the scheduler hints step', function() {
expect(launchInstanceWorkflow.steps[8].policy).toEqual(stepPolicy.schedulerHints);
});
}); });
})(); })();

View File

@ -39,6 +39,16 @@
.constant('horizon.dashboard.project.workflow.launch-instance.non_bootable_image_types', .constant('horizon.dashboard.project.workflow.launch-instance.non_bootable_image_types',
['aki', 'ari']) ['aki', 'ari'])
/**
* @name horizon.dashboard.project.workflow.launch-instance.step-policy
* @description Policies for displaying steps in the workflow.
*/
.constant('horizon.dashboard.project.workflow.launch-instance.step-policy', {
// This policy determines if the scheduler hints extension is discoverable when listing
// available extensions. It's possible the extension is installed but not discoverable.
schedulerHints: { rules: [['compute', 'os_compute_api:os-scheduler-hints:discoverable']] }
})
.filter('diskFormat', diskFormat); .filter('diskFormat', diskFormat);
config.$inject = [ config.$inject = [

View File

@ -0,0 +1,49 @@
/*
* Copyright 2016 IBM Corp.
*
* 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';
/**
* @ngdoc controller
* @name LaunchInstanceSchedulerHintsController
* @description
* The `LaunchInstanceSchedulerHintsController` controller provides functions for
* configuring the scheduler hints step of the Launch Instance Wizard.
*
*/
angular
.module('horizon.dashboard.project.workflow.launch-instance')
.controller('LaunchInstanceSchedulerHintsController', LaunchInstanceSchedulerHintsController);
LaunchInstanceSchedulerHintsController.$inject = [
'horizon.framework.util.i18n.gettext'
];
function LaunchInstanceSchedulerHintsController(gettext) {
var ctrl = this;
ctrl.text = {
/* eslint-disable max-len */
help: gettext('You can specify scheduler hints by moving items from the left column to the right column. In the left column there are scheduler hint definitions from the Glance Metadata Catalog. Use the "Custom" option to add scheduler hints with the key of your choice.'),
/* eslint-enable max-len */
available: gettext('Available Scheduler Hints'),
existing: gettext('Existing Scheduler Hints'),
noAvailable: gettext('No available scheduler hints'),
noExisting: gettext('No existing scheduler hints')
};
}
})();

View File

@ -0,0 +1,3 @@
<p translate>
Scheduler hints allow you to pass additional placement related information to the compute scheduler.
</p>

View File

@ -0,0 +1,12 @@
<div ng-controller="LaunchInstanceSchedulerHintsController as ctrl">
<p translate>
This step allows you to add scheduler hints to your instance.
</p>
<metadata-tree
ng-if="model.metadataDefs.hints"
available="::model.metadataDefs.hints"
existing="{}"
text="::ctrl.text"
model="::model.hintsTree">
</metadata-tree>
</div>

View File

@ -0,0 +1,56 @@
/*
* Copyright 2016 IBM Corp.
*
* 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';
describe('Launch Instance Scheduler Hints Step', function() {
describe('metadata tree', function() {
var $scope, $element, model;
beforeEach(module('templates'));
beforeEach(module('horizon.framework.util.i18n'));
beforeEach(module('horizon.dashboard.project.workflow.launch-instance'));
beforeEach(inject(function($injector) {
var $compile = $injector.get('$compile');
var $templateCache = $injector.get('$templateCache');
var basePath = $injector.get('horizon.dashboard.project.workflow.launch-instance.basePath');
var markup = $templateCache.get(basePath + 'scheduler-hints/scheduler-hints.html');
model = {
metadataDefs: { hints: false }
};
$scope = $injector.get('$rootScope').$new();
$scope.model = model;
$element = $compile(markup)($scope);
}));
it('should define display text values', function() {
var ctrl = $element.scope().ctrl;
expect(ctrl.text).toBeDefined();
});
it('should create metadata tree only after dependencies are received', function() {
expect($element.find('metadata-tree').length).toBe(0);
model.metadataDefs.hints = {};
$scope.$apply();
expect($element.find('metadata-tree').length).toBe(1);
});
});
});
})();

View File

@ -238,6 +238,7 @@ OPENSTACK_KEYSTONE_BACKEND = {
# properties found in the Launch Instance modal. # properties found in the Launch Instance modal.
#LAUNCH_INSTANCE_DEFAULTS = { #LAUNCH_INSTANCE_DEFAULTS = {
# 'config_drive': False, # 'config_drive': False,
# 'enable_scheduler_hints': True
#} #}
# The Xen Hypervisor has the ability to set the mount point for volumes # The Xen Hypervisor has the ability to set the mount point for volumes

View File

@ -27,16 +27,17 @@
* @kind function * @kind function
* @description * @description
* *
* A workflow decorator function that looks for the requiredServiceTypes or policy * A workflow decorator function that looks for the `requiredServiceTypes`, `policy`, or
* properties on each step in the workflow. If either of these properties exist then * `setting` properties on each step in the workflow. If any of these properties exist
* the checkReadiness method is added to the step. The checkReadiness method will * then the `checkReadiness` method is added to the step. The `checkReadiness` method will
* make sure the necessary OpenStack services are enabled and the policy check passes * make sure the necessary OpenStack services are enabled, policy check passes, and the
* in order for the step to be displayed. * setting evaluates to `true` in order for the step to be displayed.
* *
* Injected dependencies: * Injected dependencies:
* - $q * - $q
* - serviceCatalog horizon.app.core.openstack-service-api.serviceCatalog * - serviceCatalog horizon.app.core.openstack-service-api.serviceCatalog
* - policy horizon.app.core.openstack-service-api.policy * - policy horizon.app.core.openstack-service-api.policy
* - settings horizon.app.core.openstack-service-api.settings
* *
* @param {Object} spec The input workflow specification object. * @param {Object} spec The input workflow specification object.
* @returns {Object} The decorated workflow specification object, the same * @returns {Object} The decorated workflow specification object, the same
@ -50,12 +51,13 @@
dashboardWorkflowDecorator.$inject = [ dashboardWorkflowDecorator.$inject = [
'$q', '$q',
'horizon.app.core.openstack-service-api.serviceCatalog', 'horizon.app.core.openstack-service-api.serviceCatalog',
'horizon.app.core.openstack-service-api.policy' 'horizon.app.core.openstack-service-api.policy',
'horizon.app.core.openstack-service-api.settings'
]; ];
///////////// /////////////
function dashboardWorkflowDecorator($q, serviceCatalog, policy) { function dashboardWorkflowDecorator($q, serviceCatalog, policy, settings) {
return decorator; return decorator;
function decorator(spec) { function decorator(spec) {
@ -78,6 +80,9 @@
if (step.policy) { if (step.policy) {
promises.push(policy.ifAllowed(step.policy)); promises.push(policy.ifAllowed(step.policy));
} }
if (step.setting) {
promises.push(settings.ifEnabled(step.setting, true, true));
}
if (promises.length > 0) { if (promises.length > 0) {
step.checkReadiness = function () { step.checkReadiness = function () {
return $q.all(promises); return $q.all(promises);

View File

@ -17,11 +17,12 @@
'use strict'; 'use strict';
describe('Workflow Decorator', function () { describe('Workflow Decorator', function () {
var decoratorService, catalogService, policyService, $scope, deferred; var decoratorService, catalogService, policyService, settingsService, $scope, deferred;
var steps = [ var steps = [
{ id: '1' }, { id: '1' },
{ id: '2', requiredServiceTypes: ['foo-service'] }, { id: '2', requiredServiceTypes: ['foo-service'] },
{ id: '3', policy: 'foo-policy' } { id: '3', policy: 'foo-policy' },
{ id: '4', setting: 'STEPS.step_4_enabled' }
]; ];
var spec = { steps: steps }; var spec = { steps: steps };
@ -36,15 +37,17 @@
decoratorService = $injector.get('horizon.app.core.workflow.decorator'); decoratorService = $injector.get('horizon.app.core.workflow.decorator');
catalogService = $injector.get('horizon.app.core.openstack-service-api.serviceCatalog'); catalogService = $injector.get('horizon.app.core.openstack-service-api.serviceCatalog');
policyService = $injector.get('horizon.app.core.openstack-service-api.policy'); policyService = $injector.get('horizon.app.core.openstack-service-api.policy');
settingsService = $injector.get('horizon.app.core.openstack-service-api.settings');
spyOn(catalogService, 'ifTypeEnabled').and.returnValue(deferred.promise); spyOn(catalogService, 'ifTypeEnabled').and.returnValue(deferred.promise);
spyOn(policyService, 'ifAllowed').and.returnValue(deferred.promise); spyOn(policyService, 'ifAllowed').and.returnValue(deferred.promise);
spyOn(settingsService, 'ifEnabled').and.returnValue(deferred.promise);
})); }));
it('is a function', function() { it('is a function', function() {
expect(angular.isFunction(decoratorService)).toBe(true); expect(angular.isFunction(decoratorService)).toBe(true);
}); });
it('checks each step for required services and policies', function() { it('checks each step for required services, policies, and settings', function() {
decoratorService(spec); decoratorService(spec);
expect(steps[0].checkReadiness).toBeUndefined(); expect(steps[0].checkReadiness).toBeUndefined();
expect(steps[1].checkReadiness).toBeDefined(); expect(steps[1].checkReadiness).toBeDefined();
@ -53,6 +56,8 @@
expect(catalogService.ifTypeEnabled).toHaveBeenCalledWith('foo-service'); expect(catalogService.ifTypeEnabled).toHaveBeenCalledWith('foo-service');
expect(policyService.ifAllowed.calls.count()).toBe(1); expect(policyService.ifAllowed.calls.count()).toBe(1);
expect(policyService.ifAllowed).toHaveBeenCalledWith('foo-policy'); expect(policyService.ifAllowed).toHaveBeenCalledWith('foo-policy');
expect(settingsService.ifEnabled.calls.count()).toBe(1);
expect(settingsService.ifEnabled).toHaveBeenCalledWith('STEPS.step_4_enabled', true, true);
}); });
it('step checkReadiness function returns correct results', function() { it('step checkReadiness function returns correct results', function() {

View File

@ -49,13 +49,17 @@
* templateUrl: basePath + 'steps/create-volume/step3.html', * templateUrl: basePath + 'steps/create-volume/step3.html',
* helpUrl: basePath + 'steps/create-volume/step3.help.html', * helpUrl: basePath + 'steps/create-volume/step3.help.html',
* formName: 'step3Form', * formName: 'step3Form',
* policy: { rules: [['compute', 'os_compute_api:os-scheduler-hints:discoverable']] } * policy: { rules: [['compute', 'os_compute_api:os-scheduler-hints:discoverable']] },
* setting: 'LAUNCH_INSTANCE_DEFAULTS.enable_scheduler_hints'
* }] * }]
* }); * });
* ``` * ```
* For each step, the requiredServiceTypes property specifies the service types that must * For each step, the `requiredServiceTypes` property specifies the service types that must
* be available in the service catalog for the step to be displayed. The policy property * be available in the service catalog for the step to be displayed. The `policy` property
* specifies the policy check that must pass in order for the step to be displayed. * specifies the policy check that must pass in order for the step to be displayed. The
* `setting` property specifies the settings key to check (must be a boolean value) for
* determining if the step should be displayed. If the key is not found then this will resolve
* to `true`.
* *
* @param {Object} The input workflow specification object * @param {Object} The input workflow specification object
* @returns {Object} The decorated workflow specification object, the same * @returns {Object} The decorated workflow specification object, the same

View File

@ -0,0 +1,10 @@
---
features:
- Added the Scheduler Hints tab to the new Launch Instance workflow to allow
adding scheduler hints to an instance at launch. In addition to adding
custom key-value pairs, the user can also choose from properties in the
glance metadata definitions catalog that have the OS::Nova::Server resource
type and scheduler_hints properties target.
- Added settings support to the angular workflow service so each step in a
workflow can specify a boolean setting that must pass in order for the step
to be displayed.