/*
* Copyright 2015 Mirantis, Inc.
*
* 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.
**/
import $ from 'jquery';
import _ from 'underscore';
import i18n from 'i18n';
import React from 'react';
import ReactDOM from 'react-dom';
import utils from 'utils';
import dispatcher from 'dispatcher';
import {Input, ProgressBar, Tooltip} from 'views/controls';
import {
DiscardNodeChangesDialog, DeployChangesDialog, ProvisionVMsDialog,
RemoveClusterDialog, ResetEnvironmentDialog, StopDeploymentDialog
} from 'views/dialogs';
import {backboneMixin, pollingMixin, renamingMixin} from 'component_mixins';
var namespace = 'cluster_page.dashboard_tab.';
var DashboardTab = React.createClass({
mixins: [
// this is needed to somehow handle the case when verification
// is in progress and user pressed Deploy
backboneMixin({
modelOrCollection: (props) => props.cluster.get('tasks'),
renderOn: 'update change'
}),
backboneMixin({
modelOrCollection: (props) => props.cluster.get('nodes'),
renderOn: 'update change'
}),
backboneMixin({
modelOrCollection: (props) => props.cluster.get('pluginLinks'),
renderOn: 'update change'
}),
backboneMixin('cluster', 'change'),
pollingMixin(20, true)
],
fetchData() {
return this.props.cluster.get('nodes').fetch();
},
render() {
var cluster = this.props.cluster;
var nodes = cluster.get('nodes');
var release = cluster.get('release');
var runningDeploymentTask = cluster.task({group: 'deployment', active: true});
var dashboardLinks = [{
url: '/',
title: i18n(namespace + 'horizon'),
description: i18n(namespace + 'horizon_description')
}].concat(cluster.get('pluginLinks').invoke('pick', 'url', 'title', 'description'));
return (
{release.get('state') == 'unavailable' &&
{i18n('cluster_page.unavailable_release', {name: release.get('name')})}
}
{cluster.get('is_customized') &&
{i18n('cluster_page.cluster_was_modified_from_cli')}
}
{runningDeploymentTask ?
:
[
cluster.task({group: 'deployment', active: false}) &&
,
cluster.get('status') == 'operational' &&
,
(nodes.hasChanges() || cluster.needsRedeployment()) &&
,
!nodes.length && (
{i18n(namespace + 'new_environment_welcome')}
)
]
}
);
}
});
var DashboardLinks = React.createClass({
renderLink(link) {
return (
1 ? 'col-xs-6' : 'col-xs-12'}
cluster={this.props.cluster}
/>
);
},
render() {
var {links} = this.props;
if (!links.length) return null;
return (
{links.map((link, index) => {
if (index % 2 == 0) return (
{this.renderLink(link)}
{index + 1 < links.length && this.renderLink(links[index + 1])}
);
}, this)}
);
}
});
var DashboardLink = React.createClass({
propTypes: {
title: React.PropTypes.string.isRequired,
url: React.PropTypes.string.isRequired,
description: React.PropTypes.node
},
processRelativeURL(url) {
var sslSettings = this.props.cluster.get('settings').get('public_ssl');
if (sslSettings.horizon.value) return 'https://' + sslSettings.hostname.value + url;
return this.getHTTPLink(url);
},
getHTTPLink(url) {
return 'http://' + this.props.cluster.get('networkConfiguration').get('public_vip') + url;
},
render() {
var isSSLEnabled = this.props.cluster.get('settings').get('public_ssl.horizon.value');
var isURLRelative = !(/^(?:https?:)?\/\//.test(this.props.url));
var url = isURLRelative ? this.processRelativeURL(this.props.url) : this.props.url;
return (
);
}
});
var DeploymentInProgressControl = React.createClass({
showDialog(Dialog) {
Dialog.show({cluster: this.props.cluster});
},
render() {
var task = this.props.task;
var taskName = task.get('name');
var isInfiniteTask = task.isInfinite();
var taskProgress = task.get('progress');
var showStopButton = task.match({name: 'deploy'});
return (
{i18n(namespace + 'current_task') + ' '}
{i18n('cluster_page.' + taskName) + '...'}
{showStopButton &&
{i18n(namespace + 'stop')}
}
);
}
});
var DeploymentResult = React.createClass({
getInitialState() {
return {collapsed: false};
},
dismissTaskResult() {
var task = this.props.cluster.task({group: 'deployment'});
if (task) task.destroy();
},
componentDidMount() {
$('.result-details', ReactDOM.findDOMNode(this))
.on('show.bs.collapse', this.setState.bind(this, {collapsed: true}, null))
.on('hide.bs.collapse', this.setState.bind(this, {collapsed: false}, null));
},
render() {
var task = this.props.cluster.task({group: 'deployment', active: false});
if (!task) return null;
var error = task.match({status: 'error'});
var delimited = task.escape('message').split('\n\n');
var summary = delimited.shift();
var details = delimited.join('\n\n');
var warning = task.match({name: ['reset_environment', 'stop_deployment']});
var classes = {
alert: true,
'alert-warning': warning,
'alert-danger': !warning && error,
'alert-success': !warning && !error
};
return (
×
{i18n('common.' + (error ? 'error' : 'success'))}
{this.state.collapsed ? i18n('cluster_page.hide_details_button') :
i18n('cluster_page.show_details_button')}
);
}
});
var DocumentationLinks = React.createClass({
renderDocumentationLinks(link, labelKey) {
return (
);
},
render() {
var isMirantisIso = _.contains(app.version.get('feature_groups'), 'mirantis');
return (
{i18n(namespace + 'documentation')}
{i18n(namespace + 'documentation_description')}
{isMirantisIso ?
[
this.renderDocumentationLinks(
'https://www.mirantis.com/openstack-documentation/',
'mos_documentation'
),
this.renderDocumentationLinks(
utils.composeDocumentationLink('plugin-dev.html#plugin-dev'),
'plugin_documentation'
),
this.renderDocumentationLinks(
'https://software.mirantis.com/mirantis-openstack-technical-bulletins/',
'technical_bulletins'
)
]
:
[
this.renderDocumentationLinks(
'http://docs.openstack.org/',
'openstack_documentation'
),
this.renderDocumentationLinks(
'https://wiki.openstack.org/wiki/Fuel/Plugins',
'plugin_documentation'
)
]
}
);
}
});
// @FIXME (morale): this component is written in a bad pattern of 'monolith' component
// it should be refactored to provide proper logics separation and decoupling
var DeployReadinessBlock = React.createClass({
mixins: [
// this is needed to somehow handle the case when verification
// is in progress and user pressed Deploy
backboneMixin({
modelOrCollection(props) {
return props.cluster.get('tasks');
},
renderOn: 'update change'
}),
backboneMixin('cluster', 'change')
],
ns: 'dialog.display_changes.',
getConfigModels() {
var {cluster} = this.props;
return {
cluster: cluster,
settings: cluster.get('settings'),
version: app.version,
release: cluster.get('release'),
default: cluster.get('settings'),
networking_parameters: cluster.get('networkConfiguration').get('networking_parameters')
};
},
validate(cluster) {
return _.reduce(
this.validations,
(accumulator, validator) => _.merge(accumulator, validator.call(this, cluster), (a, b) =>
a.concat(_.compact(b))),
{blocker: [], error: [], warning: []}
);
},
validations: [
// check if TLS settings are not configured
function(cluster) {
var sslSettings = cluster.get('settings').get('public_ssl');
if (!sslSettings.horizon.value && !sslSettings.services.value) {
return {warning: [i18n(this.ns + 'tls_not_enabled')]};
}
if (!sslSettings.horizon.value) {
return {warning: [i18n(this.ns + 'tls_for_horizon_not_enabled')]};
}
if (!sslSettings.services.value) {
return {warning: [i18n(this.ns + 'tls_for_services_not_enabled')]};
}
},
// check if deployment failed
function(cluster) {
return cluster.needsRedeployment() && {
error: [
]
};
},
// check VCenter settings
function(cluster) {
if (cluster.get('settings').get('common.use_vcenter.value')) {
var vcenter = cluster.get('vcenter');
vcenter.setModels(this.getConfigModels());
return !vcenter.isValid() && {
blocker: [
{i18n('vmware.has_errors') + ' '}
{i18n('vmware.tab_name')}
]
};
}
},
// check cluster settings
function(cluster) {
var configModels = this.getConfigModels();
var areSettingsInvalid = !cluster.get('settings').isValid({models: configModels});
return areSettingsInvalid &&
{blocker: [
{i18n(this.ns + 'invalid_settings')}
{' ' + i18n(this.ns + 'get_more_info') + ' '}
{i18n(this.ns + 'settings_link')}
.
]};
},
// check node amount restrictions according to their roles
function(cluster) {
var configModels = this.getConfigModels();
var roleModels = cluster.get('roles');
var validRoleModels = roleModels.filter((role) => {
return !role.checkRestrictions(configModels).result;
});
var limitValidations = _.zipObject(validRoleModels.map((role) => {
return [role.get('name'), role.checkLimits(configModels, cluster.get('nodes'))];
}));
var limitRecommendations = _.zipObject(validRoleModels.map((role) => {
return [role.get('name'), role.checkLimits(configModels, cluster.get('nodes'), true,
['recommended'])];
}));
return {
blocker: roleModels.map((role) => {
var name = role.get('name');
var limits = limitValidations[name];
return limits && !limits.valid && limits.message;
}),
warning: roleModels.map((role) => {
var name = role.get('name');
var recommendation = limitRecommendations[name];
return recommendation && !recommendation.valid && recommendation.message;
})
};
},
// check cluster network configuration
function(cluster) {
if (this.props.nodeNetworkGroups.where({cluster_id: cluster.id}).length > 1) return null;
var networkVerificationTask = cluster.task('verify_networks');
var makeComponent = (text, isError) => {
var span = (
{text}
{' ' + i18n(this.ns + 'get_more_info') + ' '}
{i18n(this.ns + 'networks_link')}
.
);
return isError ? {error: [span]} : {warning: [span]};
};
if (_.isUndefined(networkVerificationTask)) {
return makeComponent(i18n(this.ns + 'verification_not_performed'));
} else if (networkVerificationTask.match({status: 'error'})) {
return makeComponent(i18n(this.ns + 'verification_failed'), true);
} else if (networkVerificationTask.match({active: true})) {
return makeComponent(i18n(this.ns + 'verification_in_progress'));
}
}
],
showDialog(Dialog, options) {
Dialog.show(_.extend({cluster: this.props.cluster}, options));
},
renderChangedNodesAmount(nodes, dictKey) {
if (!nodes.length) return null;
return (
{i18n('dialog.display_changes.' + dictKey, {count: nodes.length})}
);
},
render() {
var cluster = this.props.cluster;
var nodes = cluster.get('nodes');
var alerts = this.validate(cluster);
var isDeploymentPossible = cluster.isDeploymentPossible() && !alerts.blocker.length;
var isVMsProvisioningAvailable = nodes.any((node) => {
return node.get('pending_addition') && node.hasRole('virt');
});
return (
{nodes.hasChanges() &&
{i18n(namespace + 'changes_header')}
{this.renderChangedNodesAmount(
nodes.where({pending_addition: true}),
'added_node'
)}
{this.renderChangedNodesAmount(
nodes.where({status: 'provisioned'}),
'provisioned_node'
)}
{this.renderChangedNodesAmount(
nodes.where({pending_deletion: true}),
'deleted_node'
)}
}
{isVMsProvisioningAvailable ?
{i18n('cluster_page.provision_vms')}
:
{i18n('cluster_page.deploy_changes')}
}
{_.map(['blocker', 'error', 'warning'],
(severity) =>
)}
);
}
});
var WarningsBlock = React.createClass({
ns: 'dialog.display_changes.',
render() {
if (_.isEmpty(this.props.alerts)) return null;
var className = this.props.severity == 'warning' ? 'warning' : 'danger';
return (
{this.props.severity == 'blocker' &&
}
{_.map(this.props.alerts, (alert, index) => {
return {alert} ;
}, this)}
);
}
});
var ClusterInfo = React.createClass({
mixins: [renamingMixin('clustername')],
getClusterValue(fieldName) {
var cluster = this.props.cluster;
var settings = cluster.get('settings');
switch (fieldName) {
case 'status':
return i18n('cluster.status.' + cluster.get('status'));
case 'openstack_release':
return cluster.get('release').get('name');
case 'compute':
var libvirtSettings = settings.get('common').libvirt_type;
var computeLabel = _.find(libvirtSettings.values, {data: libvirtSettings.value}).label;
if (settings.get('common').use_vcenter.value) {
return computeLabel + ' ' + i18n(namespace + 'and_vcenter');
}
return computeLabel;
case 'network':
var networkingParameters = cluster.get('networkConfiguration').get('networking_parameters');
if (cluster.get('net_provider') == 'nova_network') {
return i18n(namespace + 'nova_with') + ' ' + networkingParameters.get('net_manager');
}
return (i18n('common.network.neutron_' + networkingParameters.get('segmentation_type')));
case 'storage_backends':
return _.map(_.where(settings.get('storage'), {value: true}), 'label') ||
i18n(namespace + 'no_storage_enabled');
default:
return cluster.get(fieldName);
}
},
renderClusterInfoFields() {
return (
_.map(['status', 'openstack_release', 'compute', 'network', 'storage_backends'], (field) => {
var value = this.getClusterValue(field);
return (
{i18n(namespace + 'cluster_info_fields.' + field)}
{_.isArray(value) ? value.map((line) =>
{line}
) :
{value}
}
);
}, this)
);
},
renderClusterCapacity() {
var cores = 0;
var hdds = 0;
var ram = 0;
var ns = namespace + 'cluster_info_fields.';
this.props.cluster.get('nodes').each((node) => {
cores += node.resource('ht_cores');
hdds += node.resource('hdd');
ram += node.resource('ram');
}, this);
return (
{i18n(ns + 'capacity')}
{i18n(ns + 'cpu_cores')}
{cores}
{i18n(ns + 'hdd')}
{utils.showDiskSize(hdds)}
{i18n(ns + 'ram')}
{utils.showDiskSize(ram)}
);
},
getNumberOfNodesWithRole(field) {
var nodes = this.props.cluster.get('nodes');
if (!nodes.length) return 0;
if (field == 'total') return nodes.length;
return _.filter(nodes.invoke('hasRole', field)).length;
},
getNumberOfNodesWithStatus(field) {
var nodes = this.props.cluster.get('nodes');
if (!nodes.length) return 0;
switch (field) {
case 'offline':
return nodes.where({online: false}).length;
case 'error':
return nodes.where({status: 'error'}).length;
case 'pending_addition':
case 'pending_deletion':
var searchObject = {};
searchObject[field] = true;
return nodes.where(searchObject).length;
default:
return nodes.where({status: field}).length;
}
},
renderLegend(fieldsData, isRole) {
var result = _.map(fieldsData, (field) => {
var numberOfNodes = isRole ? this.getNumberOfNodesWithRole(field) :
this.getNumberOfNodesWithStatus(field);
return numberOfNodes ?
{isRole && field != 'total' ?
this.props.cluster.get('roles').find({name: field}).get('label')
:
field == 'total' ?
i18n(namespace + 'cluster_info_fields.total')
:
i18n('cluster_page.nodes_tab.node.status.' + field,
{os: this.props.cluster.get('release').get('operating_system') || 'OS'})
}
:
null;
});
return result;
},
renderStatistics() {
var hasNodes = !!this.props.cluster.get('nodes').length;
var fieldRoles = _.union(['total'], this.props.cluster.get('roles').pluck('name'));
var fieldStatuses = [
'offline', 'error', 'pending_addition', 'pending_deletion', 'ready',
'provisioned', 'provisioning', 'deploying', 'removing'
];
return (
{i18n(namespace + 'cluster_info_fields.statistics')}
{hasNodes ?
[
{this.renderLegend(fieldRoles, true)}
,
{this.renderLegend(fieldStatuses)}
]
:
{i18n(namespace + 'no_nodes_warning_add_them')}
}
);
},
render() {
var cluster = this.props.cluster;
return (
{i18n(namespace + 'summary')}
{i18n(namespace + 'cluster_info_fields.name')}
{this.state.isRenaming ?
:
{cluster.get('name')}
}
{this.renderClusterInfoFields()}
{(cluster.get('status') == 'operational') &&
}
{this.renderClusterCapacity()}
{this.renderStatistics()}
);
}
});
var AddNodesButton = React.createClass({
render() {
var disabled = !!this.props.cluster.task({group: 'deployment', active: true});
return (
{i18n(namespace + 'go_to_nodes')}
);
}
});
var RenameEnvironmentAction = React.createClass({
applyAction(e) {
e.preventDefault();
var cluster = this.props.cluster;
var name = this.state.name;
if (name != cluster.get('name')) {
var deferred = cluster.save({name: name}, {patch: true, wait: true});
if (deferred) {
this.setState({disabled: true});
deferred
.fail((response) => {
if (response.status == 409) {
this.setState({error: utils.getResponseText(response)});
} else {
utils.showErrorDialog({
title: i18n(namespace + 'rename_error.title'),
response: response
});
}
})
.done(() => {
dispatcher.trigger('updatePageLayout');
})
.always(() => {
this.setState({disabled: false});
if (!(this.state && this.state.error)) this.props.endRenaming();
});
} else if (cluster.validationError) {
this.setState({error: cluster.validationError.name});
}
} else {
this.props.endRenaming();
}
},
getInitialState() {
return {
name: this.props.cluster.get('name'),
disabled: false,
error: ''
};
},
onChange(inputName, newValue) {
this.setState({
name: newValue,
error: ''
});
},
handleKeyDown(e) {
if (e.key == 'Enter') {
e.preventDefault();
this.applyAction(e);
}
if (e.key == 'Escape') {
e.preventDefault();
this.props.endRenaming();
}
},
render() {
var classes = {
'rename-block': true,
'has-error': !!this.state.error
};
return (
{this.state.error &&
{this.state.error}
}
);
}
});
var ResetEnvironmentAction = React.createClass({
mixins: [
backboneMixin('cluster'),
backboneMixin('task')
],
getDescriptionKey() {
if (this.props.task) {
if (this.props.task.match({name: 'reset_environment'})) return 'repeated_reset_disabled';
return 'reset_disabled_for_deploying_cluster';
}
if (this.props.cluster.get('status') == 'new') return 'no_changes_to_reset';
return 'reset_environment_description';
},
applyAction(e) {
e.preventDefault();
ResetEnvironmentDialog.show({cluster: this.props.cluster});
},
render() {
var isLocked = this.props.cluster.get('status') == 'new' || !!this.props.task;
return (
{i18n(namespace + 'reset_environment')}
);
}
});
var DeleteEnvironmentAction = React.createClass({
applyAction(e) {
e.preventDefault();
RemoveClusterDialog.show({cluster: this.props.cluster});
},
render() {
return (
{i18n(namespace + 'delete_environment')}
);
}
});
var InstructionElement = React.createClass({
render() {
var link = utils.composeDocumentationLink(this.props.link);
var classes = {
instruction: true
};
classes[this.props.wrapperClass] = !!this.props.wrapperClass;
return (
);
}
});
export default DashboardTab;