Angular: Configuration of boot sources for launch instance

Adds new configs to LAUNCH_INSTANCE_DEFAULTS to configure
which sources are available when launching an instance.
Provides an info message  if no boot sources are enabled.
Prevents doing extra calls if a boot source is disabled.
Adds tests to check for the proper filling of allowedBootSources.
Removes one test as the object being checked no longer exists.

Co-Authored-By: Brad Pokorny <brad_pokorny@symantec.com>
Co-Authored-By: Yosef Hoffman <yh128t@att.com>

Change-Id: I90f76c34dbfb20cb54d5f3e599052388bd0dba39
Implements: blueprint configurable-boot-sources
This commit is contained in:
Itxaka 2016-01-20 10:20:53 +01:00 committed by Yosef Hoffman
parent 526e34839d
commit 4a9f988813
9 changed files with 425 additions and 82 deletions

View File

@ -646,12 +646,17 @@ edited.
---------------------------- ----------------------------
.. versionadded:: 9.0.0(Mitaka) .. versionadded:: 9.0.0(Mitaka)
.. versionupdated:: 10.0.0(Newton)
Default:: Default::
{ {
"config_drive": False, "config_drive": False,
"enable_scheduler_hints": True "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 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 The ``enable_scheduler_hints`` setting specifies whether or not Scheduler Hints
can be provided when launching an instance. 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`` ``LAUNCH_INSTANCE_NG_ENABLED``
------------------------------ ------------------------------

View File

@ -28,7 +28,6 @@
'horizon.app.core.openstack-service-api.serviceCatalog', 'horizon.app.core.openstack-service-api.serviceCatalog',
'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.framework.widgets.toast.service', 'horizon.framework.widgets.toast.service',
'horizon.app.core.openstack-service-api.policy', 'horizon.app.core.openstack-service-api.policy',
'horizon.dashboard.project.workflow.launch-instance.step-policy' 'horizon.dashboard.project.workflow.launch-instance.step-policy'
@ -48,7 +47,10 @@
* @param {Object} securityGroup * @param {Object} securityGroup
* @param {Object} serviceCatalog * @param {Object} serviceCatalog
* @param {Object} settings * @param {Object} settings
* @param {Object} bootSourceTypes
* @param {Object} toast * @param {Object} toast
* @param {Object} policy
* @param {Object} stepPolicy
* @description * @description
* This is the M part in MVC design pattern for launch instance * This is the M part in MVC design pattern for launch instance
* wizard workflow. It is responsible for providing data to the * wizard workflow. It is responsible for providing data to the
@ -70,7 +72,6 @@
serviceCatalog, serviceCatalog,
settings, settings,
bootSourceTypes, bootSourceTypes,
nonBootableImageTypes,
toast, toast,
policy, policy,
stepPolicy stepPolicy
@ -218,16 +219,18 @@
model.allowedBootSources.length = 0; model.allowedBootSources.length = 0;
var launchInstanceDefaults = settings.getSetting('LAUNCH_INSTANCE_DEFAULTS');
promise = $q.all([ promise = $q.all([
getImages(),
novaAPI.getAvailabilityZones().then(onGetAvailabilityZones, noop), novaAPI.getAvailabilityZones().then(onGetAvailabilityZones, noop),
novaAPI.getFlavors(true, true).then(onGetFlavors, noop), novaAPI.getFlavors(true, true).then(onGetFlavors, noop),
novaAPI.getKeypairs().then(onGetKeypairs, noop), novaAPI.getKeypairs().then(onGetKeypairs, noop),
novaAPI.getLimits(true).then(onGetNovaLimits, noop), novaAPI.getLimits(true).then(onGetNovaLimits, noop),
securityGroup.query().then(onGetSecurityGroups, noop), securityGroup.query().then(onGetSecurityGroups, noop),
serviceCatalog.ifTypeEnabled('network').then(getNetworks, noop), serviceCatalog.ifTypeEnabled('network').then(getNetworks, noop),
serviceCatalog.ifTypeEnabled('volume').then(getVolumes, noop), launchInstanceDefaults.then(addImageSourcesIfEnabled, noop),
settings.getSetting('LAUNCH_INSTANCE_DEFAULTS').then(setDefaultValues, noop) launchInstanceDefaults.then(setDefaultValues, noop),
launchInstanceDefaults.then(addVolumeSourcesIfEnabled, noop)
]); ]);
promise.then(onInitSuccess, onInitFail); promise.then(onInitSuccess, onInitFail);
@ -496,15 +499,86 @@
// Boot Source // Boot Source
function getImages() { function addImageSourcesIfEnabled(config) {
return glanceAPI.getImages({status:'active'}).then(onGetImages); // 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) { function isBootableImageType(image) {
// This is a blacklist of images that can not be booted. // This is a blacklist of images that can not be booted.
// If the image container type is in the blacklist // If the image container type is in the blacklist
// The evaluation will result in a 0 or greater index. // 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) { function onGetImages(data) {
@ -514,7 +588,9 @@
(!image.properties || image.properties.image_type !== 'snapshot'); (!image.properties || image.properties.image_type !== 'snapshot');
})); }));
addAllowedBootSource(model.images, bootSourceTypes.IMAGE, gettext('Image')); addAllowedBootSource(model.images, bootSourceTypes.IMAGE, gettext('Image'));
}
function onGetSnapshots(data) {
model.imageSnapshots.length = 0; model.imageSnapshots.length = 0;
push.apply(model.imageSnapshots, data.data.items.filter(function (image) { push.apply(model.imageSnapshots, data.data.items.filter(function (image) {
return isBootableImageType(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) { function onGetVolumes(data) {
model.volumes.length = 0; model.volumes.length = 0;
push.apply(model.volumes, data.data.items); push.apply(model.volumes, data.data.items);
addAllowedBootSource(model.volumes, bootSourceTypes.VOLUME, gettext('Volume'));
} }
function onGetVolumeSnapshots(data) { function onGetVolumeSnapshots(data) {
model.volumeSnapshots.length = 0; model.volumeSnapshots.length = 0;
push.apply(model.volumeSnapshots, data.data.items); push.apply(model.volumeSnapshots, data.data.items);
addAllowedBootSource(
model.volumeSnapshots,
bootSourceTypes.VOLUME_SNAPSHOT,
gettext('Volume Snapshot')
);
} }
function addAllowedBootSource(rawTypes, type, label) { function addAllowedBootSource(rawTypes, type, label) {
if (rawTypes && rawTypes.length > 0) { if (rawTypes) {
model.allowedBootSources.push({ model.allowedBootSources.push({
type: type, type: type,
label: label label: label

View File

@ -19,10 +19,11 @@
describe('Launch Instance Model', function() { describe('Launch Instance Model', function() {
describe('launchInstanceModel Factory', 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 cinderEnabled = false;
var neutronEnabled = false; var neutronEnabled = false;
var novaExtensionsEnabled = false; var novaExtensionsEnabled = false;
var novaApi = { var novaApi = {
createServer: function(finalSpec) { createServer: function(finalSpec) {
return { return {
@ -127,6 +128,54 @@
beforeEach(module('horizon.dashboard.project.workflow.launch-instance')); 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) { beforeEach(module(function($provide) {
$provide.value('horizon.app.core.openstack-service-api.glance', { $provide.value('horizon.app.core.openstack-service-api.glance', {
getImages: function() { getImages: function() {
@ -417,7 +466,7 @@
}); });
it('should default config_drive to false if setting not provided', function() { 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); model.initialize(true);
scope.$apply(); scope.$apply();
@ -503,6 +552,188 @@
expect(model.newInstanceSpec.networks.length).toBe(1); expect(model.newInstanceSpec.networks.length).toBe(1);
expect(model.newInstanceSpec.networks).toEqual(networks); 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() { describe('Post Initialization Model - Initializing', function() {

View File

@ -34,7 +34,8 @@
IMAGE: 'image', IMAGE: 'image',
INSTANCE_SNAPSHOT: 'snapshot', INSTANCE_SNAPSHOT: 'snapshot',
VOLUME: 'volume', 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', .constant('horizon.dashboard.project.workflow.launch-instance.non_bootable_image_types',
['aki', 'ari']) ['aki', 'ari'])

View File

@ -71,26 +71,23 @@
/* /*
* Boot Sources * 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; ctrl.updateBootSourceSelection = updateBootSourceSelection;
var selection = ctrl.selection = $scope.model.newInstanceSpec.source;
/* /*
* Transfer table * Transfer table
*/ */
ctrl.tableHeadCells = []; ctrl.tableHeadCells = [];
ctrl.tableBodyCells = []; ctrl.tableBodyCells = [];
ctrl.tableData = {}; ctrl.tableData = {
available: [],
allocated: selection,
displayedAvailable: [],
displayedAllocated: []
};
ctrl.helpText = {}; ctrl.helpText = {};
ctrl.sourceDetails = basePath + 'source/source-details.html'; ctrl.sourceDetails = basePath + 'source/source-details.html';
var selection = ctrl.selection = $scope.model.newInstanceSpec.source;
var bootSources = { var bootSources = {
image: { image: {
available: $scope.model.images, 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() { $scope.$on('$destroy', function() {
allowedBootSourcesWatcher();
newSpecWatcher(); newSpecWatcher();
allocatedWatcher(); allocatedWatcher();
bootSourceWatcher(); bootSourceWatcher();
@ -383,14 +397,6 @@
snapshotWatcher(); 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) { function updateBootSourceSelection(selectedSource) {
@ -505,8 +511,11 @@
var pre = findSourceById($scope.model.images, id); var pre = findSourceById($scope.model.images, id);
if (pre) { if (pre) {
changeBootSource(bootSourceTypes.IMAGE, [pre]); changeBootSource(bootSourceTypes.IMAGE, [pre]);
$scope.model.newInstanceSpec.source_type = ctrl.bootSourcesOptions[0]; $scope.model.newInstanceSpec.source_type = {
ctrl.currentBootSource = ctrl.bootSourcesOptions[0].type; type: bootSourceTypes.IMAGE,
label: gettext('Image')
};
ctrl.currentBootSource = bootSourceTypes.IMAGE;
} }
} }
@ -514,8 +523,11 @@
var pre = findSourceById($scope.model.volumes, id); var pre = findSourceById($scope.model.volumes, id);
if (pre) { if (pre) {
changeBootSource(bootSourceTypes.VOLUME, [pre]); changeBootSource(bootSourceTypes.VOLUME, [pre]);
$scope.model.newInstanceSpec.source_type = ctrl.bootSourcesOptions[2]; $scope.model.newInstanceSpec.source_type = {
ctrl.currentBootSource = ctrl.bootSourcesOptions[2].type; type: bootSourceTypes.VOLUME,
label: gettext('Volume')
};
ctrl.currentBootSource = bootSourceTypes.VOLUME;
} }
} }
@ -523,8 +535,11 @@
var pre = findSourceById($scope.model.volumeSnapshots, id); var pre = findSourceById($scope.model.volumeSnapshots, id);
if (pre) { if (pre) {
changeBootSource(bootSourceTypes.VOLUME_SNAPSHOT, [pre]); changeBootSource(bootSourceTypes.VOLUME_SNAPSHOT, [pre]);
$scope.model.newInstanceSpec.source_type = ctrl.bootSourcesOptions[3]; $scope.model.newInstanceSpec.source_type = {
ctrl.currentBootSource = ctrl.bootSourcesOptions[3].type; type: bootSourceTypes.VOLUME_SNAPSHOT,
label: gettext('Snapshot')
};
ctrl.currentBootSource = bootSourceTypes.VOLUME_SNAPSHOT;
} }
} }
} }

View File

@ -49,6 +49,7 @@
scope.initPromise = deferred.promise; scope.initPromise = deferred.promise;
scope.model = { scope.model = {
allowedBootSources: [{type: 'image', label: 'Image'}],
newInstanceSpec: { source: [], source_type: '' }, newInstanceSpec: { source: [], source_type: '' },
images: [ { id: 'image-1' }, { id: 'image-2' } ], images: [ { id: 'image-1' }, { id: 'image-2' } ],
imageSnapshots: [], imageSnapshots: [],
@ -78,18 +79,6 @@
expect(ctrl.volumeSizeError).toBeDefined(); 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() { it('initializes transfer table variables', function() {
// NOTE: these are set by the default, not the initial values. // NOTE: these are set by the default, not the initial values.
// Arguably we shouldn't even set the original values. // Arguably we shouldn't even set the original values.
@ -214,14 +203,25 @@
}); });
describe('Scope Functions', function() { describe('Scope Functions', function() {
describe('watches', function() {
describe('watchers', function () { beforeEach( function() {
it('establishes five watches', function() { // Initialize the watchers with default data
expect(scope.$watch.calls.count()).toBe(6); scope.model.newInstanceSpec.source_type = null;
scope.model.allowedBootSources = [{type: 'test_type', label: 'test'}];
scope.$apply();
}); });
it("establishes seven watches", function () {
it("establishes two watch collections", function () { // Count calls to $watch (note: $watchCollection
expect(scope.$watchCollection.calls.count()).toBe(3); // 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');
}); });
}); });

View File

@ -1,4 +1,5 @@
<div ng-controller="LaunchInstanceSourceController as ctrl"> <div ng-controller="LaunchInstanceSourceController as ctrl">
<div ng-show="model.allowedBootSources.length > 0">
<p class="step-description" translate> <p class="step-description" translate>
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). 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. You can also choose to use persistent storage by creating a new volume.
@ -9,7 +10,7 @@
<div class="form-group" ng-class="{ 'has-error': launchInstanceSourceForm.boot-source-type.$invalid }"> <div class="form-group" ng-class="{ 'has-error': launchInstanceSourceForm.boot-source-type.$invalid }">
<label for="boot-source-type" class="control-label" translate>Select Boot Source</label> <label for="boot-source-type" class="control-label" translate>Select Boot Source</label>
<select name="boot-source-type" class="form-control" id="boot-source-type" <select name="boot-source-type" class="form-control" id="boot-source-type"
ng-options="src.label for src in ctrl.bootSourcesOptions track by src.type" ng-options="src.label for src in model.allowedBootSources| orderBy:'label' track by src.type"
ng-change="ctrl.updateBootSourceSelection(model.newInstanceSpec.source_type.type)" ng-change="ctrl.updateBootSourceSelection(model.newInstanceSpec.source_type.type)"
ng-model="model.newInstanceSpec.source_type"> ng-model="model.newInstanceSpec.source_type">
</select> </select>
@ -284,5 +285,10 @@
</hz-magic-search-context> </hz-magic-search-context>
</available> </available>
</transfer-table> </transfer-table>
</div>
<div ng-if="model.allowedBootSources.length === 0">
<div translate class="subtitle text-danger">There are no allowed boot sources.
If you think this is wrong please contact your administrator.
</div>
</div>
</div> </div>

View File

@ -241,6 +241,10 @@ OPENSTACK_KEYSTONE_BACKEND = {
#LAUNCH_INSTANCE_DEFAULTS = { #LAUNCH_INSTANCE_DEFAULTS = {
# 'config_drive': False, # 'config_drive': False,
# 'enable_scheduler_hints': True # '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 # The Xen Hypervisor has the ability to set the mount point for volumes

View File

@ -0,0 +1,8 @@
---
features:
- >
[`blueprint configurable-boot-sources <https://blueprints.launchpad.net/horizon/+spec/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).