diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst index 1d3fb2dfec..932d52020f 100644 --- a/doc/source/topics/settings.rst +++ b/doc/source/topics/settings.rst @@ -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`` ------------------------------ diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index d61ad94b4e..0333714b7a 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -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): diff --git a/openstack_dashboard/api/rest/nova.py b/openstack_dashboard/api/rest/nova.py index 682cc52224..e9c9e6df0a 100644 --- a/openstack_dashboard/api/rest/nova.py +++ b/openstack_dashboard/api/rest/nova.py @@ -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() diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.js index 57c4d51dc2..fee6bda398 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.js @@ -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) { diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.spec.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.spec.js index a9e153b9ed..51d4813033 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.spec.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.spec.js @@ -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); + }); + }); }); }); diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-workflow.service.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-workflow.service.js index 5ac0fc1c9a..3f88d7e6d0 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-workflow.service.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-workflow.service.js @@ -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', diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-workflow.service.spec.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-workflow.service.spec.js index a9aab4ac02..536a357921 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-workflow.service.spec.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-workflow.service.spec.js @@ -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); + }); }); })(); diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance.module.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance.module.js index edc0218681..2f4909e2c8 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance.module.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance.module.js @@ -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 = [ diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/scheduler-hints/scheduler-hints.controller.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/scheduler-hints/scheduler-hints.controller.js new file mode 100644 index 0000000000..cd73f02ddd --- /dev/null +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/scheduler-hints/scheduler-hints.controller.js @@ -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') + }; + } + +})(); diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/scheduler-hints/scheduler-hints.help.html b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/scheduler-hints/scheduler-hints.help.html new file mode 100644 index 0000000000..f896e7a97a --- /dev/null +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/scheduler-hints/scheduler-hints.help.html @@ -0,0 +1,3 @@ +
+ Scheduler hints allow you to pass additional placement related information to the compute scheduler. +
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/scheduler-hints/scheduler-hints.html b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/scheduler-hints/scheduler-hints.html new file mode 100644 index 0000000000..05c1841243 --- /dev/null +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/scheduler-hints/scheduler-hints.html @@ -0,0 +1,12 @@ ++ This step allows you to add scheduler hints to your instance. +
+