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:
parent
23d5b3aa32
commit
4254165250
@ -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``
|
||||||
------------------------------
|
------------------------------
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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',
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
@ -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 = [
|
||||||
|
@ -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')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
@ -0,0 +1,3 @@
|
|||||||
|
<p translate>
|
||||||
|
Scheduler hints allow you to pass additional placement related information to the compute scheduler.
|
||||||
|
</p>
|
@ -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>
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user