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)
|
.. 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``
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
@ -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'])
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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