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. +

+ + +
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/scheduler-hints/scheduler-hints.spec.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/scheduler-hints/scheduler-hints.spec.js new file mode 100644 index 0000000000..9d8a8d7eb9 --- /dev/null +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/scheduler-hints/scheduler-hints.spec.js @@ -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); + }); + }); + }); +})(); diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index 83bef29464..1e9a631dcb 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -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 diff --git a/openstack_dashboard/static/app/core/workflow/decorator.service.js b/openstack_dashboard/static/app/core/workflow/decorator.service.js index 696355c2a3..2b110cf781 100644 --- a/openstack_dashboard/static/app/core/workflow/decorator.service.js +++ b/openstack_dashboard/static/app/core/workflow/decorator.service.js @@ -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); diff --git a/openstack_dashboard/static/app/core/workflow/decorator.service.spec.js b/openstack_dashboard/static/app/core/workflow/decorator.service.spec.js index 3f54d89f2f..477c357e76 100644 --- a/openstack_dashboard/static/app/core/workflow/decorator.service.spec.js +++ b/openstack_dashboard/static/app/core/workflow/decorator.service.spec.js @@ -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() { diff --git a/openstack_dashboard/static/app/core/workflow/workflow.service.js b/openstack_dashboard/static/app/core/workflow/workflow.service.js index 6b2ba0e833..0a809afc26 100644 --- a/openstack_dashboard/static/app/core/workflow/workflow.service.js +++ b/openstack_dashboard/static/app/core/workflow/workflow.service.js @@ -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 diff --git a/releasenotes/notes/bp-add-scheduler-hints-77600faec041e134.yaml b/releasenotes/notes/bp-add-scheduler-hints-77600faec041e134.yaml new file mode 100644 index 0000000000..c45abc57a0 --- /dev/null +++ b/releasenotes/notes/bp-add-scheduler-hints-77600faec041e134.yaml @@ -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.