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 (