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:
parent
526e34839d
commit
4a9f988813
@ -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``
|
||||
------------------------------
|
||||
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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'])
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
<div ng-controller="LaunchInstanceSourceController as ctrl">
|
||||
<div ng-show="model.allowedBootSources.length > 0">
|
||||
<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).
|
||||
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 }">
|
||||
<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"
|
||||
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-model="model.newInstanceSpec.source_type">
|
||||
</select>
|
||||
@ -284,5 +285,10 @@
|
||||
</hz-magic-search-context>
|
||||
</available>
|
||||
</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>
|
||||
|
@ -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
|
||||
|
@ -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).
|
Loading…
Reference in New Issue
Block a user