581 lines
19 KiB
JavaScript
581 lines
19 KiB
JavaScript
/*
|
|
* Copyright 2015 IBM Corp.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
/**
|
|
* @ngdoc controller
|
|
* @name LaunchInstanceSourceController
|
|
* @description
|
|
* The `LaunchInstanceSourceController` controller provides functions for
|
|
* configuring the source step of the Launch Instance Wizard.
|
|
*
|
|
*/
|
|
var push = [].push;
|
|
|
|
angular
|
|
.module('horizon.dashboard.project.workflow.launch-instance')
|
|
.controller('LaunchInstanceSourceController', LaunchInstanceSourceController);
|
|
|
|
LaunchInstanceSourceController.$inject = [
|
|
'$scope',
|
|
'horizon.dashboard.project.workflow.launch-instance.boot-source-types',
|
|
'bytesFilter',
|
|
'dateFilter',
|
|
'decodeFilter',
|
|
'diskFormatFilter',
|
|
'gbFilter',
|
|
'horizon.dashboard.project.workflow.launch-instance.basePath',
|
|
'horizon.framework.widgets.transfer-table.events',
|
|
'horizon.framework.widgets.magic-search.events'
|
|
];
|
|
|
|
function LaunchInstanceSourceController($scope,
|
|
bootSourceTypes,
|
|
bytesFilter,
|
|
dateFilter,
|
|
decodeFilter,
|
|
diskFormatFilter,
|
|
gbFilter,
|
|
basePath,
|
|
events,
|
|
magicSearchEvents
|
|
) {
|
|
|
|
var ctrl = this;
|
|
|
|
// Error text for invalid fields
|
|
/*eslint-disable max-len */
|
|
ctrl.bootSourceTypeError = gettext('Volumes can only be attached to 1 active instance at a time. Please either set your instance count to 1 or select a different source type.');
|
|
/*eslint-enable max-len */
|
|
|
|
// toggle button label/value defaults
|
|
ctrl.toggleButtonOptions = [
|
|
{ label: gettext('Yes'), value: true },
|
|
{ label: gettext('No'), value: false }
|
|
];
|
|
|
|
/*
|
|
* Boot Sources
|
|
*/
|
|
ctrl.updateBootSourceSelection = updateBootSourceSelection;
|
|
var selection = ctrl.selection = $scope.model.newInstanceSpec.source;
|
|
|
|
/*
|
|
* Transfer table
|
|
*/
|
|
ctrl.tableHeadCells = [];
|
|
ctrl.tableBodyCells = [];
|
|
ctrl.tableData = {
|
|
available: [],
|
|
allocated: selection,
|
|
displayedAvailable: [],
|
|
displayedAllocated: []
|
|
};
|
|
ctrl.helpText = {};
|
|
ctrl.sourceDetails = basePath + 'source/source-details.html';
|
|
|
|
var bootSources = {
|
|
image: {
|
|
available: $scope.model.images,
|
|
allocated: selection,
|
|
displayedAvailable: $scope.model.images,
|
|
displayedAllocated: selection
|
|
},
|
|
snapshot: {
|
|
available: $scope.model.imageSnapshots,
|
|
allocated: selection,
|
|
displayedAvailable: [],
|
|
displayedAllocated: selection
|
|
},
|
|
volume: {
|
|
available: $scope.model.volumes,
|
|
allocated: selection,
|
|
displayedAvailable: [],
|
|
displayedAllocated: selection
|
|
},
|
|
volume_snapshot: {
|
|
available: $scope.model.volumeSnapshots,
|
|
allocated: selection,
|
|
displayedAvailable: [],
|
|
displayedAllocated: selection
|
|
}
|
|
};
|
|
|
|
var diskFormats = [
|
|
{ label: gettext('AKI'), key: 'aki' },
|
|
{ label: gettext('AMI'), key: 'ami' },
|
|
{ label: gettext('ARI'), key: 'ari' },
|
|
{ label: gettext('Docker'), key: 'docker' },
|
|
{ label: gettext('ISO'), key: 'iso' },
|
|
{ label: gettext('OVA'), key: 'ova' },
|
|
{ label: gettext('QCOW2'), key: 'qcow2' },
|
|
{ label: gettext('RAW'), key: 'raw' },
|
|
{ label: gettext('VDI'), key: 'vdi' },
|
|
{ label: gettext('VHD'), key: 'vhd' },
|
|
{ label: gettext('VMDK'), key: 'vmdk' }
|
|
];
|
|
|
|
// Mapping for dynamic table headers
|
|
var tableHeadCellsMap = {
|
|
image: [
|
|
{ text: gettext('Name'), sortable: true, sortDefault: true },
|
|
{ text: gettext('Updated'), sortable: true },
|
|
{ text: gettext('Size'), classList: ['number'], sortable: true },
|
|
{ text: gettext('Type'), sortable: true },
|
|
{ text: gettext('Visibility'), sortable: true }
|
|
],
|
|
snapshot: [
|
|
{ text: gettext('Name'), sortable: true, sortDefault: true },
|
|
{ text: gettext('Updated'), sortable: true },
|
|
{ text: gettext('Size'), classList: ['number'], sortable: true },
|
|
{ text: gettext('Type'), sortable: true },
|
|
{ text: gettext('Visibility'), sortable: true }
|
|
],
|
|
volume: [
|
|
{ text: gettext('Name'), sortable: true, sortDefault: true },
|
|
{ text: gettext('Description'), sortable: true },
|
|
{ text: gettext('Size'), classList: ['number'], sortable: true },
|
|
{ text: gettext('Type'), sortable: true },
|
|
{ text: gettext('Availability Zone'), sortable: true }
|
|
],
|
|
volume_snapshot: [
|
|
{ text: gettext('Name'), sortable: true, sortDefault: true },
|
|
{ text: gettext('Description'), sortable: true },
|
|
{ text: gettext('Size'), classList: ['number'], sortable: true },
|
|
{ text: gettext('Created'), sortable: true },
|
|
{ text: gettext('Status'), sortable: true }
|
|
]
|
|
};
|
|
|
|
// Map Visibility data so we can decode true/false to Public/Private
|
|
var _visibilitymap = { true: gettext('Public'), false: gettext('Private') };
|
|
|
|
// Mapping for dynamic table data
|
|
var tableBodyCellsMap = {
|
|
image: [
|
|
{ key: 'name', classList: ['hi-light', 'word-break'] },
|
|
{ key: 'updated_at', filter: dateFilter, filterArg: 'short' },
|
|
{ key: 'size', filter: bytesFilter, classList: ['number'] },
|
|
{ key: 'disk_format', filter: diskFormatFilter, filterRawData: true },
|
|
{ key: 'is_public', filter: decodeFilter, filterArg: _visibilitymap }
|
|
],
|
|
snapshot: [
|
|
{ key: 'name', classList: ['hi-light', 'word-break'] },
|
|
{ key: 'updated_at', filter: dateFilter, filterArg: 'short' },
|
|
{ key: 'size', filter: bytesFilter, classList: ['number'] },
|
|
{ key: 'disk_format', filter: diskFormatFilter, filterRawData: true },
|
|
{ key: 'is_public', filter: decodeFilter, filterArg: _visibilitymap }
|
|
],
|
|
volume: [
|
|
{ key: 'name', classList: ['hi-light', 'word-break'] },
|
|
{ key: 'description' },
|
|
{ key: 'size', filter: gbFilter, classList: ['number'] },
|
|
{ key: 'volume_image_metadata', filter: diskFormatFilter },
|
|
{ key: 'availability_zone' }
|
|
],
|
|
volume_snapshot: [
|
|
{ key: 'name', classList: ['hi-light', 'word-break'] },
|
|
{ key: 'description' },
|
|
{ key: 'size', filter: gbFilter, classList: ['number'] },
|
|
{ key: 'created_at', filter: dateFilter, filterArg: 'short' },
|
|
{ key: 'status' }
|
|
]
|
|
};
|
|
|
|
/**
|
|
* Filtering - client-side MagicSearch
|
|
*/
|
|
ctrl.sourceFacets = [];
|
|
|
|
// All facets for source step
|
|
var facets = {
|
|
created: {
|
|
label: gettext('Created'),
|
|
name: 'created_at',
|
|
singleton: true
|
|
},
|
|
description: {
|
|
label: gettext('Description'),
|
|
name: 'description',
|
|
singleton: true
|
|
},
|
|
encrypted: {
|
|
label: gettext('Encrypted'),
|
|
name: 'encrypted',
|
|
singleton: true,
|
|
options: [
|
|
{ label: gettext('Yes'), key: 'true' },
|
|
{ label: gettext('No'), key: 'false' }
|
|
]
|
|
},
|
|
name: {
|
|
label: gettext('Name'),
|
|
name: 'name',
|
|
singleton: true
|
|
},
|
|
size: {
|
|
label: gettext('Size'),
|
|
name: 'size',
|
|
singleton: true
|
|
},
|
|
status: {
|
|
label: gettext('Status'),
|
|
name: 'status',
|
|
singleton: true,
|
|
options: [
|
|
{ label: gettext('Available'), key: 'available' },
|
|
{ label: gettext('Creating'), key: 'creating' },
|
|
{ label: gettext('Deleting'), key: 'deleting' },
|
|
{ label: gettext('Error'), key: 'error' },
|
|
{ label: gettext('Error Deleting'), key: 'error_deleting' }
|
|
]
|
|
},
|
|
type: {
|
|
label: gettext('Type'),
|
|
name: 'disk_format',
|
|
singleton: true,
|
|
options: diskFormats
|
|
},
|
|
updated: {
|
|
label: gettext('Updated'),
|
|
name: 'updated_at',
|
|
singleton: true
|
|
},
|
|
visibility: {
|
|
label: gettext('Visibility'),
|
|
name: 'is_public',
|
|
singleton: true,
|
|
options: [
|
|
{ label: gettext('Public'), key: 'true' },
|
|
{ label: gettext('Private'), key: 'false' }
|
|
]
|
|
},
|
|
volumeType: {
|
|
label: gettext('Type'),
|
|
name: 'volume_image_metadata.disk_format',
|
|
singleton: true,
|
|
options: diskFormats
|
|
}
|
|
};
|
|
|
|
// Mapping for filter facets based on boot source type
|
|
var sourceTypeFacets = {
|
|
image: [
|
|
facets.name, facets.updated, facets.size, facets.type, facets.visibility
|
|
],
|
|
snapshot: [
|
|
facets.name, facets.updated, facets.size, facets.type, facets.visibility
|
|
],
|
|
volume: [
|
|
facets.name, facets.description, facets.size, facets.volumeType, facets.encrypted
|
|
],
|
|
volume_snapshot: [
|
|
facets.name, facets.description, facets.size, facets.created, facets.status
|
|
]
|
|
};
|
|
|
|
var newSpecWatcher = $scope.$watch(
|
|
function () {
|
|
return $scope.model.newInstanceSpec.instance_count;
|
|
},
|
|
function (newValue, oldValue) {
|
|
if (newValue !== oldValue) {
|
|
validateBootSourceType();
|
|
}
|
|
}
|
|
);
|
|
|
|
var allocatedWatcher = $scope.$watch(
|
|
function () {
|
|
return ctrl.tableData.allocated.length;
|
|
},
|
|
function (newValue) {
|
|
checkVolumeForImage(newValue);
|
|
}
|
|
);
|
|
|
|
// Since available transfer table for Launch Instance Source step is
|
|
// dynamically selected based on Boot Source, we need to update the
|
|
// model here accordingly. Otherwise it will only calculate the items
|
|
// available based on the original selection Boot Source: Image.
|
|
var bootSourceWatcher = $scope.$watch(
|
|
function getBootSource() {
|
|
return ctrl.currentBootSource;
|
|
},
|
|
function onBootSourceChange(newValue, oldValue) {
|
|
if (newValue !== oldValue) {
|
|
$scope.$broadcast(events.AVAIL_CHANGED, {
|
|
'data': bootSources[newValue]
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
var imagesWatcher = $scope.$watchCollection(
|
|
function getImages() {
|
|
return $scope.model.images;
|
|
},
|
|
function onImagesChange() {
|
|
$scope.initPromise.then(function () {
|
|
$scope.$applyAsync(function () {
|
|
if ($scope.launchContext.imageId) {
|
|
setSourceImageWithId($scope.launchContext.imageId);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
);
|
|
|
|
var imageSnapshotsWatcher = $scope.$watchCollection(
|
|
function getImageSnapshots() {
|
|
return $scope.model.imageSnapshots;
|
|
},
|
|
function onImageSnapshotsChange() {
|
|
$scope.initPromise.then(function () {
|
|
$scope.$applyAsync(function () {
|
|
if ($scope.launchContext.imageId) {
|
|
setSourceImageSnapshotWithId($scope.launchContext.imageId);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
);
|
|
|
|
var volumeWatcher = $scope.$watchCollection(
|
|
function getVolumes() {
|
|
return $scope.model.volumes;
|
|
},
|
|
function onVolumesChange() {
|
|
$scope.initPromise.then(function onInit() {
|
|
$scope.$applyAsync(function setDefaultVolume() {
|
|
if ($scope.launchContext.volumeId) {
|
|
setSourceVolumeWithId($scope.launchContext.volumeId);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
);
|
|
|
|
var snapshotWatcher = $scope.$watchCollection(
|
|
function getSnapshots() {
|
|
return $scope.model.volumeSnapshots;
|
|
},
|
|
function onSnapshotsChange() {
|
|
$scope.initPromise.then(function onInit() {
|
|
$scope.$applyAsync(function setDefaultSnapshot() {
|
|
if ($scope.launchContext.snapshotId) {
|
|
setSourceSnapshotWithId($scope.launchContext.snapshotId);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
);
|
|
|
|
// 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();
|
|
imagesWatcher();
|
|
imageSnapshotsWatcher();
|
|
volumeWatcher();
|
|
snapshotWatcher();
|
|
});
|
|
|
|
////////////////////
|
|
|
|
function updateBootSourceSelection(selectedSource) {
|
|
ctrl.currentBootSource = selectedSource;
|
|
if ((selectedSource === bootSourceTypes.IMAGE ||
|
|
selectedSource === bootSourceTypes.INSTANCE_SNAPSHOT) && $scope.model.volumeBootable) {
|
|
$scope.model.newInstanceSpec.vol_create =
|
|
$scope.model.newInstanceSpec.create_volume_default;
|
|
} else {
|
|
$scope.model.newInstanceSpec.vol_create = false;
|
|
}
|
|
$scope.model.newInstanceSpec.vol_delete_on_instance_delete = false;
|
|
changeBootSource(selectedSource);
|
|
validateBootSourceType();
|
|
}
|
|
|
|
// Dynamically update page based on boot source selection
|
|
function changeBootSource(key, preSelection) {
|
|
updateDataSource(key, preSelection);
|
|
updateHelpText(key);
|
|
updateTableHeadCells(key);
|
|
updateTableBodyCells(key);
|
|
updateFacets(key);
|
|
}
|
|
|
|
function updateDataSource(key, preSelection) {
|
|
selection.length = 0;
|
|
if (preSelection) {
|
|
push.apply(selection, preSelection);
|
|
}
|
|
angular.extend(ctrl.tableData, bootSources[key]);
|
|
}
|
|
|
|
function updateHelpText() {
|
|
angular.extend(ctrl.helpText, {
|
|
noneAllocText: gettext('Select a source from those listed below.'),
|
|
availHelpText: gettext('Select one'),
|
|
/*eslint-disable max-len */
|
|
volumeAZHelpText: gettext('When selecting volume as boot source, please ensure the instance\'s availability zone is compatible with your volume\'s availability zone.')
|
|
/*eslint-enable max-len */
|
|
});
|
|
}
|
|
|
|
function updateTableHeadCells(key) {
|
|
refillArray(ctrl.tableHeadCells, tableHeadCellsMap[key]);
|
|
}
|
|
|
|
function updateTableBodyCells(key) {
|
|
refillArray(ctrl.tableBodyCells, tableBodyCellsMap[key]);
|
|
}
|
|
|
|
function updateFacets(key) {
|
|
refillArray(ctrl.sourceFacets, sourceTypeFacets[key]);
|
|
$scope.$broadcast(magicSearchEvents.FACETS_CHANGED);
|
|
}
|
|
|
|
function refillArray(arrayToRefill, contentArray) {
|
|
arrayToRefill.length = 0;
|
|
Array.prototype.push.apply(arrayToRefill, contentArray);
|
|
}
|
|
|
|
/*
|
|
* Validation
|
|
*/
|
|
|
|
/*
|
|
* If boot source type is 'image' and 'Create New Volume' is checked, set the minimum volume
|
|
* size for validating vol_size field
|
|
*/
|
|
function checkVolumeForImage() {
|
|
var source = selection[0];
|
|
|
|
if (source && ctrl.currentBootSource === bootSourceTypes.IMAGE) {
|
|
var imageGb = source.size * 1e-9;
|
|
var imageDisk = source.min_disk;
|
|
ctrl.minVolumeSize = Math.ceil(Math.max(imageGb, imageDisk));
|
|
if ($scope.model.newInstanceSpec.vol_size < ctrl.minVolumeSize) {
|
|
$scope.model.newInstanceSpec.vol_size = ctrl.minVolumeSize;
|
|
}
|
|
var volumeSizeText = gettext('The volume size must be at least %(minVolumeSize)s GB');
|
|
var volumeSizeObj = { minVolumeSize: ctrl.minVolumeSize };
|
|
ctrl.volumeSizeError = interpolate(volumeSizeText, volumeSizeObj, true);
|
|
} else {
|
|
ctrl.minVolumeSize = 0;
|
|
ctrl.volumeSizeError = gettext('Volume size is required and must be an integer');
|
|
}
|
|
}
|
|
|
|
// Validator for boot source type. Instance count must to be 1 if volume selected
|
|
function validateBootSourceType() {
|
|
var bootSourceType = ctrl.currentBootSource;
|
|
var instanceCount = $scope.model.newInstanceSpec.instance_count;
|
|
|
|
/*
|
|
* Field is valid if boot source type is not volume, instance count is blank/undefined
|
|
* (this is an error with instance count) or instance count is 1
|
|
*/
|
|
var isValid = bootSourceType !== bootSourceTypes.VOLUME ||
|
|
!instanceCount ||
|
|
instanceCount === 1;
|
|
|
|
$scope.launchInstanceSourceForm['boot-source-type']
|
|
.$setValidity('bootSourceType', isValid);
|
|
}
|
|
|
|
function findSourceById(sources, id) {
|
|
var len = sources.length;
|
|
var source;
|
|
for (var i = 0; i < len; i++) {
|
|
source = sources[i];
|
|
if (source.id === id) {
|
|
return source;
|
|
}
|
|
}
|
|
}
|
|
|
|
function setSourceImageWithId(id) {
|
|
var pre = findSourceById($scope.model.images, id);
|
|
if (pre) {
|
|
changeBootSource(bootSourceTypes.IMAGE, [pre]);
|
|
$scope.model.newInstanceSpec.source_type = {
|
|
type: bootSourceTypes.IMAGE,
|
|
label: gettext('Image')
|
|
};
|
|
ctrl.currentBootSource = bootSourceTypes.IMAGE;
|
|
}
|
|
}
|
|
|
|
function setSourceImageSnapshotWithId(id) {
|
|
var pre = findSourceById($scope.model.imageSnapshots, id);
|
|
if (pre) {
|
|
changeBootSource(bootSourceTypes.INSTANCE_SNAPSHOT, [pre]);
|
|
$scope.model.newInstanceSpec.source_type = {
|
|
type: bootSourceTypes.INSTANCE_SNAPSHOT,
|
|
label: gettext('Snapshot')
|
|
};
|
|
ctrl.currentBootSource = bootSourceTypes.INSTANCE_SNAPSHOT;
|
|
}
|
|
}
|
|
|
|
function setSourceVolumeWithId(id) {
|
|
var pre = findSourceById($scope.model.volumes, id);
|
|
if (pre) {
|
|
changeBootSource(bootSourceTypes.VOLUME, [pre]);
|
|
$scope.model.newInstanceSpec.source_type = {
|
|
type: bootSourceTypes.VOLUME,
|
|
label: gettext('Volume')
|
|
};
|
|
ctrl.currentBootSource = bootSourceTypes.VOLUME;
|
|
}
|
|
}
|
|
|
|
function setSourceSnapshotWithId(id) {
|
|
var pre = findSourceById($scope.model.volumeSnapshots, id);
|
|
if (pre) {
|
|
changeBootSource(bootSourceTypes.VOLUME_SNAPSHOT, [pre]);
|
|
$scope.model.newInstanceSpec.source_type = {
|
|
type: bootSourceTypes.VOLUME_SNAPSHOT,
|
|
label: gettext('Snapshot')
|
|
};
|
|
ctrl.currentBootSource = bootSourceTypes.VOLUME_SNAPSHOT;
|
|
}
|
|
}
|
|
}
|
|
})();
|