diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst index 2817a6da91..6d39485bad 100755 --- a/doc/source/topics/settings.rst +++ b/doc/source/topics/settings.rst @@ -646,12 +646,17 @@ edited. ---------------------------- .. versionadded:: 9.0.0(Mitaka) +.. versionupdated:: 10.0.0(Newton) Default:: { "config_drive": False, "enable_scheduler_hints": True + "disable_image": False, + "disable_instance_snapshot": False, + "disable_volume": False, + "disable_volume_snapshot": False, } A dictionary of settings which can be used to provide the default values for @@ -663,6 +668,21 @@ Drive property. The ``enable_scheduler_hints`` setting specifies whether or not Scheduler Hints can be provided when launching an instance. +The ``disable_image`` setting disables Images as a valid boot source for launching +instances. Image sources won't show up in the Launch Instance modal. + +The ``disable_instance_snapshot`` setting disables Snapshots as a valid boot +source for launching instances. Snapshots sources won't show up in the Launch +Instance modal. + +The ``disable_volume`` setting disables Volumes as a valid boot +source for launching instances. Volumes sources won't show up +in the Launch Instance modal. + +The ``disable_volume_snapshot`` setting disables Volume Snapshots as a valid +boot source for launching instances. Volume Snapshots sources won't show up +in the Launch Instance modal. + ``LAUNCH_INSTANCE_NG_ENABLED`` ------------------------------ 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 6e96961d8d..50f81acf93 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 @@ -28,7 +28,6 @@ 'horizon.app.core.openstack-service-api.serviceCatalog', '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.app.core.openstack-service-api.policy', 'horizon.dashboard.project.workflow.launch-instance.step-policy' @@ -48,7 +47,10 @@ * @param {Object} securityGroup * @param {Object} serviceCatalog * @param {Object} settings + * @param {Object} bootSourceTypes * @param {Object} toast + * @param {Object} policy + * @param {Object} stepPolicy * @description * This is the M part in MVC design pattern for launch instance * wizard workflow. It is responsible for providing data to the @@ -70,7 +72,6 @@ serviceCatalog, settings, bootSourceTypes, - nonBootableImageTypes, toast, policy, stepPolicy @@ -218,16 +219,18 @@ model.allowedBootSources.length = 0; + var launchInstanceDefaults = settings.getSetting('LAUNCH_INSTANCE_DEFAULTS'); + promise = $q.all([ - getImages(), novaAPI.getAvailabilityZones().then(onGetAvailabilityZones, noop), novaAPI.getFlavors(true, true).then(onGetFlavors, noop), novaAPI.getKeypairs().then(onGetKeypairs, noop), novaAPI.getLimits(true).then(onGetNovaLimits, noop), securityGroup.query().then(onGetSecurityGroups, noop), serviceCatalog.ifTypeEnabled('network').then(getNetworks, noop), - serviceCatalog.ifTypeEnabled('volume').then(getVolumes, noop), - settings.getSetting('LAUNCH_INSTANCE_DEFAULTS').then(setDefaultValues, noop) + launchInstanceDefaults.then(addImageSourcesIfEnabled, noop), + launchInstanceDefaults.then(setDefaultValues, noop), + launchInstanceDefaults.then(addVolumeSourcesIfEnabled, noop) ]); promise.then(onInitSuccess, onInitFail); @@ -496,15 +499,86 @@ // Boot Source - function getImages() { - return glanceAPI.getImages({status:'active'}).then(onGetImages); + function addImageSourcesIfEnabled(config) { + // in case settings are deleted or not present + var allEnabled = !config; + // if the settings are missing or the specific setting is missing default to true + var enabledImage = allEnabled || !config.disable_image; + var enabledSnapshot = allEnabled || !config.disable_instance_snapshot; + + if (enabledImage || enabledSnapshot) { + return glanceAPI.getImages({status: 'active'}).then(function getEnabledImages(data) { + if (enabledImage) { + onGetImages(data); + } + if (enabledSnapshot) { + onGetSnapshots(data); + } + }); + } + } + + function addVolumeSourcesIfEnabled(config) { + var volumeDeferred = $q.defer(); + var volumeSnapshotDeferred = $q.defer(); + serviceCatalog + .ifTypeEnabled('volume') + .then(onVolumeServiceEnabled, resolvePromises); + function onVolumeServiceEnabled() { + model.volumeBootable = true; + novaExtensions + .ifNameEnabled('BlockDeviceMappingV2Boot') + .then(onBootToVolumeSupported); + if (!config || !config.disable_volume) { + getVolumes().then(resolveVolumes, failVolumes); + } else { + resolveVolumes(); + } + if (!config || !config.disable_volume_snapshot) { + getVolumeSnapshots().then(resolveVolumeSnapshots, failVolumeSnapshots); + } else { + resolveVolumeSnapshots(); + } + } + function onBootToVolumeSupported() { + model.allowCreateVolumeFromImage = true; + } + function getVolumes() { + return cinderAPI.getVolumes({status: 'available', bootable: 1}) + .then(onGetVolumes); + } + function getVolumeSnapshots() { + return cinderAPI.getVolumeSnapshots({status: 'available'}) + .then(onGetVolumeSnapshots); + } + function resolvePromises() { + volumeDeferred.resolve(); + volumeSnapshotDeferred.resolve(); + } + function resolveVolumes() { + volumeDeferred.resolve(); + } + function failVolumes() { + volumeDeferred.resolve(); + } + function resolveVolumeSnapshots() { + volumeSnapshotDeferred.resolve(); + } + function failVolumeSnapshots() { + volumeSnapshotDeferred.resolve(); + } + return $q.all( + [ + volumeDeferred.promise, + volumeSnapshotDeferred.promise + ]); } function isBootableImageType(image) { // This is a blacklist of images that can not be booted. // If the image container type is in the blacklist // The evaluation will result in a 0 or greater index. - return nonBootableImageTypes.indexOf(image.container_format) < 0; + return bootSourceTypes.NON_BOOTABLE_IMAGE_TYPES.indexOf(image.container_format) < 0; } function onGetImages(data) { @@ -514,7 +588,9 @@ (!image.properties || image.properties.image_type !== 'snapshot'); })); addAllowedBootSource(model.images, bootSourceTypes.IMAGE, gettext('Image')); + } + function onGetSnapshots(data) { model.imageSnapshots.length = 0; push.apply(model.imageSnapshots, data.data.items.filter(function (image) { return isBootableImageType(image) && @@ -528,42 +604,24 @@ ); } - function getVolumes() { - var volumePromises = []; - // Need to check if Volume service is enabled before getting volumes - model.volumeBootable = true; - addAllowedBootSource(model.volumes, bootSourceTypes.VOLUME, gettext('Volume')); - addAllowedBootSource( - model.volumeSnapshots, - bootSourceTypes.VOLUME_SNAPSHOT, - gettext('Volume Snapshot') - ); - volumePromises.push(cinderAPI.getVolumes({ status: 'available', bootable: true }) - .then(onGetVolumes)); - volumePromises.push(cinderAPI.getVolumeSnapshots({ status: 'available' }) - .then(onGetVolumeSnapshots)); - - // Can only boot image to volume if the Nova extension is enabled. - novaExtensions.ifNameEnabled('BlockDeviceMappingV2Boot') - .then(function() { - model.allowCreateVolumeFromImage = true; - }); - - return $q.all(volumePromises); - } - function onGetVolumes(data) { model.volumes.length = 0; push.apply(model.volumes, data.data.items); + addAllowedBootSource(model.volumes, bootSourceTypes.VOLUME, gettext('Volume')); } function onGetVolumeSnapshots(data) { model.volumeSnapshots.length = 0; push.apply(model.volumeSnapshots, data.data.items); + addAllowedBootSource( + model.volumeSnapshots, + bootSourceTypes.VOLUME_SNAPSHOT, + gettext('Volume Snapshot') + ); } function addAllowedBootSource(rawTypes, type, label) { - if (rawTypes && rawTypes.length > 0) { + if (rawTypes) { model.allowedBootSources.push({ type: type, label: label 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 95b6b93c0c..9e8f6c3c33 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,10 +19,11 @@ describe('Launch Instance Model', function() { describe('launchInstanceModel Factory', function() { - var model, scope, settings, $q, glance; + var model, scope, settings, $q, glance, IMAGE, VOLUME, VOLUME_SNAPSHOT, INSTANCE_SNAPSHOT; var cinderEnabled = false; var neutronEnabled = false; var novaExtensionsEnabled = false; + var novaApi = { createServer: function(finalSpec) { return { @@ -127,6 +128,54 @@ beforeEach(module('horizon.dashboard.project.workflow.launch-instance')); + beforeEach(module(function($provide) { + $provide.value('horizon.app.core.openstack-service-api.glance', { + getImages: function () { + var images = [ + {container_format: 'aki', properties: {}}, + {container_format: 'ari', properties: {}}, + {container_format: 'ami', properties: {}}, + {container_format: 'raw', properties: {}}, + {container_format: 'ami', properties: {image_type: 'snapshot'}}, + {container_format: 'raw', properties: {image_type: 'snapshot'}} + ]; + + var deferred = $q.defer(); + deferred.resolve({data: {items: images}}); + + return deferred.promise; + }, + getNamespaces: function () { + var namespaces = ['ns-1', 'ns-2']; + + var deferred = $q.defer(); + deferred.resolve({data: {items: namespaces}}); + + return deferred.promise; + } + }); + + beforeEach(function () { + settings = { + LAUNCH_INSTANCE_DEFAULTS: { + config_drive: false, + disable_image: false, + disable_instance_snapshot: false, + disable_volume: false, + disable_volume_snapshot: false + } + }; + IMAGE = {type: 'image', label: 'Image'}; + VOLUME = {type: 'volume', label: 'Volume'}; + VOLUME_SNAPSHOT = {type: 'volume_snapshot', label: 'Volume Snapshot'}; + INSTANCE_SNAPSHOT = {type: 'snapshot', label: 'Instance Snapshot'}; + }); + + $provide.value('horizon.app.core.openstack-service-api.nova', novaApi); + })); + + beforeEach(module('horizon.dashboard.project.workflow.launch-instance')); + beforeEach(module(function($provide) { $provide.value('horizon.app.core.openstack-service-api.glance', { getImages: function() { @@ -417,7 +466,7 @@ }); it('should default config_drive to false if setting not provided', function() { - delete settings.LAUNCH_INSTANCE_DEFAULTS; + delete settings.LAUNCH_INSTANCE_DEFAULTS.config_drive; model.initialize(true); scope.$apply(); @@ -503,6 +552,188 @@ expect(model.newInstanceSpec.networks.length).toBe(1); expect(model.newInstanceSpec.networks).toEqual(networks); }); + + it('should have the proper entries in allowedBootSources', function() { + model.initialize(true); + scope.$apply(); + + expect(model.allowedBootSources).toBeDefined(); + expect(model.allowedBootSources.length).toBe(4); + expect(model.allowedBootSources).toContain(IMAGE); + expect(model.allowedBootSources).toContain(VOLUME); + expect(model.allowedBootSources).toContain(VOLUME_SNAPSHOT); + expect(model.allowedBootSources).toContain(INSTANCE_SNAPSHOT); + }); + + it('should have proper allowedBootSources if settings are missing', function() { + delete settings.LAUNCH_INSTANCE_DEFAULTS; + model.initialize(true); + scope.$apply(); + + expect(model.allowedBootSources).toBeDefined(); + expect(model.allowedBootSources.length).toBe(4); + expect(model.allowedBootSources).toContain(IMAGE); + expect(model.allowedBootSources).toContain(INSTANCE_SNAPSHOT); + expect(model.allowedBootSources).toContain(VOLUME); + expect(model.allowedBootSources).toContain(VOLUME_SNAPSHOT); + }); + + it('should have proper allowedBootSources if specific settings missing', function() { + delete settings.LAUNCH_INSTANCE_DEFAULTS.disable_image; + delete settings.LAUNCH_INSTANCE_DEFAULTS.disable_instance_snapshot; + delete settings.LAUNCH_INSTANCE_DEFAULTS.disable_volume; + model.initialize(true); + scope.$apply(); + + expect(model.allowedBootSources).toBeDefined(); + expect(model.allowedBootSources.length).toBe(4); + expect(model.allowedBootSources).toContain(IMAGE); + expect(model.allowedBootSources).toContain(INSTANCE_SNAPSHOT); + expect(model.allowedBootSources).toContain(VOLUME); + expect(model.allowedBootSources).toContain(VOLUME_SNAPSHOT); + }); + + it('should have no images if disable_image is set to true', function() { + settings.LAUNCH_INSTANCE_DEFAULTS.disable_image = true; + model.initialize(true); + scope.$apply(); + + expect(model.images.length).toBe(0); + expect(model.images).toEqual([]); + expect(model.allowedBootSources).toBeDefined(); + expect(model.allowedBootSources.length).toBe(3); + expect(model.allowedBootSources).not.toContain(IMAGE); + expect(model.allowedBootSources).toContain(INSTANCE_SNAPSHOT); + expect(model.allowedBootSources).toContain(VOLUME); + expect(model.allowedBootSources).toContain(VOLUME_SNAPSHOT); + }); + + it('should have images if disable_image is missing', function() { + delete settings.LAUNCH_INSTANCE_DEFAULTS.disable_image; + model.initialize(true); + scope.$apply(); + + expect(model.allowedBootSources).toBeDefined(); + expect(model.allowedBootSources.length).toBe(4); + expect(model.allowedBootSources).toContain(IMAGE); + expect(model.allowedBootSources).toContain(INSTANCE_SNAPSHOT); + expect(model.allowedBootSources).toContain(VOLUME); + expect(model.allowedBootSources).toContain(VOLUME_SNAPSHOT); + }); + + it('should have no volumes if disable_volume is set to true', function() { + settings.LAUNCH_INSTANCE_DEFAULTS.disable_volume = true; + model.initialize(true); + scope.$apply(); + + expect(model.volumes.length).toBe(0); + expect(model.volumes).toEqual([]); + expect(model.volumeSnapshots.length).toBe(2); + expect(model.volumeSnapshots).toEqual([{ id: 'snap-1' }, { id: 'snap-2' }]); + expect(model.allowedBootSources).toBeDefined(); + expect(model.allowedBootSources.length).toBe(3); + expect(model.allowedBootSources).toContain(IMAGE); + expect(model.allowedBootSources).toContain(INSTANCE_SNAPSHOT); + expect(model.allowedBootSources).not.toContain(VOLUME); + expect(model.allowedBootSources).toContain(VOLUME_SNAPSHOT); + }); + + it('should have volumes if disable_volume is missing', function() { + delete settings.LAUNCH_INSTANCE_DEFAULTS.disable_volume; + model.initialize(true); + scope.$apply(); + + expect(model.allowedBootSources).toBeDefined(); + expect(model.allowedBootSources.length).toBe(4); + expect(model.allowedBootSources).toContain(IMAGE); + expect(model.allowedBootSources).toContain(INSTANCE_SNAPSHOT); + expect(model.allowedBootSources).toContain(VOLUME); + expect(model.allowedBootSources).toContain(VOLUME_SNAPSHOT); + }); + + it('should have volume snapshots if disable_volume_snapshot is missing', function() { + delete settings.LAUNCH_INSTANCE_DEFAULTS.disable_volume_snapshot; + model.initialize(true); + scope.$apply(); + + expect(model.allowedBootSources).toBeDefined(); + expect(model.allowedBootSources.length).toBe(4); + expect(model.allowedBootSources).toContain(IMAGE); + expect(model.allowedBootSources).toContain(INSTANCE_SNAPSHOT); + expect(model.allowedBootSources).toContain(VOLUME); + expect(model.allowedBootSources).toContain(VOLUME_SNAPSHOT); + }); + + it('should not have volume snapshots if disable_volume_snapshot is set to true', + function() { + settings.LAUNCH_INSTANCE_DEFAULTS.disable_volume_snapshot = true; + model.initialize(true); + scope.$apply(); + + expect(model.allowedBootSources).toBeDefined(); + expect(model.allowedBootSources.length).toBe(3); + expect(model.allowedBootSources).toContain(IMAGE); + expect(model.allowedBootSources).toContain(INSTANCE_SNAPSHOT); + expect(model.allowedBootSources).toContain(VOLUME); + expect(model.allowedBootSources).not.toContain(VOLUME_SNAPSHOT); + }); + + it('should have no snapshot if disable_instance_snapshot is set to true', function() { + settings.LAUNCH_INSTANCE_DEFAULTS.disable_instance_snapshot = true; + model.initialize(true); + scope.$apply(); + + expect(model.allowedBootSources).toBeDefined(); + expect(model.allowedBootSources.length).toBe(3); + expect(model.allowedBootSources).toContain(IMAGE); + expect(model.allowedBootSources).not.toContain(INSTANCE_SNAPSHOT); + expect(model.allowedBootSources).toContain(VOLUME); + expect(model.allowedBootSources).toContain(VOLUME_SNAPSHOT); + }); + + it('should have snapshot if disable_instance_snapshot is missing', function() { + delete settings.LAUNCH_INSTANCE_DEFAULTS.disable_instance_snapshot; + model.initialize(true); + scope.$apply(); + + expect(model.allowedBootSources).toBeDefined(); + expect(model.allowedBootSources.length).toBe(4); + expect(model.allowedBootSources).toContain(IMAGE); + expect(model.allowedBootSources).toContain(INSTANCE_SNAPSHOT); + expect(model.allowedBootSources).toContain(VOLUME); + expect(model.allowedBootSources).toContain(VOLUME_SNAPSHOT); + }); + + it('should have no snapshot and no image if both are disabled', function() { + settings.LAUNCH_INSTANCE_DEFAULTS.disable_image = true; + settings.LAUNCH_INSTANCE_DEFAULTS.disable_instance_snapshot = true; + + model.initialize(true); + scope.$apply(); + + expect(model.allowedBootSources).toBeDefined(); + expect(model.allowedBootSources.length).toBe(2); + expect(model.allowedBootSources).not.toContain(IMAGE); + expect(model.allowedBootSources).not.toContain(INSTANCE_SNAPSHOT); + expect(model.allowedBootSources).toContain(VOLUME); + expect(model.allowedBootSources).toContain(VOLUME_SNAPSHOT); + }); + + it('should have snapshot and image if both are missing', function() { + delete settings.LAUNCH_INSTANCE_DEFAULTS.disable_image; + delete settings.LAUNCH_INSTANCE_DEFAULTS.disable_instance_snapshot; + + model.initialize(true); + scope.$apply(); + + expect(model.allowedBootSources).toBeDefined(); + expect(model.allowedBootSources.length).toBe(4); + expect(model.allowedBootSources).toContain(IMAGE); + expect(model.allowedBootSources).toContain(INSTANCE_SNAPSHOT); + expect(model.allowedBootSources).toContain(VOLUME); + expect(model.allowedBootSources).toContain(VOLUME_SNAPSHOT); + }); + }); describe('Post Initialization Model - Initializing', function() { 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 08b5f4cdad..7948189cff 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 @@ -34,7 +34,8 @@ IMAGE: 'image', INSTANCE_SNAPSHOT: 'snapshot', VOLUME: 'volume', - VOLUME_SNAPSHOT: 'volume_snapshot' + VOLUME_SNAPSHOT: 'volume_snapshot', + NON_BOOTABLE_IMAGE_TYPES: ['aki', 'ari'] }) .constant('horizon.dashboard.project.workflow.launch-instance.non_bootable_image_types', ['aki', 'ari']) diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.controller.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.controller.js index aedfcc3530..ecd8aed47f 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.controller.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.controller.js @@ -71,26 +71,23 @@ /* * Boot Sources */ - ctrl.bootSourcesOptions = [ - { type: bootSourceTypes.IMAGE, label: gettext('Image') }, - { type: bootSourceTypes.INSTANCE_SNAPSHOT, label: gettext('Instance Snapshot') }, - { type: bootSourceTypes.VOLUME, label: gettext('Volume') }, - { type: bootSourceTypes.VOLUME_SNAPSHOT, label: gettext('Volume Snapshot') } - ]; - ctrl.updateBootSourceSelection = updateBootSourceSelection; + var selection = ctrl.selection = $scope.model.newInstanceSpec.source; /* * Transfer table */ ctrl.tableHeadCells = []; ctrl.tableBodyCells = []; - ctrl.tableData = {}; + ctrl.tableData = { + available: [], + allocated: selection, + displayedAvailable: [], + displayedAllocated: [] + }; ctrl.helpText = {}; ctrl.sourceDetails = basePath + 'source/source-details.html'; - var selection = ctrl.selection = $scope.model.newInstanceSpec.source; - var bootSources = { image: { available: $scope.model.images, @@ -373,8 +370,25 @@ } ); - // Explicitly remove watchers on desruction of this controller + // When the allowedboot list changes, change the source_type + // and update the table for the new source selection. Only done + // with the first item for the list + var allowedBootSourcesWatcher = $scope.$watchCollection( + function getAllowedBootSources() { + return $scope.model.allowedBootSources; + }, + function changeBootSource(newValue) { + if (angular.isArray(newValue) && newValue.length > 0 && + !$scope.model.newInstanceSpec.source_type) { + updateBootSourceSelection(newValue[0].type); + $scope.model.newInstanceSpec.source_type = newValue[0]; + } + } + ); + + // Explicitly remove watchers on destruction of this controller $scope.$on('$destroy', function() { + allowedBootSourcesWatcher(); newSpecWatcher(); allocatedWatcher(); bootSourceWatcher(); @@ -383,14 +397,6 @@ snapshotWatcher(); }); - // Initialize - changeBootSource(ctrl.bootSourcesOptions[0].type); - - if (!$scope.model.newInstanceSpec.source_type) { - $scope.model.newInstanceSpec.source_type = ctrl.bootSourcesOptions[0]; - ctrl.currentBootSource = ctrl.bootSourcesOptions[0].type; - } - //////////////////// function updateBootSourceSelection(selectedSource) { @@ -505,8 +511,11 @@ var pre = findSourceById($scope.model.images, id); if (pre) { changeBootSource(bootSourceTypes.IMAGE, [pre]); - $scope.model.newInstanceSpec.source_type = ctrl.bootSourcesOptions[0]; - ctrl.currentBootSource = ctrl.bootSourcesOptions[0].type; + $scope.model.newInstanceSpec.source_type = { + type: bootSourceTypes.IMAGE, + label: gettext('Image') + }; + ctrl.currentBootSource = bootSourceTypes.IMAGE; } } @@ -514,8 +523,11 @@ var pre = findSourceById($scope.model.volumes, id); if (pre) { changeBootSource(bootSourceTypes.VOLUME, [pre]); - $scope.model.newInstanceSpec.source_type = ctrl.bootSourcesOptions[2]; - ctrl.currentBootSource = ctrl.bootSourcesOptions[2].type; + $scope.model.newInstanceSpec.source_type = { + type: bootSourceTypes.VOLUME, + label: gettext('Volume') + }; + ctrl.currentBootSource = bootSourceTypes.VOLUME; } } @@ -523,8 +535,11 @@ var pre = findSourceById($scope.model.volumeSnapshots, id); if (pre) { changeBootSource(bootSourceTypes.VOLUME_SNAPSHOT, [pre]); - $scope.model.newInstanceSpec.source_type = ctrl.bootSourcesOptions[3]; - ctrl.currentBootSource = ctrl.bootSourcesOptions[3].type; + $scope.model.newInstanceSpec.source_type = { + type: bootSourceTypes.VOLUME_SNAPSHOT, + label: gettext('Snapshot') + }; + ctrl.currentBootSource = bootSourceTypes.VOLUME_SNAPSHOT; } } } diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.controller.spec.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.controller.spec.js index d7359edc99..cbef3b0a9a 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.controller.spec.js +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.controller.spec.js @@ -49,6 +49,7 @@ scope.initPromise = deferred.promise; scope.model = { + allowedBootSources: [{type: 'image', label: 'Image'}], newInstanceSpec: { source: [], source_type: '' }, images: [ { id: 'image-1' }, { id: 'image-2' } ], imageSnapshots: [], @@ -78,18 +79,6 @@ expect(ctrl.volumeSizeError).toBeDefined(); }); - it('defines the correct boot source options', function() { - expect(ctrl.bootSourcesOptions).toBeDefined(); - var types = ['image', 'snapshot', 'volume', 'volume_snapshot']; - var opts = ctrl.bootSourcesOptions.map(function(x) { - return x.type; - }); - types.forEach(function(key) { - expect(opts).toContain(key); - }); - expect(ctrl.bootSourcesOptions.length).toBe(types.length); - }); - it('initializes transfer table variables', function() { // NOTE: these are set by the default, not the initial values. // Arguably we shouldn't even set the original values. @@ -214,14 +203,25 @@ }); describe('Scope Functions', function() { - - describe('watchers', function () { - it('establishes five watches', function() { - expect(scope.$watch.calls.count()).toBe(6); + describe('watches', function() { + beforeEach( function() { + // Initialize the watchers with default data + scope.model.newInstanceSpec.source_type = null; + scope.model.allowedBootSources = [{type: 'test_type', label: 'test'}]; + scope.$apply(); }); - - it("establishes two watch collections", function () { - expect(scope.$watchCollection.calls.count()).toBe(3); + it("establishes seven watches", function () { + // Count calls to $watch (note: $watchCollection + // also calls $watch) + expect(scope.$watch.calls.count()).toBe(7); + }); + it("establishes four watch collections", function () { + expect(scope.$watchCollection.calls.count()).toBe(4); + }); + it('should set source type on new allowedbootsources', function() { + expect(angular.equals(scope.model.newInstanceSpec.source_type, + {type: 'test_type', label: 'test'})).toBe(true); + expect(ctrl.currentBootSource).toBe('test_type'); }); }); diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.html b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.html index d17c56328e..0a8cb73300 100644 --- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.html +++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.html @@ -1,4 +1,5 @@
+

Instance source is the template used to create an instance. You can use a snapshot of an existing instance, an image, or a volume (if enabled). You can also choose to use persistent storage by creating a new volume. @@ -9,7 +10,7 @@

@@ -284,5 +285,10 @@ - +
+
+
There are no allowed boot sources. + If you think this is wrong please contact your administrator. +
+
diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index 89f5183c57..4be4c1d14b 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -241,6 +241,10 @@ OPENSTACK_KEYSTONE_BACKEND = { #LAUNCH_INSTANCE_DEFAULTS = { # 'config_drive': False, # 'enable_scheduler_hints': True +# 'disable_image': False, +# 'disable_instance_snapshot': False, +# 'disable_volume': False, +# 'disable_volume_snapshot': False, #} # The Xen Hypervisor has the ability to set the mount point for volumes diff --git a/releasenotes/notes/bp-configurable-boot-sources-4ba89f3b2a927801.yaml b/releasenotes/notes/bp-configurable-boot-sources-4ba89f3b2a927801.yaml new file mode 100644 index 0000000000..e6ecb2fced --- /dev/null +++ b/releasenotes/notes/bp-configurable-boot-sources-4ba89f3b2a927801.yaml @@ -0,0 +1,8 @@ +--- +features: + - > + [`blueprint configurable-boot-sources `_] + Allows administrators to restrict which sources are available to boot from in the + Launch Instance modal by adding 4 new settings to + LAUNCH_INSTANCE_DEFAULTS (disable_image, disable_instance_snapshot, disable_volume, + disable_volume_snapshot).