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::
{
"config_drive": False
"config_drive": False,
"enable_scheduler_hints": True
}
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
Drive property.
The ``enable_scheduler_hints`` setting specifies whether or not Scheduler Hints
can be provided when launching an instance.
``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,
block_device_mapping_v2=None, nics=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(
name, image, flavor, userdata=user_data,
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,
min_count=instance_count, admin_pass=admin_pass,
disk_config=disk_config, config_drive=config_drive,
meta=meta), request)
meta=meta, scheduler_hints=scheduler_hints), request)
def server_delete(request, instance):

View File

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

View File

@ -29,7 +29,9 @@
'horizon.app.core.openstack-service-api.settings',
'horizon.dashboard.project.workflow.launch-instance.boot-source-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,
bootSourceTypes,
nonBootableImageTypes,
toast
toast,
policy,
stepPolicy
) {
var initPromise;
@ -125,7 +129,8 @@
flavor: null,
image: null,
volume: null,
instance: null
instance: null,
hints: null
},
networks: [],
ports: [],
@ -137,6 +142,7 @@
volumes: [],
volumeSnapshots: [],
metadataTree: null,
hintsTree: null,
/**
* api methods for UI controllers
@ -270,6 +276,7 @@
setFinalSpecPorts(finalSpec);
setFinalSpecKeyPairs(finalSpec);
setFinalSpecSecurityGroups(finalSpec);
setFinalSpecSchedulerHints(finalSpec);
setFinalSpecMetadata(finalSpec);
return novaAPI.createServer(finalSpec).then(successMessage);
@ -592,6 +599,20 @@
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
function setFinalSpecMetadata(finalSpec) {
@ -608,10 +629,10 @@
// Metadata Definitions
/*
* Metadata definitions provide supplemental information in source image
* detail rows and are used on the metadata tab for adding metadata to the
* instance.
/**
* Metadata definitions provide supplemental information in source image detail
* rows and are used on the metadata tab for adding metadata to the instance and
* on the scheduler hints tab.
*/
function getMetadataDefinitions() {
// Metadata definitions often apply to multiple resource types. It is optimal to make a
@ -625,6 +646,14 @@
};
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) {

View File

@ -19,7 +19,7 @@
describe('Launch Instance Model', function() {
describe('launchInstanceModel Factory', function() {
var model, scope, settings, $q;
var model, scope, settings, $q, glance;
var cinderEnabled = false;
var neutronEnabled = 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', {
ifNameEnabled: function() {
var deferred = $q.defer();
@ -207,6 +217,27 @@
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;
}
});
@ -216,10 +247,12 @@
});
}));
beforeEach(inject(function(launchInstanceModel, $rootScope, _$q_) {
model = launchInstanceModel;
$q = _$q_;
scope = $rootScope.$new();
beforeEach(inject(function($injector) {
model = $injector.get('launchInstanceModel');
$q = $injector.get('$q');
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() {
@ -252,7 +285,8 @@
expect(model.metadataDefs.image).toBeNull();
expect(model.metadataDefs.volume).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() {
@ -271,6 +305,10 @@
expect(model.metadataTree).toBe(null);
});
it('defaults "hintsTree" to null', function() {
expect(model.hintsTree).toBe(null);
});
it('initializes "nova limits" to empty object', function() {
expect(model.novaLimits).toEqual({});
});
@ -387,6 +425,19 @@
scope.$apply();
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() {
@ -472,7 +523,7 @@
});
describe('Create Instance', function() {
var metadata;
var metadata, hints;
beforeEach(function() {
// initialize some data
@ -495,6 +546,13 @@
return metadata;
}
};
hints = {'group': 'group1'};
model.hintsTree = {
getExisting: function() {
return hints;
}
};
});
it('should set final spec in format required by Nova (Neutron disabled)', function() {
@ -649,6 +707,23 @@
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 = [
'horizon.dashboard.project.workflow.launch-instance.basePath',
'horizon.dashboard.project.workflow.launch-instance.step-policy',
'horizon.app.core.workflow.factory'
];
function launchInstanceWorkflow(basePath, dashboardWorkflow) {
function launchInstanceWorkflow(basePath, stepPolicy, dashboardWorkflow) {
return dashboardWorkflow({
title: gettext('Launch Instance'),
@ -89,6 +90,16 @@
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'),
templateUrl: basePath + 'metadata/metadata.html',
helpUrl: basePath + 'metadata/metadata.help.html',

View File

@ -17,28 +17,19 @@
'use strict';
describe('horizon.dashboard.project.workflow.launch-instance.workflow tests', function () {
var launchInstanceWorkflow;
var launchInstanceWorkflow, stepPolicy;
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(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) {
launchInstanceWorkflow = $injector.get(
'horizon.dashboard.project.workflow.launch-instance.workflow'
);
stepPolicy = $injector.get('horizon.dashboard.project.workflow.launch-instance.step-policy');
}));
it('should be defined', function () {
@ -49,9 +40,9 @@
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.length).toBe(9);
expect(launchInstanceWorkflow.steps.length).toBe(10);
var forms = [
'launchInstanceDetailsForm',
@ -62,6 +53,7 @@
'launchInstanceAccessAndSecurityForm',
'launchInstanceKeypairForm',
'launchInstanceConfigurationForm',
'launchInstanceSchedulerHintsForm',
'launchInstanceMetadataForm'
];
@ -77,6 +69,10 @@
it('specifies that the network port step requires the network service type', function() {
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',
['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);
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.
#LAUNCH_INSTANCE_DEFAULTS = {
# 'config_drive': False,
# 'enable_scheduler_hints': True
#}
# The Xen Hypervisor has the ability to set the mount point for volumes

View File

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

View File

@ -17,11 +17,12 @@
'use strict';
describe('Workflow Decorator', function () {
var decoratorService, catalogService, policyService, $scope, deferred;
var decoratorService, catalogService, policyService, settingsService, $scope, deferred;
var steps = [
{ id: '1' },
{ 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 };
@ -36,15 +37,17 @@
decoratorService = $injector.get('horizon.app.core.workflow.decorator');
catalogService = $injector.get('horizon.app.core.openstack-service-api.serviceCatalog');
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(policyService, 'ifAllowed').and.returnValue(deferred.promise);
spyOn(settingsService, 'ifEnabled').and.returnValue(deferred.promise);
}));
it('is a function', function() {
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);
expect(steps[0].checkReadiness).toBeUndefined();
expect(steps[1].checkReadiness).toBeDefined();
@ -53,6 +56,8 @@
expect(catalogService.ifTypeEnabled).toHaveBeenCalledWith('foo-service');
expect(policyService.ifAllowed.calls.count()).toBe(1);
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() {

View File

@ -49,13 +49,17 @@
* templateUrl: basePath + 'steps/create-volume/step3.html',
* helpUrl: basePath + 'steps/create-volume/step3.help.html',
* 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
* 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.
* 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
* 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
* @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.