Show Node Introspection Data

* Fetches introspection data on expanding node
* If introspection data are available, display them in NodeExtendedInfo

Change-Id: I5feac2b64803e1cf2c31f0bbc259940a80ddae3c
This commit is contained in:
Jiri Tomasek 2017-05-31 14:52:13 +02:00
parent 7e65fba52f
commit 046188f13a
15 changed files with 600 additions and 71 deletions

View File

@ -0,0 +1,5 @@
---
features:
- |
Nodes List view now fetches and displays introspection data in expanded
Node view

View File

@ -121,6 +121,12 @@ let createResolvingPromise = data => {
};
};
let createRejectingPromise = error => {
return () => {
return when.reject(error);
};
};
describe('Asynchronous Nodes Actions', () => {
beforeEach(done => {
spyOn(utils, 'getAuthTokenId').and.returnValue('mock-auth-token');
@ -184,6 +190,72 @@ describe('Asynchronous Nodes Actions', () => {
});
});
describe('Fetching Introspection data success', () => {
const response = { interfaces: { eth0: { mac: '00:00:00:00:00:11' } } };
const nodeId = '598612eb-f21b-435e-a868-7bb74e576cc2';
beforeEach(done => {
spyOn(utils, 'getAuthTokenId').and.returnValue('mock-auth-token');
spyOn(utils, 'getServiceUrl').and.returnValue('mock-url');
spyOn(NodesActions, 'fetchNodeIntrospectionDataSuccess');
spyOn(IronicInspectorApiService, 'getIntrospectionData').and.callFake(
createResolvingPromise(response)
);
NodesActions.fetchNodeIntrospectionData(nodeId)(() => {}, () => {});
setTimeout(() => {
done();
}, 1);
});
it('dispatches fetchNodeIntrospectionDataSuccess', () => {
expect(IronicInspectorApiService.getIntrospectionData).toHaveBeenCalledWith(
nodeId
);
expect(NodesActions.fetchNodeIntrospectionDataSuccess).toHaveBeenCalledWith(
nodeId,
response
);
});
});
describe('Fetching Introspection data error', () => {
const nodeId = '598612eb-f21b-435e-a868-7bb74e576cc2';
const message = 'Data for specified node not available';
const error = {
status: 404,
responseText: `{ "error": { "message": "${message}" } }`
};
beforeEach(done => {
spyOn(utils, 'getAuthTokenId').and.returnValue('mock-auth-token');
spyOn(utils, 'getServiceUrl').and.returnValue('mock-url');
spyOn(NodesActions, 'fetchNodeIntrospectionDataFailed');
spyOn(NotificationActions, 'notify');
spyOn(IronicInspectorApiService, 'getIntrospectionData').and.callFake(
createRejectingPromise(error)
);
NodesActions.fetchNodeIntrospectionData(nodeId)(() => {}, () => {});
setTimeout(() => {
done();
}, 1);
});
it('dispatches fetchNodeIntrospectionDataFailed', () => {
expect(IronicInspectorApiService.getIntrospectionData).toHaveBeenCalledWith(
nodeId
);
expect(NodesActions.fetchNodeIntrospectionDataFailed).toHaveBeenCalledWith(
nodeId
);
expect(NotificationActions.notify).toHaveBeenCalledWith({
title: 'Introspection data not found',
message: message
});
});
});
describe('Asynchronous Introspect Nodes Action', () => {
beforeEach(done => {
spyOn(utils, 'getAuthTokenId').and.returnValue('mock-auth-token');

View File

@ -14,31 +14,25 @@
* under the License.
*/
import { Map, Set } from 'immutable';
import { fromJS, Map, Set } from 'immutable';
import NodesConstants from '../../js/constants/NodesConstants';
import nodesReducer from '../../js/reducers/nodesReducer';
import { NodesState } from '../../js/immutableRecords/nodes';
describe('nodesReducer', () => {
const initialState = Map({
const initialState = new NodesState({
isFetching: false,
nodesInProgress: Set(),
allFilter: '',
registeredFilter: '',
introspectedFilter: '',
deployedFilter: '',
maintenanceFilter: '',
all: Map()
all: Map(),
ports: Map(),
introspectionStatuses: Map(),
introspectionData: Map()
});
const updatedState = Map({
const updatedState = new NodesState({
isFetching: false,
nodesInProgress: Set(),
allFilter: '',
registeredFilter: '',
introspectedFilter: '',
deployedFilter: '',
maintenanceFilter: '',
all: Map({
uuid1: Map({
uuid: 'uuid1'
@ -46,17 +40,15 @@ describe('nodesReducer', () => {
uuid2: Map({
uuid: 'uuid2'
})
})
}),
ports: Map(),
introspectionStatuses: Map(),
introspectionData: Map()
});
const updatedNodeState = Map({
const updatedNodeState = new NodesState({
isFetching: false,
nodesInProgress: Set(),
allFilter: '',
registeredFilter: '',
introspectedFilter: '',
deployedFilter: '',
maintenanceFilter: '',
all: Map({
uuid1: Map({
uuid: 'uuid1',
@ -67,7 +59,10 @@ describe('nodesReducer', () => {
uuid2: Map({
uuid: 'uuid2'
})
})
}),
ports: Map(),
introspectionStatuses: Map(),
introspectionData: Map()
});
it('should return initial state', () => {
@ -144,4 +139,27 @@ describe('nodesReducer', () => {
const newState = nodesReducer(initialState, action);
expect(newState).toEqual(updatedState);
});
it('should handle FETCH_NODE_INTROSPECTION_DATA_SUCCESS action', () => {
const action = {
type: NodesConstants.FETCH_NODE_INTROSPECTION_DATA_SUCCESS,
payload: {
nodeId: 'uuid1',
data: { interfaces: { eth0: { mac: '00:00:00:00:00:11' } } }
}
};
const newState = nodesReducer(updatedState, action);
expect(newState.introspectionData.get('uuid1')).toEqual(
fromJS(action.payload.data)
);
});
it('should handle FETCH_NODE_INTROSPECTION_DATA_FAILED action', () => {
const action = {
type: NodesConstants.FETCH_NODE_INTROSPECTION_DATA_FAILED,
payload: 'uuid1'
};
const newState = nodesReducer(updatedState, action);
expect(newState.introspectionData.get('uuid1')).toEqual(undefined);
});
});

View File

@ -20,6 +20,8 @@ import when from 'when';
import { getNodesByIds } from '../selectors/nodes';
import IronicApiErrorHandler from '../services/IronicApiErrorHandler';
import IronicInspectorApiErrorHandler
from '../services/IronicInspectorApiErrorHandler';
import IronicApiService from '../services/IronicApiService';
import IronicInspectorApiService from '../services/IronicInspectorApiService';
import MistralApiService from '../services/MistralApiService';
@ -119,6 +121,40 @@ export default {
};
},
fetchNodeIntrospectionDataSuccess(nodeId, data) {
return {
type: NodesConstants.FETCH_NODE_INTROSPECTION_DATA_SUCCESS,
payload: { nodeId, data }
};
},
fetchNodeIntrospectionDataFailed(nodeId) {
return {
type: NodesConstants.FETCH_NODE_INTROSPECTION_DATA_FAILED,
payload: nodeId
};
},
fetchNodeIntrospectionData(nodeId) {
return dispatch => {
IronicInspectorApiService.getIntrospectionData(nodeId)
.then(response => {
dispatch(this.fetchNodeIntrospectionDataSuccess(nodeId, response));
})
.catch(error => {
dispatch(this.fetchNodeIntrospectionDataFailed(nodeId));
logger.error(
'Error in NodesActions.fetchNodeIntrospectionData',
error.stack || error
);
let errorHandler = new IronicInspectorApiErrorHandler(error);
errorHandler.errors.forEach(error => {
dispatch(NotificationActions.notify(error));
});
});
};
},
/*
* Poll fetchNodes until no node is in progress
*/

View File

@ -69,6 +69,7 @@ class Nodes extends React.Component {
? <NodesTableView />
: <NodesListForm>
<NodesListView
fetchNodeIntrospectionData={this.props.fetchNodeIntrospectionData}
nodes={this.props.nodes}
nodesInProgress={this.props.nodesInProgress}
/>
@ -118,6 +119,7 @@ class Nodes extends React.Component {
Nodes.propTypes = {
contentView: PropTypes.string.isRequired,
currentPlanName: PropTypes.string.isRequired,
fetchNodeIntrospectionData: PropTypes.func.isRequired,
fetchNodes: PropTypes.func.isRequired,
fetchRoles: PropTypes.func.isRequired,
fetchingNodes: PropTypes.bool.isRequired,
@ -141,6 +143,8 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
fetchNodes: () => dispatch(NodesActions.fetchNodes()),
fetchNodeIntrospectionData: nodeId =>
dispatch(NodesActions.fetchNodeIntrospectionData(nodeId)),
fetchRoles: currentPlanName =>
dispatch(RolesActions.fetchRoles(currentPlanName))
});

View File

@ -1,52 +1,251 @@
import { FormattedDate, FormattedTime } from 'react-intl';
/**
* Copyright 2017 Red Hat 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 {
defineMessages,
FormattedDate,
FormattedMessage,
FormattedTime,
injectIntl
} from 'react-intl';
import { startCase } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { Row, Col } from 'react-bootstrap';
const messages = defineMessages({
macAddresses: {
id: 'NodeExtendedInfo.macAddresses',
defaultMessage: 'Mac Addresses:'
},
interfaces: {
id: 'nodeExtendedinfo.interfaces',
defaultMessage: 'Interfaces:'
},
macAddress: {
id: 'nodeExtendedinfo.interfaceMacAddress',
defaultMessage: 'MAC Address'
},
ipAddress: {
id: 'nodeExtendedinfo.interfaceIpAddress',
defaultMessage: 'IP Address'
},
bios: {
id: 'nodeExtendedinfo.bios',
defaultMessage: 'Bios:'
},
rootDisk: {
id: 'nodeExtendedinfo.rootDisk',
defaultMessage: 'Root Disk:'
},
product: {
id: 'nodeExtendedinfo.product',
defaultMessage: 'Product:'
},
productName: {
id: 'nodeExtendedinfo.productName',
defaultMessage: 'Name'
},
productVendor: {
id: 'nodeExtendedinfo.productVendor',
defaultMessage: 'Vendor'
},
productVersion: {
id: 'nodeExtendedinfo.productVersion',
defaultMessage: 'Version'
},
kernel: {
id: 'nodeExtendedinfo.kernel',
defaultMessage: 'Kernel:'
},
uuid: {
id: 'nodeExtendedinfo.uuid',
defaultMessage: 'UUID:'
},
registered: {
id: 'nodeExtendedinfo.registered',
defaultMessage: 'Registered:'
},
architecture: {
id: 'nodeExtendedinfo.architecture',
defaultMessage: 'Architecture:'
},
driver: {
id: 'nodeExtendedinfo.driver',
defaultMessage: 'Driver:'
}
});
class NodeExtendedInfo extends React.Component {
componentDidMount() {
if (this.props.node.getIn(['introspectionStatus', 'finished'])) {
this.props.fetchNodeIntrospectionData(this.props.node.get('uuid'));
}
}
renderInterfaces() {
const { intl, node } = this.props;
if (node.getIn(['introspectionData', 'interfaces']).isEmpty()) {
return (
<dl>
<dt><FormattedMessage {...messages.macAddresses} /></dt>
{node.get('macs').map(mac => <dd key={mac}>{mac}</dd>)}
</dl>
);
} else {
return (
<dl>
<dt><FormattedMessage {...messages.interfaces} /></dt>
<dd>
{node
.getIn(['introspectionData', 'interfaces'])
.map((ifc, k) => {
return (
<div key={k}>
{k}
{' '} - {' '}
<span title={intl.formatMessage(messages.macAddress)}>
{ifc.get('mac')}
</span>
{' '} | {' '}
<span title={intl.formatMessage(messages.ipAddress)}>
{ifc.get('ip')}
</span>
{ifc.get('pxe') && '| PXE'}
</div>
);
})
.toList()}
</dd>
</dl>
);
}
}
renderBios() {
const bios = this.props.node.getIn(['introspectionData', 'bios']);
return (
!bios.isEmpty() &&
<div>
<dt><FormattedMessage {...messages.bios} /></dt>
<dd>
{bios
.map((i, k) => <span key={k} title={startCase(k)}>{i} </span>)
.toList()}
</dd>
</div>
);
}
renderRootDisk() {
const rootDisk = this.props.node.getIn(['introspectionData', 'rootDisk']);
return (
rootDisk &&
<div>
<dt><FormattedMessage {...messages.rootDisk} /></dt>
<dd>{rootDisk}</dd>
</div>
);
}
renderProduct() {
const product = this.props.node.getIn(['introspectionData', 'product']);
return (
!product.isEmpty() &&
<div>
<dt><FormattedMessage {...messages.product} /></dt>
<dd>
<span title={this.props.intl.formatMessage(messages.productName)}>
{product.get('name')}
</span>
{' '} - {' '}
<span title={this.props.intl.formatMessage(messages.productVendor)}>
{product.get('vendor')}
</span>
{' '} | {' '}
<span title={this.props.intl.formatMessage(messages.productVersion)}>
{product.get('version')}
</span>
</dd>
</div>
);
}
renderKernel() {
const kernelVersion = this.props.node.getIn([
'introspectionData',
'kernelVersion'
]);
return (
kernelVersion &&
<div>
<dt><FormattedMessage {...messages.kernel} /></dt>
<dd>{kernelVersion}</dd>
</div>
);
}
render() {
const { node } = this.props;
return (
<Row>
<Col lg={3} md={6}>
<Col lg={4} md={6}>
<dl className="dl-horizontal dl-horizontal-condensed">
<dt>UUID:</dt>
<dd>{node.uuid}</dd>
<dt>Registered:</dt>
<dt><FormattedMessage {...messages.uuid} /></dt>
<dd>{node.get('uuid')}</dd>
<dt><FormattedMessage {...messages.registered} /></dt>
<dd>
<FormattedDate value={node.created_at} />
<FormattedDate value={node.get('created_at')} />
&nbsp;
<FormattedTime value={node.created_at} />
<FormattedTime value={node.get('created_at')} />
</dd>
<dt>Architecture:</dt>
<dd>{node.properties.cpu_arch}</dd>
</dl>
</Col>
<Col lg={2} md={4}>
<dl>
<dt>Mac Addresses:</dt>
{node.macs.map(mac => <dd key={mac}>{mac}</dd>)}
<dt><FormattedMessage {...messages.architecture} /></dt>
<dd>{node.getIn(['properties', 'cpu_arch'])}</dd>
{this.renderRootDisk()}
{this.renderBios()}
{this.renderProduct()}
{this.renderKernel()}
</dl>
</Col>
<Col lg={4} md={6}>
<dl className="dl-horizontal dl-horizontal-condensed">
<dt>Driver:</dt>
<dd>{node.driver}</dd>
{Object.keys(node.driver_info).map(key => (
<span key={key}>
<dt>{startCase(key)}:</dt>
<dd>{node.driver_info[key]}</dd>
</span>
))}
<dt><FormattedMessage {...messages.driver} /></dt>
<dd>{node.get('driver')}</dd>
{node
.get('driver_info')
.map((dInfo, key) => (
<span key={key}>
<dt>{startCase(key)}:</dt>
<dd>{dInfo}</dd>
</span>
))
.toList()}
</dl>
</Col>
<Col lg={3} md={6}>
{this.renderInterfaces()}
</Col>
</Row>
);
}
}
NodeExtendedInfo.propTypes = {
fetchNodeIntrospectionData: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
node: PropTypes.object.isRequired
};
export default NodeExtendedInfo;
export default injectIntl(NodeExtendedInfo);

View File

@ -1,7 +1,24 @@
/**
* Copyright 2017 Red Hat 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 ClassNames from 'classnames';
import { defineMessages, FormattedMessage } from 'react-intl';
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import {
ListViewAdditionalInfo,
@ -61,7 +78,7 @@ export default class NodeListItem extends React.Component {
}
render() {
const { node, inProgress } = this.props;
const { fetchNodeIntrospectionData, node, inProgress } = this.props;
const iconClass = ClassNames({
'pficon pficon-server': true,
@ -73,7 +90,7 @@ export default class NodeListItem extends React.Component {
<ListViewExpand expanded={this.state.expanded} />
<ListViewCheckbox
disabled={inProgress}
name={`values.${node.uuid}`}
name={`values.${node.get('uuid')}`}
/>
<ListViewMainInfo>
<ListViewLeft>
@ -82,23 +99,25 @@ export default class NodeListItem extends React.Component {
<ListViewBody>
<ListViewDescription>
<ListViewDescriptionHeading>
{node.name || node.uuid}
{node.get('name') || node.get('uuid')}
</ListViewDescriptionHeading>
<ListViewDescriptionText>
<NodePowerState
powerState={node.power_state}
targetPowerState={node.target_power_state}
powerState={node.get('power_state')}
targetPowerState={node.get('target_power_state')}
/>
<NodeMaintenanceState
maintenance={node.maintenance}
reason={node.maintenance_reason}
maintenance={node.get('maintenance')}
reason={node.get('maintenance_reason')}
/>
{' | '}
<NodeIntrospectionStatus status={node.introspectionStatus} />
<NodeIntrospectionStatus
status={node.get('introspectionStatus').toJS()}
/>
{' | '}
<NodeProvisionState
provisionState={node.provision_state}
targetProvisionState={node.target_provision_state}
provisionState={node.get('provision_state')}
targetProvisionState={node.get('target_provision_state')}
/>
</ListViewDescriptionText>
</ListViewDescription>
@ -107,27 +126,30 @@ export default class NodeListItem extends React.Component {
<span className="pficon pficon-flavor" />
<FormattedMessage {...messages.profile} />
&nbsp;
{parseNodeCapabilities(node.properties.capabilities)
.profile || '-'}
{parseNodeCapabilities(
node.getIn(['properties', 'capabilities'])
).profile || '-'}
</ListViewAdditionalInfoItem>
<ListViewAdditionalInfoItem>
<span className="pficon pficon-cpu" />
<strong>{node.properties.cpus || '-'}</strong>
<strong>{node.getIn(['properties', 'cpus'], '-')}</strong>
&nbsp;
<FormattedMessage
{...messages.cpuCores}
values={{ cpuCores: node.properties.cpus }}
values={{ cpuCores: node.getIn(['properties', 'cpus']) }}
/>
</ListViewAdditionalInfoItem>
<ListViewAdditionalInfoItem>
<span className="pficon pficon-memory" />
<strong>{node.properties.memory_mb || '-'}</strong>
<strong>
{node.getIn(['properties', 'memory_mb'], '-')}
</strong>
&nbsp;
<FormattedMessage {...messages.ram} />
</ListViewAdditionalInfoItem>
<ListViewAdditionalInfoItem>
<span className="fa fa-database" />
<strong>{node.properties.local_gb || '-'}</strong>
<strong>{node.getIn(['properties', 'local_gb'], '-')}</strong>
&nbsp;
<FormattedMessage {...messages.disk} />
</ListViewAdditionalInfoItem>
@ -139,13 +161,17 @@ export default class NodeListItem extends React.Component {
onClose={this.toggleExpanded.bind(this)}
expanded={this.state.expanded}
>
<NodeExtendedInfo node={node} />
<NodeExtendedInfo
node={node}
fetchNodeIntrospectionData={fetchNodeIntrospectionData}
/>
</ListViewItemContainer>
</ListViewItem>
);
}
}
NodeListItem.propTypes = {
fetchNodeIntrospectionData: PropTypes.func.isRequired,
inProgress: PropTypes.bool.isRequired,
node: PropTypes.object.isRequired
node: ImmutablePropTypes.map.isRequired
};

View File

@ -1,3 +1,19 @@
/**
* Copyright 2017 Red Hat 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 ClassNames from 'classnames';
import { defineMessages, FormattedMessage } from 'react-intl';
import React from 'react';

View File

@ -1,3 +1,19 @@
/**
* Copyright 2017 Red Hat 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 { connect } from 'react-redux';
import { defineMessages, injectIntl } from 'react-intl';
import React from 'react';

View File

@ -1,4 +1,21 @@
/**
* Copyright 2017 Red Hat 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 ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import React from 'react';
import { ListView } from '../../ui/ListView';
@ -10,12 +27,12 @@ export default class NodesListView extends React.Component {
<ListView>
{this.props.nodes
.toList()
.toJS()
.map(node => (
<NodeListItem
fetchNodeIntrospectionData={this.props.fetchNodeIntrospectionData}
node={node}
key={node.uuid}
inProgress={this.props.nodesInProgress.includes(node.uuid)}
key={node.get('uuid')}
inProgress={this.props.nodesInProgress.includes(node.get('uuid'))}
/>
))}
</ListView>
@ -23,6 +40,7 @@ export default class NodesListView extends React.Component {
}
}
NodesListView.propTypes = {
fetchNodeIntrospectionData: PropTypes.func.isRequired,
nodes: ImmutablePropTypes.map.isRequired,
nodesInProgress: ImmutablePropTypes.set.isRequired
};

View File

@ -20,6 +20,8 @@ export default keyMirror({
REQUEST_NODES: null,
RECEIVE_NODES: null,
FETCH_NODE_MACS_SUCCESS: null,
FETCH_NODE_INTROSPECTION_DATA_SUCCESS: null,
FETCH_NODE_INTROSPECTION_DATA_FAILED: null,
START_NODES_OPERATION: null,
FINISH_NODES_OPERATION: null,
UPDATE_NODE_PENDING: null,

View File

@ -22,7 +22,8 @@ export const NodesState = Record({
nodesInProgress: Set(),
all: Map(),
ports: Map(),
introspectionStatuses: Map()
introspectionStatuses: Map(),
introspectionData: Map()
});
export const NodeToRegister = Record({

View File

@ -43,6 +43,14 @@ export default function nodesReducer(state = initialState, action) {
.set('isFetching', false);
}
case NodesConstants.FETCH_NODE_INTROSPECTION_DATA_SUCCESS: {
const { nodeId, data } = action.payload;
return state.setIn(['introspectionData', nodeId], fromJS(data));
}
case NodesConstants.FETCH_NODE_INTROSPECTION_DATA_FAILED:
return state.deleteIn(['introspectionData', action.payload]);
case NodesConstants.START_NODES_OPERATION:
return state.update('nodesInProgress', nodesInProgress =>
nodesInProgress.union(action.payload)

View File

@ -15,7 +15,7 @@
*/
import { createSelector } from 'reselect';
import { List, Set } from 'immutable';
import { List, Map, Set } from 'immutable';
import { getFilterByName } from './filters';
import { getRoles } from './roles';
@ -30,6 +30,8 @@ export const getNodesByIds = (state, nodeIds) =>
.filter((v, k) => nodeIds.includes(k))
.sortBy(n => n.get('uuid'));
export const getPorts = state => state.nodes.get('ports');
export const getIntrospectionData = state =>
state.nodes.get('introspectionData');
export const getIntrospectionStatuses = state =>
state.nodes.get('introspectionStatuses');
export const nodesInProgress = state => state.nodes.get('nodesInProgress');
@ -40,8 +42,8 @@ export const nodesToolbarFilter = state =>
* Return Nodes including mac addresses as string at macs attribute
*/
export const getDetailedNodes = createSelector(
[getNodes, getPorts, getIntrospectionStatuses],
(nodes, ports, introspectionStatuses) =>
[getNodes, getPorts, getIntrospectionStatuses, getIntrospectionData],
(nodes, ports, introspectionStatuses, introspectionData) =>
nodes.map(node =>
node
.set(
@ -55,6 +57,38 @@ export const getDetailedNodes = createSelector(
'introspectionStatus',
introspectionStatuses.get(node.get('uuid'), new IntrospectionStatus())
)
.setIn(
['introspectionData', 'interfaces'],
introspectionData.getIn([node.get('uuid'), 'interfaces'], Map())
)
.setIn(
['introspectionData', 'rootDisk'],
introspectionData.getIn([node.get('uuid'), 'root_disk', 'name'])
)
.setIn(
['introspectionData', 'product'],
introspectionData.getIn(
[node.get('uuid'), 'extra', 'system', 'product'],
Map()
)
)
.setIn(
['introspectionData', 'kernelVersion'],
introspectionData.getIn([
node.get('uuid'),
'extra',
'system',
'kernel',
'version'
])
)
.setIn(
['introspectionData', 'bios'],
introspectionData.getIn(
[node.get('uuid'), 'extra', 'firmware', 'bios'],
Map()
)
)
)
);

View File

@ -0,0 +1,74 @@
/**
* Copyright 2017 Red Hat 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 BaseHttpRequestErrorHandler
from '../components/utils/BaseHttpRequestErrorHandler';
import LoginActions from '../actions/LoginActions';
import store from '../store';
export default class IronicInspectorApiErrorHandler
extends BaseHttpRequestErrorHandler {
_generateErrors(errorObj) {
let errors = [];
let error;
// A weak check to find out if it's not an xhr object.
if (!errorObj.status && errorObj.message) {
errors.push({
title: 'Error',
message: errorObj.message
});
return errors;
}
switch (errorObj.status) {
case 0:
errors.push({
title: 'Connection Error',
message: 'Connection to Ironic Inspector is not available'
});
break;
case 404:
error = JSON.parse(errorObj.responseText).error.message;
errors.push({
title: 'Introspection data not found',
message: error
});
break;
case 401:
error = JSON.parse(errorObj.responseText).error.message;
errors.push({
title: 'Unauthorized',
message: error
});
store.dispatch(LoginActions.logoutUser());
break;
default:
error = JSON.parse(errorObj.responseText).error.message;
status = errorObj.status;
errors.push({
title: `Error ${status}`,
message: error
});
break;
}
return errors;
}
// TODO(jtomasek): remove this, I am leaving this here just for example reasons
// this function should be implemented by form related subclass.
_generateFormFieldErrors() {
return {};
}
}