diff --git a/releasenotes/notes/node_counts_improved-def0d57d4f3d72ec.yaml b/releasenotes/notes/node_counts_improved-def0d57d4f3d72ec.yaml
new file mode 100644
index 00000000..52e55a3d
--- /dev/null
+++ b/releasenotes/notes/node_counts_improved-def0d57d4f3d72ec.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ Fixes `bug 1750821 `__
+ Available node counts in Role cards are properly calculated based on
+ node - flavor - role tagging
diff --git a/src/__tests__/selectors/nodesAssignment.tests.js b/src/__tests__/selectors/nodesAssignment.tests.js
index cc248f96..c4c7de07 100644
--- a/src/__tests__/selectors/nodesAssignment.tests.js
+++ b/src/__tests__/selectors/nodesAssignment.tests.js
@@ -17,6 +17,7 @@
import { fromJS, Map } from 'immutable';
import * as selectors from '../../js/selectors/nodesAssignment';
+import { Flavor } from '../../js/immutableRecords/flavors';
import { Port } from '../../js/immutableRecords/nodes';
import { Role, RolesState } from '../../js/immutableRecords/roles';
import {
@@ -161,6 +162,145 @@ describe('Nodes Assignment selectors', () => {
expect(selectors.getUntaggedAvailableNodes(state).size).toEqual(2);
});
+ describe('getFlavorParametersByRole', () => {
+ const roles = Map({
+ Controller: new Role({
+ name: 'Controller'
+ }),
+ Compute: new Role({
+ name: 'Compute'
+ }),
+ BlockStorage: new Role({
+ name: 'BlockStorage'
+ })
+ });
+ const parameters = Map({
+ OvercloudControllerFlavor: new Parameter({
+ name: 'OvercloudControllerFlavor',
+ default: 'control'
+ }),
+ OvercloudComputeFlavor: new Parameter({
+ name: 'OvercloudComputeFlavor',
+ default: 'compute'
+ }),
+ OvercloudBlockStorageFlavor: new Parameter({
+ name: 'OvercloudBlockStorageFlavor',
+ default: 'block-storage'
+ }),
+ AnotherParameter1: new Parameter({
+ name: 'AnotherParameter1',
+ default: 'value 1'
+ }),
+ AnotherParameter2: new Parameter({
+ name: 'AnotherParameter2',
+ default: 'value 2'
+ })
+ });
+
+ it('provides flavor parameters by role', () => {
+ const result = selectors.getFlavorParametersByRole.resultFunc(
+ roles,
+ parameters
+ );
+ expect(result.get('Controller')).toEqual(
+ new Parameter({
+ name: 'OvercloudControllerFlavor',
+ default: 'control'
+ })
+ );
+ expect(result.get('Compute')).toEqual(
+ new Parameter({
+ name: 'OvercloudComputeFlavor',
+ default: 'compute'
+ })
+ );
+ expect(result.get('BlockStorage')).toEqual(
+ new Parameter({
+ name: 'OvercloudBlockStorageFlavor',
+ default: 'block-storage'
+ })
+ );
+ });
+ });
+
+ describe('getFlavorProfilesByRole', () => {
+ const flavors = Map({
+ control: new Flavor({
+ id: 'aaaa',
+ name: 'control',
+ extra_specs: Map({
+ 'capabilities:profile': 'control'
+ })
+ }),
+ compute: new Flavor({
+ id: 'bbbb',
+ name: 'compute',
+ extra_specs: Map({
+ 'capabilities:profile': 'compute'
+ })
+ }),
+ 'block-storage': new Flavor({
+ id: 'cccc',
+ name: 'block-storage',
+ extra_specs: Map({
+ 'capabilities:profile': 'block-storage'
+ })
+ })
+ });
+ const flavorParametersByRole = Map({
+ Controller: new Parameter({
+ name: 'OvercloudControllerFlavor',
+ default: 'control'
+ }),
+ Compute: new Parameter({
+ name: 'OvercloudComputeFlavor',
+ default: 'compute'
+ }),
+ BlockStorage: new Parameter({
+ name: 'OvercloudBlockStorageFlavor',
+ default: 'block-storage'
+ })
+ });
+
+ it('provides flavor profiles by role', () => {
+ const result = selectors.getFlavorProfilesByRole.resultFunc(
+ flavorParametersByRole,
+ flavors
+ );
+ expect(result.get('Controller')).toEqual('control');
+ expect(result.get('Compute')).toEqual('compute');
+ expect(result.get('BlockStorage')).toEqual('block-storage');
+ });
+ });
+
+ describe('getTaggedNodesCountByRole', () => {
+ const availableNodes = fromJS({
+ node1: {
+ uuid: 'node1',
+ properties: { capabilities: 'boot_option:local' }
+ },
+ node2: {
+ uuid: 'node2',
+ properties: { capabilities: 'boot_option:local,profile:control' }
+ }
+ });
+ const flavorProfilesByRole = Map({
+ Controller: 'control',
+ Compute: 'compute',
+ BlockStorage: 'block-storage'
+ });
+
+ it('calculates tagged node counts by role', () => {
+ const result = selectors.getTaggedNodesCountByRole.resultFunc(
+ availableNodes,
+ flavorProfilesByRole
+ );
+ expect(result.get('Controller')).toEqual(1);
+ expect(result.get('Compute')).toEqual(0);
+ expect(result.get('BlockStorage')).toEqual(0);
+ });
+ });
+
describe('provides getTotalUntaggedAssignedNodesCount selector', () => {
const nodes = fromJS({
node1: {
@@ -186,6 +326,11 @@ describe('Nodes Assignment selectors', () => {
identifier: 'block-storage'
})
});
+ const taggedNodesCountByRole = Map({
+ Controller: 1,
+ Compute: 0,
+ BlockStorage: 0
+ });
const parametersByRole = Map({
Controler: new Parameter({
name: 'ControllerCount',
@@ -201,6 +346,7 @@ describe('Nodes Assignment selectors', () => {
const result = selectors.getTotalUntaggedAssignedNodesCount.resultFunc(
nodes,
roles,
+ taggedNodesCountByRole,
parametersByRole
);
expect(result).toEqual(1);
@@ -224,6 +370,7 @@ describe('Nodes Assignment selectors', () => {
const result = selectors.getTotalUntaggedAssignedNodesCount.resultFunc(
nodes,
roles,
+ taggedNodesCountByRole,
parametersByRole
);
expect(result).toEqual(1);
@@ -264,6 +411,11 @@ describe('Nodes Assignment selectors', () => {
identifier: 'block-storage'
})
});
+ const taggedNodesCountByRole = Map({
+ Controller: 1,
+ Compute: 0,
+ BlockStorage: 0
+ });
const nodeCountParametersByRole = Map({
Controller: new Parameter({
name: 'ControllerCount',
@@ -285,6 +437,7 @@ describe('Nodes Assignment selectors', () => {
availableNodes,
untaggedAvailableNodes,
roles,
+ taggedNodesCountByRole,
nodeCountParametersByRole,
totalUntaggedAssignedNodesCount
);
@@ -313,6 +466,7 @@ describe('Nodes Assignment selectors', () => {
availableNodes,
untaggedAvailableNodes,
roles,
+ taggedNodesCountByRole,
nodeCountParametersByRole,
totalUntaggedAssignedNodesCount
);
diff --git a/src/js/components/deployment_plan/RolesStep.js b/src/js/components/deployment_plan/RolesStep.js
index b6410c01..765c58d5 100644
--- a/src/js/components/deployment_plan/RolesStep.js
+++ b/src/js/components/deployment_plan/RolesStep.js
@@ -20,6 +20,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { withRouter } from 'react-router-dom';
+import FlavorsActions from '../../actions/FlavorsActions';
import { getCurrentPlanName } from '../../selectors/plans';
import { getNodes } from '../../selectors/nodes';
import {
@@ -54,8 +55,10 @@ const RolesStep = ({
allNodesCount,
availableNodesCount,
currentPlanName,
+ fetchFlavors,
fetchRoles,
fetchNodes,
+ flavorsLoaded,
intl,
isFetchingNodes,
isFetchingParameters,
@@ -92,9 +95,10 @@ const RolesStep = ({
);
@@ -103,8 +107,10 @@ RolesStep.propTypes = {
allNodesCount: PropTypes.number.isRequired,
availableNodesCount: PropTypes.number.isRequired,
currentPlanName: PropTypes.string,
+ fetchFlavors: PropTypes.func.isRequired,
fetchNodes: PropTypes.func.isRequired,
fetchRoles: PropTypes.func.isRequired,
+ flavorsLoaded: PropTypes.bool.isRequired,
intl: PropTypes.object,
isFetchingNodes: PropTypes.bool.isRequired,
isFetchingParameters: PropTypes.bool.isRequired,
@@ -117,6 +123,7 @@ const mapStateToProps = state => ({
allNodesCount: getNodes(state).size,
availableNodesCount: getAvailableNodes(state).size,
currentPlanName: getCurrentPlanName(state),
+ flavorsLoaded: state.flavors.isLoaded,
isFetchingNodes: state.nodes.get('isFetching'),
isFetchingParameters: state.parameters.isFetching,
rolesLoaded: state.roles.loaded,
@@ -124,6 +131,7 @@ const mapStateToProps = state => ({
totalAssignedNodesCount: getTotalAssignedNodesCount(state)
});
const mapDispatchToProps = dispatch => ({
+ fetchFlavors: () => dispatch(FlavorsActions.fetchFlavors()),
fetchRoles: planName => dispatch(RolesActions.fetchRoles(planName)),
fetchNodes: () => dispatch(NodesActions.fetchNodes())
});
diff --git a/src/js/components/roles/Roles.js b/src/js/components/roles/Roles.js
index d0174a49..c5ff2f2f 100644
--- a/src/js/components/roles/Roles.js
+++ b/src/js/components/roles/Roles.js
@@ -34,6 +34,7 @@ class Roles extends React.Component {
componentDidMount() {
this.props.fetchRoles();
this.props.fetchNodes();
+ this.props.fetchFlavors();
}
render() {
@@ -56,6 +57,7 @@ class Roles extends React.Component {
}
}
Roles.propTypes = {
+ fetchFlavors: PropTypes.func.isRequired,
fetchNodes: PropTypes.func.isRequired,
fetchRoles: PropTypes.func.isRequired,
intl: PropTypes.object,
diff --git a/src/js/selectors/nodesAssignment.js b/src/js/selectors/nodesAssignment.js
index 59ae5d2b..0d691b00 100644
--- a/src/js/selectors/nodesAssignment.js
+++ b/src/js/selectors/nodesAssignment.js
@@ -17,9 +17,12 @@
import { createSelector } from 'reselect';
import { getFormValues } from 'redux-form';
+import { Flavor } from '../immutableRecords/flavors';
import { getNodes, getNodeCapabilities } from './nodes';
import { getParameters } from './parameters';
import { getRoles } from './roles';
+import { getFlavors } from './flavors';
+import { Parameter } from '../immutableRecords/parameters';
/**
* Return Nodes which are either available or deployed (active) with current Plan
@@ -65,16 +68,62 @@ export const getNodeCountParametersByRoleFromFormValues = createSelector(
)
);
+/**
+ * Returns OvercloudFlavor parameters for each Role
+ */
+export const getFlavorParametersByRole = createSelector(
+ [getRoles, getParameters],
+ (roles, parameters) =>
+ roles.map(role =>
+ parameters.get(`Overcloud${role.name}Flavor`, new Parameter())
+ )
+);
+
+/**
+ * Returns flavor profile each Role.
+ */
+export const getFlavorProfilesByRole = createSelector(
+ [getFlavorParametersByRole, getFlavors],
+ (flavorParametersByRole, flavors) =>
+ flavorParametersByRole.map(roleFlavorParameter =>
+ flavors
+ .find(
+ flavor => flavor.name === roleFlavorParameter.default,
+ null,
+ new Flavor()
+ )
+ .getIn(['extra_specs', 'capabilities:profile'])
+ )
+);
+
+/**
+ * Returns number of tagged nodes for each Role
+ */
+export const getTaggedNodesCountByRole = createSelector(
+ [getAvailableNodes, getFlavorProfilesByRole],
+ (nodes, flavorProfilesByRole) =>
+ flavorProfilesByRole.map(
+ (roleFlavorProfile, roleName) =>
+ nodes.filter(node => {
+ const nodeProfile = getNodeCapabilities(node).profile;
+ return nodeProfile && nodeProfile === roleFlavorProfile;
+ }).size
+ )
+);
+
/**
* Returns sum of untagged assigned Nodes counts across all Roles
*/
export const getTotalUntaggedAssignedNodesCount = createSelector(
- [getAvailableNodes, getRoles, getNodeCountParametersByRoleFromFormValues],
- (nodes, roles, parametersByRole) =>
+ [
+ getAvailableNodes,
+ getRoles,
+ getTaggedNodesCountByRole,
+ getNodeCountParametersByRoleFromFormValues
+ ],
+ (nodes, roles, taggedNodesCountByRole, parametersByRole) =>
roles.reduce((total, role) => {
- const taggedCount = nodes.filter(
- node => getNodeCapabilities(node).profile === role.identifier
- ).size;
+ const taggedCount = taggedNodesCountByRole.get(role.name);
const assignedCount = parametersByRole.getIn([role.name, 'default'], 0);
const remainder = Math.max(0, assignedCount - taggedCount);
return total + remainder;
@@ -89,14 +138,20 @@ export const getAvailableNodesCountsByRole = createSelector(
getAvailableNodes,
getUntaggedAvailableNodes,
getRoles,
+ getTaggedNodesCountByRole,
getNodeCountParametersByRoleFromFormValues,
getTotalUntaggedAssignedNodesCount
],
- (nodes, untaggedNodes, roles, parametersByRole, untaggedAssignedCount) =>
+ (
+ nodes,
+ untaggedNodes,
+ roles,
+ taggedNodesCountByRole,
+ parametersByRole,
+ untaggedAssignedCount
+ ) =>
roles.map(role => {
- const taggedCount = nodes.filter(
- node => getNodeCapabilities(node).profile === role.identifier
- ).size;
+ const taggedCount = taggedNodesCountByRole.get(role.name);
const assignedCount = parametersByRole.getIn([role.name, 'default'], 0);
const untaggedCount = untaggedNodes.size;
return (