Allow adding roles to deployed nodes

Partial-Bug: #1642165

Change-Id: I19e4cb65f36fd812a82573cacddf3c9e9257079b
This commit is contained in:
Julia Aranovich 2016-11-15 12:47:02 +03:00
parent 969a74a0e7
commit ace554d9cc
7 changed files with 143 additions and 57 deletions

View File

@ -496,7 +496,8 @@ models.Node = BaseModel.extend({
var status = this.get('status');
return status === 'provisioned' ||
status === 'stopped' ||
status === 'error' && this.get('error_type') === 'deploy';
status === 'error' && this.get('error_type') === 'deploy' ||
status === 'ready' && !!this.get('pending_roles').length;
},
hasChanges() {
return this.get('pending_addition') ||

View File

@ -2657,7 +2657,7 @@ input[type=range] {
}
.role-list {
li {
li:not(.text-success) {
color: @base-text-color;
}
}
@ -3262,7 +3262,7 @@ input[type=range] {
cursor: default;
color: @orange;
border-color: lighten(@orange, 20%);
&.indeterminated {
&.selected, &.indeterminated {
background-color: lighten(@green, 15%);
border: 1px solid lighten(@green, 15%);
color: @white;

View File

@ -645,6 +645,7 @@
"remove": "Remove",
"delete_node": "Delete node",
"discard_deletion": "Discard Deletion",
"discard_role_changes": "Discard Role Changes",
"cant_discard": "Cannot discard the node deletion",
"view_logs": "View Logs",
"edit_name": "Edit Name",
@ -662,6 +663,7 @@
"no_nodes_in_environment": "To add nodes to the environment:\n1. Click Add Nodes.\n2. Select the nodes you want to allocate.\n3. Assign a role to each node.",
"no_nodes_in_fuel": "A pool of one or more unallocated nodes is needed for this operation. To add nodes to the pool, configure the node to boot from the network (a.k.a. PXE booting). Fuel will automatically provision and discover the nodes.",
"role_conflict": "This role cannot be combined with the selected roles.",
"deployed_role": "This role is deployed on some node(s).",
"node_deletion_error": {
"title": "Node Deletion Error",
"node_deletion_warning": "Unable to delete the nodes."
@ -774,6 +776,8 @@
"http_plugin_link": "(HTTP)",
"added_node": "Added __count__ node",
"added_node_plural": "Added __count__ nodes",
"changed_roles": "Roles changed for __count__ node",
"changed_roles_plural": "Roles changed for __count__ nodes",
"deleted_node": "Deleted __count__ node",
"deleted_node_plural": "Deleted __count__ nodes",
"changed_configuration": "Changed environment configuration",
@ -818,8 +822,10 @@
"deployment": {
"title": "Deployment only",
"description": "\"Advanced deployment\" deploys OpenStack services on nodes which have the operating system already provisioned.\nTo complete provisioning and deployment at the same time, select \"Provisioning + Deployment\" from the Deployment Mode dropdown.",
"nodes_to_deploy": "__count__ node is provisioned",
"nodes_to_deploy_plural": "__count__ nodes are provisioned",
"nodes_to_redeploy": "__count__ node to redeploy",
"nodes_to_redeploy_plural": "__count__ nodes to redeploy",
"provisioned_nodes_to_deploy": "__count__ node is provisioned",
"provisioned_nodes_to_deploy_plural": "__count__ nodes are provisioned",
"no_nodes": "Nodes can only be deployed using the \"Deployment only\" feature if they have been provisioned with an operating system.\nPlease select \"Provisioning only\" or \"Provisioning + Deployment\" from the Deployment Mode dropdown to continue.",
"button_title_all_nodes": "Deploy __count__ Node",
"button_title_all_nodes_plural": "Deploy __count__ Nodes",
@ -1240,6 +1246,7 @@
"title": "Discard Changes",
"discard_button": "Discard",
"discard_addition": "Are you sure you want to discard node addition?",
"discard_role_changes": "Are you sure you want to discard role changes?",
"discard_deletion": "Are you sure you want to discard node deletion?",
"discard_environment_configuration": "Are you sure you want to discard changes in the environment network and Openstack settings configuration? The configuration will be reset to its deployed state.",
"cant_discard": "Cannot discard the changes.",

View File

@ -769,6 +769,12 @@ var ClusterActionsPanel = React.createClass({
actionControls = [
<ul key='cluster-changes'>
{this.renderClusterChangeItem('added_node', nodes.filter({pending_addition: true}))}
{this.renderClusterChangeItem(
'changed_roles',
nodes.filter(
(node) => !node.get('pending_addition') && node.get('pending_roles').length
)
)}
{this.renderClusterChangeItem(
'provisioned_node',
nodes.filter({pending_deletion: false, status: 'provisioned'}),
@ -820,16 +826,21 @@ var ClusterActionsPanel = React.createClass({
];
break;
case 'deployment':
var provisionedNodes = nodes.filter({status: 'provisioned'});
var nodesWithChangedRoles = nodes.filter(
(node) => node.get('status') === 'ready' && !!node.get('pending_roles').length
);
actionControls = [
!!nodes.length &&
<ul key='node-changes'>
<li>
{i18n(actionNs + 'nodes_to_deploy', {count: nodes.length})}
</li>
{!!provisionedNodes.length &&
<li>{i18n(actionNs + 'provisioned_nodes', {count: provisionedNodes.length})}</li>
}
{!!nodesWithChangedRoles.length &&
<li>{i18n(ns + 'changed_roles', {count: nodesWithChangedRoles.length})}</li>
}
{!!offlineNodes.length &&
<li>
{i18n(ns + 'offline_nodes', {count: offlineNodes.length})}
</li>
<li>{i18n(ns + 'offline_nodes', {count: offlineNodes.length})}</li>
}
</ul>,
<ClusterActionButton
@ -838,7 +849,7 @@ var ClusterActionsPanel = React.createClass({
className='btn-deploy-nodes'
dialog={DeployNodesDialog}
canSelectNodes
nodeStatusesToFilter={['provisioned', 'stopped', 'error']}
nodeStatusesToFilter={['provisioned', 'stopped', 'error', 'ready']}
/>
];
break;

View File

@ -95,6 +95,25 @@ var Node = React.createClass({
this.setState({actionInProgress: false});
});
},
discardRoleChanges(e) {
e.preventDefault();
if (this.state.actionInProgress) return;
this.setState({actionInProgress: true});
new models.Node(this.props.node.attributes)
.save({pending_roles: []}, {patch: true})
.then(
() => this.props.cluster.get('nodes').fetch(),
(response) => {
utils.showErrorDialog({
title: i18n('cluster_page.nodes_tab.node.cant_discard'),
response
});
}
)
.then(() => {
this.setState({actionInProgress: false});
});
},
removeNode(e) {
e.preventDefault();
if (this.props.viewMode === 'compact') this.toggleExtendedNodePanel();
@ -211,18 +230,19 @@ var Node = React.createClass({
);
},
renderRoleList(roles) {
var {node} = this.props;
return (
<ul>
{_.map(roles, (role) => {
return (
<li
key={this.props.node.id + role}
className={utils.classNames({'text-success': !this.props.node.get('roles').length})}
>
{role}
</li>
);
})}
{_.map(roles, (role) =>
<li
key={node.id + role}
className={utils.classNames({
'text-success': _.includes(node.get('pending_roles'), role)
})}
>
{role}
</li>
)}
</ul>
);
},
@ -294,22 +314,33 @@ var Node = React.createClass({
</Link>
}
{renderActionButtons &&
(node.get('pending_addition') || node.get('pending_deletion')) &&
!locked &&
(
node.get('pending_addition') ||
node.get('pending_deletion') ||
!!node.get('cluster') && !!node.get('pending_roles').length
) &&
!locked &&
<button
className='btn btn-discard'
key='btn-discard'
onClick={node.get('pending_deletion') ?
this.discardNodeDeletion
:
this.showDeleteNodesDialog
onClick={
node.get('pending_deletion') ?
this.discardNodeDeletion
:
node.get('pending_addition') ?
this.showDeleteNodesDialog
:
this.discardRoleChanges
}
>
{i18n(ns + (node.get('pending_deletion') ?
'discard_deletion'
:
'delete_node'
))}
{i18n(ns +
(node.get('pending_deletion') ?
'discard_deletion'
:
node.get('pending_addition') ?
'discard_addition' : 'discard_role_changes'
)
)}
</button>
}
</div>
@ -471,21 +502,36 @@ var Node = React.createClass({
<Link className='btn-view-logs icon icon-logs' to={this.getNodeLogsLink()} />
</Tooltip>,
renderActionButtons &&
(node.get('pending_addition') || node.get('pending_deletion')) &&
!locked &&
(
node.get('pending_addition') ||
node.get('pending_deletion') ||
!!node.get('cluster') && !!node.get('pending_roles').length
) &&
!locked &&
<Tooltip
wrap
key={'discard-node-changes-' + node.id}
text={i18n(ns +
(node.get('pending_deletion') ? 'discard_deletion' : 'delete_node')
)}
text={
i18n(ns +
(node.get('pending_deletion') ?
'discard_deletion'
:
node.get('pending_addition') ?
'discard_addition' : 'discard_role_changes'
)
)
}
>
<div
className='icon btn-discard'
onClick={node.get('pending_deletion') ?
this.discardNodeDeletion
:
this.showDeleteNodesDialog
onClick={
node.get('pending_deletion') ?
this.discardNodeDeletion
:
node.get('pending_addition') ?
this.showDeleteNodesDialog
:
this.discardRoleChanges
}
/>
</Tooltip>

View File

@ -1151,7 +1151,7 @@ ManagementPanel = React.createClass({
{i18n('common.delete_button')}
</button>
}
{!!nodes.length && !nodes.some({pending_addition: false}) &&
{!locked && !!nodes.length &&
<button
className='btn btn-success btn-edit-roles'
onClick={_.partial(this.changeScreen, 'edit', true)}
@ -1641,12 +1641,15 @@ RolePanel = React.createClass({
});
},
processRestrictions(role) {
var {
cluster, nodes, selectedRoles, indeterminateRoles, processedRoleLimits, configModels
} = this.props;
var name = role.get('name');
var restrictionsCheck = role.checkRestrictions(this.props.configModels, 'disable');
var roleLimitsCheckResults = this.props.processedRoleLimits[name];
var roles = this.props.cluster.get('roles');
var conflicts = _.chain(this.props.selectedRoles)
.union(this.props.indeterminateRoles)
var restrictionsCheck = role.checkRestrictions(configModels, 'disable');
var roleLimitsCheckResults = processedRoleLimits[name];
var roles = cluster.get('roles');
var conflicts = _.chain(selectedRoles)
.union(indeterminateRoles)
.map((role) => roles.find({name: role}).conflicts)
.flatten()
.uniq()
@ -1663,10 +1666,19 @@ RolePanel = React.createClass({
warnings.push(i18n('cluster_page.nodes_tab.role_conflict'));
}
var isDeployed = nodes.some((node) => node.hasRole(name, true));
if (isDeployed) {
warnings.push(i18n('cluster_page.nodes_tab.deployed_role'));
}
return {
result: restrictionsCheck.result || _.includes(conflicts, name) ||
(roleLimitsCheckResults && !roleLimitsCheckResults.valid &&
!_.includes(this.props.selectedRoles, name)
result: restrictionsCheck.result ||
_.includes(conflicts, name) ||
isDeployed ||
(
roleLimitsCheckResults &&
!roleLimitsCheckResults.valid &&
!_.includes(selectedRoles, name)
),
warnings
};
@ -1696,8 +1708,7 @@ RolePanel = React.createClass({
<Role
key={roleName}
ref={roleName}
role={role}
selected={selected}
{...{role, selected}}
indeterminated={_.includes(indeterminateRoles, roleName)}
restrictions={this.processRestrictions(role)}
isRolePanelDisabled={!nodes.length}
@ -1788,7 +1799,7 @@ Role = React.createClass({
<i
className={utils.classNames({
glyphicon: true,
'glyphicon-selected-role': selected,
'glyphicon-selected-role': selected && !warnings.length,
'glyphicon-indeterminated-role': indeterminated && !warnings.length,
'glyphicon-warning-sign': !!warnings.length
})}

View File

@ -405,10 +405,16 @@ export var DiscardClusterChangesDialog = React.createClass({
pending_deletion: false
};
}
if (node.get('pending_addition')) {
return {
id: node.id,
cluster_id: null,
pending_addition: false,
pending_roles: []
};
}
return {
id: node.id,
cluster_id: null,
pending_addition: false,
pending_roles: []
};
}));
@ -429,7 +435,11 @@ export var DiscardClusterChangesDialog = React.createClass({
var text = changeName === 'changed_configuration' ?
i18n(ns + 'discard_environment_configuration')
:
i18n(ns + (nodes[0].get('pending_deletion') ? 'discard_deletion' : 'discard_addition'));
i18n(ns + (nodes[0].get('pending_deletion') ?
'discard_deletion'
:
nodes[0].get('pending_addition') ? 'discard_addition' : 'discard_role_changes'
));
return (
<div className='text-danger'>
{this.renderImportantLabel()}