diff --git a/dashboard/frontend/package.json b/dashboard/frontend/package.json index 8f7f28d6..e9f59fab 100644 --- a/dashboard/frontend/package.json +++ b/dashboard/frontend/package.json @@ -1,7 +1,7 @@ { - "name": "react-starter", + "name": "simengine", "version": "1.0.0", - "description": "A basic template that consists of the essential elements that are required to start building a React application", + "description": "Dashboard for SimEngine platform", "scripts": { "build": "webpack", "build:dev": "npm run build -- --env.env=dev", @@ -20,9 +20,9 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/drminnaar/react-starter.git" + "url": "git+https://github.com/Seneca-CDOT/simengine.git" }, - "license": "MIT", + "license": "GPL", "babel": { "presets": [ "env", diff --git a/dashboard/frontend/src/components/App.js b/dashboard/frontend/src/components/App.js index 6b436a72..0de73613 100644 --- a/dashboard/frontend/src/components/App.js +++ b/dashboard/frontend/src/components/App.js @@ -1,29 +1,25 @@ import React, { Component } from 'react'; -import { Stage, Layer, Line } from 'react-konva'; -import gridBackground from '../images/grid.png'; +import { Stage } from 'react-konva'; import PropTypes from 'prop-types'; // Material import { withStyles } from '@material-ui/core/styles'; -import Snackbar from '@material-ui/core/Snackbar'; // Local Components - Layout -import { Server, Pdu, Ups, Socket, Lamp } from './Assets'; // Text & info boxes import AssetDetails from './AssetDetails'; -import TopNav from './TopNav'; +import TopNav from './Navigation/TopNav'; +import Canvas from './Canvas'; +import Notifications from './Notifications'; // few helpers import { onWheelScroll, onWheelDown } from './canvasEvents'; import simengineSocketClient from './socketClient'; +import styles from './App.styles'; -import colors from '../styles/colors'; - -const drawerWidth = 240; - - class App extends Component { +class App extends Component { constructor() { super(); @@ -31,7 +27,7 @@ const drawerWidth = 240; this.state = { assets: null, selectedAssetKey: 0, - connections:{}, + connections: {}, ambient: 0, ambientRising: false, mainsStatus: 1, @@ -92,7 +88,7 @@ const drawerWidth = 240; const isComponent = !this.state.assets[data.key]; if (isComponent) { - const parentId = this._get_parent_key(data.key); + const parentId = this._getParentKey(data.key); let assetDetails = {...assets[parentId].children[data.key]}; assets[parentId].children[data.key] = {...assetDetails, ...data}; } else { @@ -125,28 +121,19 @@ const drawerWidth = 240; }); } - _get_parent_key(key) { + _getParentKey(key) { const strkey = (''+key); return strkey.substring(0, strkey.length===1?1:strkey.length-1); } - _get_asset_by_key(key) { - if (key && !this.state.assets[key]) { - const parent_key = this._get_parent_key(key); - return this.state.assets[parent_key].children[key]; - } else { - return this.state.assets[key]; - } - } + _updateWiring(asset, key, coord) { - _update_wiring(asset, key, coord) { - let newConn = {}; const connections = this.state.connections; if(asset['parent']) { asset['parent'].forEach((p, idx) => { - newConn[p.key] = {...connections[p.key], destX:coord[idx].x, destY:coord[idx].y }; + newConn[p.key] = { ...connections[p.key], destX:coord[idx].x, destY:coord[idx].y }; }); } else if (key in connections && coord[0]) { newConn[key] = { ...connections[key], sourceX:coord[0].x, sourceY:coord[0].y }; @@ -155,14 +142,24 @@ const drawerWidth = 240; return newConn; } + getAssetByKey = (key) => { + if (key && !this.state.assets[key]) { + const parentKey = this._getParentKey(key); + return this.state.assets[parentKey].children[key]; + } else { + return this.state.assets[key]; + } + } + + /** Update connections between assets (wires) */ - onPosChange(key, coord) { + onPosChange = (key, coord) => { - const asset = this._get_asset_by_key(key); + const asset = this.getAssetByKey(key); // find all the incoming connections as well as output wiring const connections = this.state.connections; - let newConn = this._update_wiring(asset, key, coord.inputConnections.map((c)=>c={x: c.x+coord.x, y: c.y+coord.y})); + let newConn = this._updateWiring(asset, key, coord.inputConnections.map((c)=>c={x: c.x+coord.x, y: c.y+coord.y})); let childConn = {}; let assets = {...this.state.assets}; @@ -174,36 +171,36 @@ const drawerWidth = 240; // output wiring if (asset.children) { for (const ckey of Object.keys(coord.outputConnections)) { - const c = this._update_wiring( - this._get_asset_by_key(ckey), ckey, [{x: coord.x + coord.outputConnections[ckey].x, y: coord.y + coord.outputConnections[ckey].y}] + const c = this._updateWiring( + this.getAssetByKey(ckey), ckey, [{ x: coord.x + coord.outputConnections[ckey].x, y: coord.y + coord.outputConnections[ckey].y }] ); Object.assign(childConn, c); } } - this.setState({ assets, connections: {...connections, ...newConn, ...childConn }}); + this.setState({ assets, connections: { ...connections, ...newConn, ...childConn } }); } - /** Handle Asset Selection */ - onElementSelection(assetKey, assetInfo) { + /** Handle Asset Selection (deselect on second click, select asset otherwise) */ + onElementSelection = (asset) => { this.setState((oldState) => { return { - selectedAssetKey: oldState.selectedAssetKey === assetKey ? 0 : assetKey, - selectedAsset: assetInfo + selectedAssetKey: oldState.selectedAssetKey === asset.key ? 0 : asset.key, + selectedAsset: asset }; }); } /** Send a status change request */ - changeStatus(assetKey, assetInfo) { - let data = {...assetInfo}; + changeStatus = (asset) => { + let data = {...asset}; data.status = !data.status; - this.ws.sendData({ request: 'power', key: assetKey, data }); + this.ws.sendData({ request: 'power', key: asset.key, data }); } /** Save assets' coordinates in db */ - saveLayout() { + saveLayout = () => { let data = {}; const { assets } = this.state; @@ -218,165 +215,33 @@ const drawerWidth = 240; data['assets'] = {}; // add asset layout info - Object.keys(assets).map((a) => ( data['assets'][a]={ x: assets[a].x, y: assets[a].y })); + Object.keys(assets).map((a) => ( data['assets'][a]={ x: assets[a].x, y: assets[a].y } )); if (this.ws.socketOnline()) { this.ws.sendData({request: 'layout', data }); this.setState({ changesSaved: true }); + setTimeout(() => { + this.setState({ changesSaved: false }); + }, 5000); } } - /** Add Socket to the Layout */ - drawSocket(key, asset) { - const powered = asset.parent?this._get_asset_by_key(asset.parent[0].key).status:true; - return ( - ); - } - - - /** Add PDU to the Layout */ - drawPdu(key, asset) { - const powered = asset.parent?this._get_asset_by_key(asset.parent[0].key).status:false; - return ( - ); - } - - /* Add Server to the layout */ - drawServer(key, asset) { - let powered = false; - if (asset.parent) { - powered = asset.parent.find((x) => this._get_asset_by_key(x.key).status != 0) !== undefined; - } - - return ( - ); - } - - /* Add Ups to the layout */ - drawUps(key, asset) { - let powered = false; - if (asset.parent) { - powered = asset.parent.find((x) => this._get_asset_by_key(x.key).status != 0) !== undefined; - } - - return ( - ); - } - - drawLamp(key, asset) { - - let powered = false; - if (asset.parent) { - powered = asset.parent.find((x) => this._get_asset_by_key(x.key).status != 0) !== undefined; - } - - return ( - - ) - } - - render() { const { classes } = this.props; const { assets, connections } = this.state; // currently selected asset - const selectedAsset = assets ? this._get_asset_by_key(this.state.selectedAssetKey) : null; - - // asset drawings & their connections - let systemLayout = []; - let wireDrawing = []; - - if (assets) { - // Initialize HA system layout - for (const key of Object.keys(assets)) { - if (assets[key].type == 'outlet' || assets[key].type === 'staticasset') { - systemLayout.push(this.drawSocket(key, assets[key])); - } else if (assets[key].type === 'pdu') { - systemLayout.push(this.drawPdu(key, assets[key])); - } else if (assets[key].type === 'server' || assets[key].type === 'serverwithbmc') { - systemLayout.push(this.drawServer(key, assets[key])); - } else if (assets[key].type === 'ups') { - systemLayout.push(this.drawUps(key, assets[key])); - } else if (assets[key].type === 'lamp') { - systemLayout.push(this.drawLamp(key, assets[key])); - } - } - - // draw wires - for (const key of Object.keys(connections)) { - const asset = this._get_asset_by_key(key); - - wireDrawing.push( - - ); - } - } - - const snackbarOrigin = {vertical: 'bottom', horizontal: 'left',}; + const selectedAsset = assets ? this.getAssetByKey(this.state.selectedAssetKey) : null; + + // configure app's notifications: + const snackbarOrigin = { vertical: 'bottom', horizontal: 'left', }; + const displayedSnackbars = { + socketOffline: this.state.socketOffline, + changesSaved: this.state.changesSaved, + layoutEmpty: !this.state.socketOffline && !assets, + }; return (
@@ -384,7 +249,7 @@ const drawerWidth = 240; {/* Top-Navigation component */} {/* Main Canvas */} -
+
{/* Drawings */} @@ -402,88 +267,33 @@ const drawerWidth = 240; height={window.innerHeight * 0.88} ref="stage" > - - {systemLayout} - {wireDrawing} - + {/* RightMost Card -> Display Element Details */} - {(this.state.selectedAssetKey) ? - : '' + {!!this.state.selectedAssetKey && + } - - {/* Display message if backend is not available */} - Socket is unavailable: trying to reconnect...} - /> - - {/* 'Changes Applied'/'Saved' Message */} - this.setState({changesSaved: false})} - autoHideDuration={1500} - message={Changes saved!} - /> - {/* The layout was not initialized -> display link to the documentation*/} - The system toplology appears to be empty.
Please, refer to the documentation - (System Modelling link)} - /> + {/* Bottom-Left corner pop-ups */} +
); } } -const styles = theme => ({ - root: { - flexGrow: 1, - }, - appFrame: { - zIndex: 1, - overflow: 'hidden', - position: 'relative', - display: 'flex', - width: '100%', - }, - appBar: { - width: `100%`, - }, - 'appBar-left': { - backgroundColor: "#36454F", - marginLeft: drawerWidth, - }, - drawerPaper: { - width: drawerWidth - }, - toolbar: theme.mixins.toolbar, - content: { - flexGrow: 1, - backgroundColor: theme.palette.background.default, - padding: theme.spacing.unit * 3, - }, - menuButton: { - marginLeft: -12, - marginRight: 20, - }, - list: { - width: 250, - }, -}); - App.propTypes = { - classes: PropTypes.object, // stype + classes: PropTypes.object, }; diff --git a/dashboard/frontend/src/components/App.styles.js b/dashboard/frontend/src/components/App.styles.js new file mode 100644 index 00000000..381b7320 --- /dev/null +++ b/dashboard/frontend/src/components/App.styles.js @@ -0,0 +1,44 @@ +import gridBackground from '../images/grid.png'; + +const drawerWidth = 240; + +const styles = theme => ({ + root: { + flexGrow: 1, + }, + appFrame: { + zIndex: 1, + overflow: 'hidden', + position: 'relative', + display: 'flex', + width: '100%', + }, + appBar: { + width: `100%`, + }, + 'appBar-left': { + backgroundColor: "#36454F", + marginLeft: drawerWidth, + }, + drawerPaper: { + width: drawerWidth + }, + toolbar: theme.mixins.toolbar, + content: { + flexGrow: 1, + backgroundColor: theme.palette.background.default, + backgroundImage: 'url('+gridBackground+')', + backgroundRepeat: "repeat", + backgroundSize: "auto", + padding: theme.spacing.unit * 3, + }, + menuButton: { + marginLeft: -12, + marginRight: 20, + }, + list: { + width: 250, + }, +}); + +export default styles; \ No newline at end of file diff --git a/dashboard/frontend/src/components/AssetDetails.js b/dashboard/frontend/src/components/AssetDetails.js index 81885c71..881c40d0 100644 --- a/dashboard/frontend/src/components/AssetDetails.js +++ b/dashboard/frontend/src/components/AssetDetails.js @@ -1,29 +1,26 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/core/styles'; -import Card from '@material-ui/core/Card'; -import CardContent from '@material-ui/core/CardContent'; -import CardHeader from '@material-ui/core/CardHeader'; -import Divider from '@material-ui/core/Divider'; -import Typography from '@material-ui/core/Typography'; +import { Card, CardContent, CardHeader, Divider, Typography } from '@material-ui/core'; + +// ** local imports import PowerSwitch from './common/PowerSwitch'; -function AssetDetails(props) { - const { classes, assetInfo, assetKey, changeStatus } = props; +const AssetDetails = ({ classes, asset, changeStatus }) => { + let children = []; - if(assetInfo.children) { - children.push( -

Connected Components

- ); + if(asset.children) { + children.push(

Connected Components

); - const c = assetInfo.children; + const c = asset.children; for (const ckey of Object.keys(c)) { + const childStatus = c[ckey].status === 1?(on):(off); children.push(
- {ckey}-{c[ckey].type} is {c[ckey].status === 1?on:off} + {ckey}-{c[ckey].type} is {childStatus}
); @@ -39,26 +36,25 @@ function AssetDetails(props) { /> - Asset: {assetKey}-{assetInfo.type} + Asset: {asset.key}-{asset.type} - Status: {assetInfo.status === 1?on:off} + Status: {asset.status === 1?on:off} - Name: {assetInfo.name} + Name: {asset.name} - - Current Load: {assetInfo.load ? assetInfo.load.toFixed(2): 0} Amp + Current Load: {asset.load ? asset.load.toFixed(2): 0} Amp - {/* Turn off/on the component */} - changeStatus(assetKey, assetInfo)} - label={Toggle Status} - /> + {/* Turn off/on the component */} + changeStatus(asset)} + label={Toggle Status} + /> {/* Display any nested elements */}
@@ -68,12 +64,11 @@ function AssetDetails(props) {
); -} +}; AssetDetails.propTypes = { classes: PropTypes.object.isRequired, - assetInfo: PropTypes.object.isRequired, - assetKey: PropTypes.string.isRequired, + asset: PropTypes.object.isRequired, changeStatus: PropTypes.func.isRequired, // Change asset state }; diff --git a/dashboard/frontend/src/components/Assets/Lamp/Lamp.js b/dashboard/frontend/src/components/Assets/Lamp/Lamp.js index bc26ffc0..d0bbe389 100644 --- a/dashboard/frontend/src/components/Assets/Lamp/Lamp.js +++ b/dashboard/frontend/src/components/Assets/Lamp/Lamp.js @@ -19,39 +19,39 @@ const SCALE = 0.7; class Lamp extends Asset { - constructor(props) { - super(props); - this.state = { - // graphics - lampImg: null, - lampOffImg: null, - backgroundImg: null, - }; - } - - /** Load Lamp Image */ - componentDidMount() { - Promise.all(this.loadImages({ lampImg: lampSource, lampOffImg: lampOffSource })).then(() => { - this.props.onPosChange(this.props.assetId, this.formatAssetCoordinates(this.props)); - }); - } - - getInputCoordinates = (center=true) => [ - (center&&this.state.lampImg)?{ x: this.state.lampImg.width*0.5*SCALE, y: this.state.lampImg.height*SCALE, }:{ x: 0, y: 0 } - ]; - - render() { - - const { lampImg, lampOffImg } = this.state; - // const strokeColor = (this.props.selected) ? colors.selectedAsset: colors.deselectedAsset; - - return( - - {/* Outlet Image */} - - - ); - } + constructor(props) { + super(props); + this.state = { + // graphics + lampImg: null, + lampOffImg: null, + backgroundImg: null, + }; + } + + /** Load Lamp Image */ + componentDidMount() { + Promise.all(this.loadImages({ lampImg: lampSource, lampOffImg: lampOffSource })).then(() => { + this.props.onPosChange(this.props.asset.key, this.formatAssetCoordinates(this.props)); + }); + } + + getInputCoordinates = (center=true) => [ + (center&&this.state.lampImg)?{ x: this.state.lampImg.width*0.5*SCALE, y: this.state.lampImg.height*SCALE, }:{ x: 0, y: 0 } + ]; + + render() { + + const { lampImg, lampOffImg } = this.state; + // const strokeColor = (this.props.selected) ? colors.selectedAsset: colors.deselectedAsset; + + return( + + {/* Outlet Image */} + + + ); + } } diff --git a/dashboard/frontend/src/components/Assets/PDU/Pdu.js b/dashboard/frontend/src/components/Assets/PDU/Pdu.js index f4d57681..ab375365 100644 --- a/dashboard/frontend/src/components/Assets/PDU/Pdu.js +++ b/dashboard/frontend/src/components/Assets/PDU/Pdu.js @@ -34,7 +34,7 @@ export default class Pdu extends OutputAsset { Promise.all(this.loadImages({ c14Img: c14Source })) .then(Socket.socketSize) .then((size) => this.setState({ socketSize: size })) - .then(() => this.props.onPosChange(this.props.assetId, this.formatAssetCoordinates(this.props))); + .then(() => this.props.onPosChange(this.props.asset.key, this.formatAssetCoordinates(this.props))); } getOutputCoordinates = (center=true) => { diff --git a/dashboard/frontend/src/components/Assets/Server/Server.js b/dashboard/frontend/src/components/Assets/Server/Server.js index 599d8ea3..cda81ed7 100644 --- a/dashboard/frontend/src/components/Assets/Server/Server.js +++ b/dashboard/frontend/src/components/Assets/Server/Server.js @@ -32,13 +32,13 @@ export default class Server extends Asset { Promise.all(this.loadImages({ serverPlaceholderImg: serverPlaceholderSource})) .then(PowerSupply.psuSize) .then((size) => { this.setState({ psuSize: size }); }) - .then(() => this.props.onPosChange(this.props.assetId, this.formatAssetCoordinates(this.props))); + .then(() => this.props.onPosChange(this.props.asset.key, this.formatAssetCoordinates(this.props))); } /** Notify top-lvl Component that on of the PSUs was selected*/ selectPSU = (ckey) => { this.setState({ selectedPsuKey: ckey }); - this.props.onElementSelection(ckey, this.props.asset.children[ckey]); + this.props.onElementSelection(this.props.asset.children[ckey]); } getOutputCoordinates = () => { return {}; } @@ -72,7 +72,6 @@ export default class Server extends Asset { onElementSelection={() => { this.selectPSU(ckey); }} draggable={false} asset={asset.children[ckey]} - assetId={ckey} selected={this.state.selectedPsuKey === ckey && this.props.nestedComponentSelected} powered={this.props.powered} parentSelected={this.props.selected} diff --git a/dashboard/frontend/src/components/Assets/UPS/Ups.js b/dashboard/frontend/src/components/Assets/UPS/Ups.js index c86a712b..5e8ceb3e 100644 --- a/dashboard/frontend/src/components/Assets/UPS/Ups.js +++ b/dashboard/frontend/src/components/Assets/UPS/Ups.js @@ -36,7 +36,7 @@ export default class Ups extends OutputAsset { Promise.all(this.loadImages({ upsMonitorImg: upsMonitorSource, c14Img: c14Source })) .then(Socket.socketSize) .then((size) => { this.setState({ socketSize: size }); }) - .then(() => this.props.onPosChange(this.props.assetId, this.formatAssetCoordinates(this.props))); + .then(() => this.props.onPosChange(this.props.asset.key, this.formatAssetCoordinates(this.props))); } getOutputCoordinates = (center=true) => { diff --git a/dashboard/frontend/src/components/Assets/common/Asset.js b/dashboard/frontend/src/components/Assets/common/Asset.js index 69775da9..5490a707 100644 --- a/dashboard/frontend/src/components/Assets/common/Asset.js +++ b/dashboard/frontend/src/components/Assets/common/Asset.js @@ -7,9 +7,10 @@ import PropTypes from 'prop-types'; * */ class Asset extends React.Component { - /** Load images into state (returns array of promises) */ + loadImages = (assetImages) => { - + /** Load images into state (returns array of promises) */ + let imagePromises = []; for (const [imageName, imageSource] of Object.entries(assetImages)) { @@ -32,22 +33,24 @@ class Asset extends React.Component { getInputCoordinates = () => [] formatAssetCoordinates = ({x, y}) => ({ - x: x, - y: y, - inputConnections: this.getInputCoordinates(), - outputConnections: this.getOutputCoordinates(), + x: x, // asset position -> x + y: y, // asset position -> y + + // i/o coordinates are relative to the asset { x, y } coordinates + inputConnections: this.getInputCoordinates(), // input power position { x, y } + outputConnections: this.getOutputCoordinates(), // output power position { x, y } (if supported) }); /** Notify Parent of Selection */ handleClick = () => { this.refs.asset.setZIndex(100); - this.props.onElementSelection(this.props.assetId, this.props.asset); + this.props.onElementSelection(this.props.asset); }; /** returns global asset position (x, y), relative output & input outlet coordinates */ updateAssetPos = (s) => { const coord = this.formatAssetCoordinates(s.target.attrs); - this.props.onPosChange(this.props.assetId, coord); + this.props.onPosChange(this.props.asset.key, coord); } } @@ -57,7 +60,6 @@ Asset.propTypes = { x: PropTypes.number, // X position of the asset y: PropTypes.number, // Y position of the asset asset: PropTypes.object.isRequired, // Asset Details - assetId: PropTypes.string.isRequired, // Asset Key selected: PropTypes.bool.isRequired, // Asset Selected by a user powered: PropTypes.bool.isRequired, // indicates if upstream power is present diff --git a/dashboard/frontend/src/components/Assets/common/OutputAsset.js b/dashboard/frontend/src/components/Assets/common/OutputAsset.js index 665813ff..e29affab 100644 --- a/dashboard/frontend/src/components/Assets/common/OutputAsset.js +++ b/dashboard/frontend/src/components/Assets/common/OutputAsset.js @@ -24,7 +24,7 @@ class OutputAsset extends Asset { /** Notify top-lvl Component that OUT-outlet was selected*/ selectSocket = (ckey) => { this.setState({ selectedSocketKey: ckey }); - this.props.onElementSelection(ckey, this.props.asset.children[ckey]); + this.props.onElementSelection(this.props.asset.children[ckey]); } getOutputSockets = (hideName=false) => { @@ -39,7 +39,6 @@ class OutputAsset extends Asset { x={outputCoord[ckey].x} y={outputCoord[ckey].y} asset={this.props.asset.children[ckey]} - assetId={ckey} key={ckey} onElementSelection={() => { this.selectSocket(ckey); }} diff --git a/dashboard/frontend/src/components/Assets/common/Socket.js b/dashboard/frontend/src/components/Assets/common/Socket.js index f311801a..68e61ddf 100644 --- a/dashboard/frontend/src/components/Assets/common/Socket.js +++ b/dashboard/frontend/src/components/Assets/common/Socket.js @@ -17,76 +17,76 @@ import colors from '../../../styles/colors'; */ class Socket extends Asset { - constructor(props) { - super(props); - this.state = { - // graphics - socketImg: null, - backgroundImg: null, - }; - } - - /** Load Socket Image */ - componentDidMount() { - const backgroundImg = 'imgUrl' in this.props.asset?this.props.asset['imgUrl']:null; - - Promise.all(this.loadImages({ socketImg: socketSource, backgroundImg })).then(() => { - let { backgroundImg } = this.state; - if (backgroundImg) { - // resize the image - backgroundImg.width = backgroundImg.width / (backgroundImg.height/160); - backgroundImg.height = 160; - this.setState({ backgroundImg }); - } + constructor(props) { + super(props); + this.state = { + // graphics + socketImg: null, + backgroundImg: null, + }; + } + + /** Load Socket Image */ + componentDidMount() { + const backgroundImg = 'imgUrl' in this.props.asset?this.props.asset['imgUrl']:null; + + Promise.all(this.loadImages({ socketImg: socketSource, backgroundImg })).then(() => { + let { backgroundImg } = this.state; + if (backgroundImg) { + // resize the image + backgroundImg.width = backgroundImg.width / (backgroundImg.height/160); + backgroundImg.height = 160; + this.setState({ backgroundImg }); + } + + this.props.onPosChange(this.props.asset.key, this.formatAssetCoordinates(this.props)); + }); + } + + getInputCoordinates = (center=true) => [ + (center&&this.state.socketImg)?{ x: this.state.socketImg.width*0.5, y: this.state.socketImg.height*0.5, }:{ x: 0, y: 0 } + ]; + + render() { + + const { backgroundImg, socketImg } = this.state; + + // Selected when either parent element (e.g. PDU outlet belongs to) is selected + // or the socket was selected + const strokeColor = (this.props.selected || this.props.parentSelected) ? colors.selectedAsset: colors.deselectedAsset; + + return( + + + {/* Optional background image */} + {backgroundImg && } + + {/* Outlet Image */} + + + {/* LED */} + - this.props.onPosChange(this.props.assetId, this.formatAssetCoordinates(this.props)); - }); - } - - getInputCoordinates = (center=true) => [ - (center&&this.state.socketImg)?{ x: this.state.socketImg.width*0.5, y: this.state.socketImg.height*0.5, }:{ x: 0, y: 0 } - ]; - - render() { - - const { backgroundImg, socketImg } = this.state; - - // Selected when either parent element (e.g. PDU outlet belongs to) is selected - // or the socket was selected - const strokeColor = (this.props.selected || this.props.parentSelected) ? colors.selectedAsset: colors.deselectedAsset; - - return( - - - {/* Optional background image */} - {backgroundImg && } - - {/* Outlet Image */} - - - {/* LED */} - - - {/* Socket title */} - {!this.props.hideName && - - } - - - ); - } + {/* Socket title */} + {!this.props.hideName && + + } + + + ); + } } Socket.socketSize = () => { diff --git a/dashboard/frontend/src/components/Canvas.js b/dashboard/frontend/src/components/Canvas.js index e69de29b..f8dd95c8 100644 --- a/dashboard/frontend/src/components/Canvas.js +++ b/dashboard/frontend/src/components/Canvas.js @@ -0,0 +1,101 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Layer, Line } from 'react-konva'; + +import { Server, Pdu, Ups, Socket, Lamp } from './Assets'; + +import colors from '../styles/colors'; + + +class Canvas extends Component { + + // map asset types to react components + assetMap = { + 'outlet': Socket, + 'staticasset': Socket, + 'server': Server, + 'serverwithbmc': Server, + 'pdu': Pdu, + 'ups': Ups, + 'lamp': Lamp, + }; + + + getAssetComponent(ReactElement, asset) { + /**Render asset element (on of the components defined in 'assetMap') */ + + // asset props + let elementProps = { + onPosChange: this.props.onPosChange.bind(this), + onElementSelection: this.props.onElementSelection.bind(this), + key: asset.key, + asset: asset, + selected: this.props.selectedAssetKey === asset.key, + isComponent: false, + powered: false, + x: asset.x, + y: asset.y, + fontSize: 14, + }; + + // check if upstream power source is present + const upstreamPowered = (x) => this.props.getAssetByKey(x.key).status != 0; + elementProps['powered'] = asset.parent?(asset.parent.find(upstreamPowered) !== undefined):(true); + + // select child elements + if ('children' in asset) { + elementProps['nestedComponentSelected'] = this.props.selectedAssetKey in asset.children; + } + + return React.createElement(ReactElement, elementProps); + } + + + render() { + const { assets, connections } = this.props; + + // asset drawings & their connections + let systemLayout = []; + let wireDrawing = []; + + if (assets) { + // Initialize HA system layout + for (const key of Object.keys(assets)) { + systemLayout.push(this.getAssetComponent(this.assetMap[assets[key].type], assets[key])); + } + + // draw wires + for (const key of Object.keys(connections)) { + const asset = this.props.getAssetByKey(key); + + wireDrawing.push( + + ); + } + } + + return ( + + {systemLayout} + {wireDrawing} + + ); + } +} + +Canvas.propTypes = { + assets: PropTypes.object, + connections: PropTypes.object, + selectedAssetKey: PropTypes.number, + onPosChange: PropTypes.func.isRequired, // called on element dragged + onElementSelection: PropTypes.func.isRequired, // called when asset is selected + getAssetByKey: PropTypes.func.isRequired, // retrieve asset props by key +}; + +export default Canvas; diff --git a/dashboard/frontend/src/components/Navigation/SettingsOption.js b/dashboard/frontend/src/components/Navigation/SettingsOption.js new file mode 100644 index 00000000..641c95de --- /dev/null +++ b/dashboard/frontend/src/components/Navigation/SettingsOption.js @@ -0,0 +1,76 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; + +import { Settings } from "@material-ui/icons"; +import { IconButton, Divider, Drawer, List, ListItem, ListItemText } from '@material-ui/core'; + +/** + * Drawer option/settings + */ +class SettingsOption extends React.Component { + + constructor(props) { + super(props); + this.state = { drawerAnchor: null,}; + } + + openDrawer = event => this.setState({ drawerAnchor: event.currentTarget }); + handleDrawerClose = () => this.setState({ drawerAnchor: null }); + + render() { + + const { classes } = this.props; + + const { drawerAnchor } = this.state; + const drawerOpen = Boolean(drawerAnchor); + + return ( + + {/* Button to open up a menu */} + + + + + {/* Sidebar menu */} + +
+ +
+ {/* Sidebar options */} + + + + + window.open('https://simengine.readthedocs.io/en/latest/')}> + + + +
+ + + ); + } +} + + +SettingsOption.propTypes = { + classes: PropTypes.object, // styling + saveLayout: PropTypes.func.isRequired, // drawer Save Layout callback +}; + +export default SettingsOption; diff --git a/dashboard/frontend/src/components/Navigation/SysStatusOption.js b/dashboard/frontend/src/components/Navigation/SysStatusOption.js new file mode 100644 index 00000000..38b329c4 --- /dev/null +++ b/dashboard/frontend/src/components/Navigation/SysStatusOption.js @@ -0,0 +1,87 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { AcUnit, PowerSettingsNew, ArrowDownward, ArrowUpward } from "@material-ui/icons"; +import { Grid, Typography, Fade } from '@material-ui/core'; + +// local imports +import PowerSwitch from '../common/PowerSwitch'; +import colors from '../../styles/colors'; + +/** + * Top-Right Nav options + */ +const SysStatusOption = ({ mainsStatus, ambient, ambientRising, flash, togglePower }) => { + return ( + + + {/* Wall power status */} + + togglePower(!mainsStatus)} + label={ + + + The Mains: + + {" "}{mainsStatus?"online":"offline"} + + + } + /> + + + {/* Ambient Temperature */} + + + + 27)?styles.heating:styles.cooling}>{ambient}° + + {ambientRising + ? + : + } + + + + + + ); +}; + +const styles = { + inlineIcon: { + marginRight: '0.3em', + marginBottom: '-0.2em', + fontSize: 22 + }, + cooling: { + color: colors.blue + }, + heating: { + color: colors.red + }, + online: { + color: colors.green + }, + menuOptions: { + padding: '0.7em', + }, + tempGauge: { + borderColor:"white", + borderLeftStyle: 'solid', + } +}; + + +SysStatusOption.propTypes = { + classes: PropTypes.object, // styling + togglePower: PropTypes.func.isRequired, // on mains update + ambient: PropTypes.number.isRequired, // room temp + ambientRising: PropTypes.bool.isRequired, // is room temp going up? + mainsStatus: PropTypes.bool.isRequired, // mains power source status + flash: PropTypes.bool.isRequired, // indicates if temp arrow should be flashing +}; + +export default SysStatusOption; diff --git a/dashboard/frontend/src/components/Navigation/TopNav.js b/dashboard/frontend/src/components/Navigation/TopNav.js new file mode 100644 index 00000000..bdaede0a --- /dev/null +++ b/dashboard/frontend/src/components/Navigation/TopNav.js @@ -0,0 +1,110 @@ +import React, { Fragment } from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; + +// Material imports +import { withStyles } from '@material-ui/core/styles'; +import { AppBar, Toolbar, Typography } from '@material-ui/core'; + +// local imports +import SettingsOption from './SettingsOption'; +import SysStatusOption from './SysStatusOption'; + + +class TopNav extends React.Component { + + constructor(props) { + super(props); + + this.state = { + ambientRising: false, + lastTempChange: new Date(), + flash: false, + }; + + this.flashArrow = null; + } + + componentWillReceiveProps(newProps) { + /** Flash temperature arrow */ + + if (newProps.ambient == this.props.ambient) { return; } + const elapsedSinceLastTemp = new Date() - this.state.lastTempChange; + + clearInterval(this.flashArrow); + + // flash arrow icon on temp changes + this.flashArrow = setInterval(() => { + this.setState(() => ({flash: true})); + setTimeout(() => { + this.setState(() => ({flash: false})); + }, elapsedSinceLastTemp*0.5*0.8); + }, elapsedSinceLastTemp*0.5); + + + // stop flashing arrow after a while (max is 1 minute) + const maxFlashingTime = 60*1000; + + setTimeout(() => { + clearInterval(this.flashArrow); + this.setState({ flash: false, ambientRising: false }); + + }, (elapsedSinceLastTemp > maxFlashingTime?maxFlashingTime:elapsedSinceLastTemp)); + + this.setState({ + ambientRising: newProps.ambientRising, + lastTempChange: new Date(), + }); + } + + render() { + const { classes } = this.props; + + return ( + + + + + HAos Simulation Engine + + {/* Gear openning a drawer */} +
+ +
+ + {/* Top-right nav options*/} +
+ +
+
+
+
+ ); + } +} + +const styles = { + grow: { + flexGrow: 1, + }, +}; + +TopNav.propTypes = { + classes: PropTypes.object, // styling + saveLayout: PropTypes.func.isRequired, // drawer Save Layout callback + ambient: PropTypes.number.isRequired, // room temp + ambientRising: PropTypes.bool.isRequired, // is room temp going up? + mainsStatus: PropTypes.bool.isRequired, // mains power source status + togglePower: PropTypes.func.isRequired, +}; + +export default withStyles(styles)(TopNav); diff --git a/dashboard/frontend/src/components/Notifications.js b/dashboard/frontend/src/components/Notifications.js new file mode 100644 index 00000000..62f8e1ba --- /dev/null +++ b/dashboard/frontend/src/components/Notifications.js @@ -0,0 +1,42 @@ + +import React from 'react'; +import Snackbar from '@material-ui/core/Snackbar'; +import PropTypes from 'prop-types'; + + +const Notifications = ({ anchorOrigin, displayedSnackbars }) => { + + const open = !!Object.keys(displayedSnackbars).find((k)=>displayedSnackbars[k]); + let snackbarMessage = ''; + + if (displayedSnackbars.socketOffline) { + snackbarMessage = Socket is unavailable: trying to reconnect...; + + } else if (displayedSnackbars.changesSaved) { + snackbarMessage = Changes saved!; + + } else if (displayedSnackbars.layoutEmpty) { + snackbarMessage = ( + + The system toplology appears to be empty.
+ Please, refer to the documentation (System Modelling   + link) +
+ ); + } + + return ( + + ); +}; + +Notifications.propTypes = { + anchorOrigin: PropTypes.object.isRequired, // notification position + displayedSnackbars: PropTypes.object.isRequired, // indicates what snackbar message +}; + +export default Notifications; diff --git a/dashboard/frontend/src/components/TopNav.js b/dashboard/frontend/src/components/TopNav.js deleted file mode 100644 index 6a52fd31..00000000 --- a/dashboard/frontend/src/components/TopNav.js +++ /dev/null @@ -1,208 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import PropTypes from 'prop-types'; - -// Material imports -import { withStyles } from '@material-ui/core/styles'; -import { Settings, AcUnit, PowerSettingsNew, ArrowDownward, ArrowUpward } from "@material-ui/icons"; -// import s from "@material-ui/icons/ArrowUpward"; -import AppBar from '@material-ui/core/AppBar'; -import Toolbar from '@material-ui/core/Toolbar'; -import IconButton from '@material-ui/core/IconButton'; -import Typography from '@material-ui/core/Typography'; -import Grid from '@material-ui/core/Grid'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import { Divider, Drawer, Fade } from '@material-ui/core'; - -// local imports -import colors from '../styles/colors'; -import PowerSwitch from './common/PowerSwitch'; - - -class TopNav extends React.Component { - - constructor(props) { - super(props); - - this.state = { - drawerAnchor: null, - ambientRising: false, - lastTempChange: new Date(), - flash: false, - }; - - this.flashArrow = null; - } - - componentWillReceiveProps(newProps) { - - if (newProps.ambient == this.props.ambient) { return; } - const elapsedSinceLastTemp = new Date() - this.state.lastTempChange; - - clearInterval(this.flashArrow); - - // flash arrow icon on temp changes - this.flashArrow = setInterval(() => { - this.setState(() => ({flash: true})); - setTimeout(() => { - this.setState(() => ({flash: false})); - }, elapsedSinceLastTemp*0.5*0.8); - }, elapsedSinceLastTemp*0.5); - - - // stop flashing arrow after a while (max is 1 minute) - const maxFlashingTime = 60*1000; - - setTimeout(() => { - clearInterval(this.flashArrow); - this.setState({ flash: false, ambientRising: false }); - - }, (elapsedSinceLastTemp > maxFlashingTime?maxFlashingTime:elapsedSinceLastTemp)); - - this.setState({ - ambientRising: newProps.ambientRising, - lastTempChange: new Date(), - }); - } - - handleMenu = event => { - this.setState({ drawerAnchor: event.currentTarget }); - }; - - handleDrawerClose = () => { - this.setState({ drawerAnchor: null }); - }; - - render() { - - const { classes } = this.props; - - const { drawerAnchor } = this.state; - const drawerOpen = Boolean(drawerAnchor); - - return ( -
- - - - HAos Simulation Engine - -
- - - - -
- -
-
- - - - - -
-
- -
-
- - - this.props.togglePower(!this.props.mainsStatus)} - label={ - - - The Mains: - - {" "}{this.props.mainsStatus?"online":"offline"} - - - } - /> - - - - - 27)?styles.heating:styles.cooling}>{this.props.ambient}° - - {this.state.ambientRising - ? - : - } - - - - - -
- - - -
- ); - } -} - - -const styles = { - root: { - flexGrow: 1, - }, - inlineIcon: { - marginRight: '0.3em', - marginBottom: '-0.2em', - fontSize: 22 - }, - grow: { - flexGrow: 1, - }, - cooling: { - color: colors.blue - }, - heating: { - color: colors.red - }, - online: { - color: colors.green - }, - rightMenuContainer: { - display: 'flex', - direction: 'column' - }, - menuOptions: { - padding: '0.7em', - }, - tempGauge: { - borderColor:"white", - borderLeftStyle: 'solid', - } -}; - -TopNav.propTypes = { - classes: PropTypes.object, // styling - saveLayout: PropTypes.func.isRequired, // drawer Save Layout callback - ambient: PropTypes.number.isRequired, // room temp - ambientRising: PropTypes.bool.isRequired, // is room temp going up? - mainsStatus: PropTypes.bool.isRequired, // mains power source status - togglePower: PropTypes.func.isRequired, -}; - - -export default withStyles(styles)(TopNav); diff --git a/dashboard/frontend/src/components/canvasEvents.js b/dashboard/frontend/src/components/canvasEvents.js index e8a092d2..ffe121a8 100644 --- a/dashboard/frontend/src/components/canvasEvents.js +++ b/dashboard/frontend/src/components/canvasEvents.js @@ -10,16 +10,16 @@ const onWheelScroll = (stage) => { const oldScale = stage.scaleX(); const mousePointTo = { - x: stage.getPointerPosition().x / oldScale - stage.x() / oldScale, - y: stage.getPointerPosition().y / oldScale - stage.y() / oldScale, + x: stage.getPointerPosition().x / oldScale - stage.x() / oldScale, + y: stage.getPointerPosition().y / oldScale - stage.y() / oldScale, }; const newScale = e.deltaY > 0 ? oldScale * scaleBy : oldScale / scaleBy; stage.scale({ x: newScale, y: newScale }); const newPos = { - x: -(mousePointTo.x - stage.getPointerPosition().x / newScale) * newScale, - y: -(mousePointTo.y - stage.getPointerPosition().y / newScale) * newScale + x: -(mousePointTo.x - stage.getPointerPosition().x / newScale) * newScale, + y: -(mousePointTo.y - stage.getPointerPosition().y / newScale) * newScale }; stage.position(newPos); stage.batchDraw(); @@ -29,10 +29,9 @@ const onWheelScroll = (stage) => { /** Move canvas on middle mouse button down */ const onWheelDown = (stage) => { const moveCanvas = (e) => { - e.preventDefault(); - const newPos = { - x: (stage.x() + e.movementX), - y: (stage.y() + e.movementY), + const newPos = { + x: (stage.x() + e.movementX), + y: (stage.y() + e.movementY), }; stage.position(newPos); stage.batchDraw(); @@ -40,14 +39,12 @@ const onWheelDown = (stage) => { window.addEventListener("mousedown", (e) => { if (e.button == 1) { - e.preventDefault(); window.addEventListener("mousemove", moveCanvas); } }); window.addEventListener("mouseup", (e) => { if (e.button == 1) { - e.preventDefault(); window.removeEventListener("mousemove", moveCanvas); } }); diff --git a/dashboard/frontend/src/favicon.ico b/dashboard/frontend/src/favicon.ico index a11777cc..d96415bc 100644 Binary files a/dashboard/frontend/src/favicon.ico and b/dashboard/frontend/src/favicon.ico differ diff --git a/dashboard/frontend/src/logo.png b/dashboard/frontend/src/logo.png deleted file mode 100644 index 47d57f64..00000000 Binary files a/dashboard/frontend/src/logo.png and /dev/null differ diff --git a/docs/System Modeling.md b/docs/System Modeling.md index 7552387a..5405be96 100644 --- a/docs/System Modeling.md +++ b/docs/System Modeling.md @@ -74,7 +74,7 @@ Some properties can be configured later as in this example: SNMP OID configurations (oid mappings), PSU settings & outlet count cannot be updated after asset's creation; Howerver, you can delete the existing asset, add a new one and recreate its power connections; -See `simengine-cli model update {asset_type} -h` for the list of supported properties and [Assets Configurations](./Assets%20Configurations.md) page for more detailed documentation. +See `simengine-cli model update {asset_type} -h` for the list of supported properties and [Assets Configurations](./Assets%20Configurations) page for more detailed documentation. *Note* that the main engine daemon will need to be reloaded before schema changes can take place. diff --git a/docs/index.md b/docs/index.md index a92f5274..f24253e0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,9 +7,9 @@ The engine can reconstruct behaviour of system’s core components such as PDUs, The project exposes core assets’ functionalities through both GUI and UI though limited to the power component at the moment. Some management tools can be utilised through the Redis `pub/sub` based communication as well. -You can model your own set-up (see [System Modelling](./System%20Modeling.md)) and automate/perform power-related tasks (see [Power Management](./Power%20Management.md)). +You can model your own set-up (see [System Modelling](./System%20Modeling)) and automate/perform power-related tasks (see [Power Management](./Power%20Management)). -See [Anvil Model](./Anvil%20Model.md) for a real-world high-availability system example; +See [Anvil Model](./Anvil%20Model) for a real-world high-availability system example; ![](./server.png) diff --git a/enginecore/app.py b/enginecore/app.py index fc487a3f..aacb19be 100755 --- a/enginecore/app.py +++ b/enginecore/app.py @@ -48,16 +48,19 @@ def configure_env(relative=False): if relative: static_path = os.path.abspath(os.path.join(os.pardir, "data")) ipmi_templ_path = os.path.abspath("ipmi_template") + storcli_templ_path = os.path.abspath("storcli_template") lua_script_path = os.path.join("script", "snmppub.lua") else: share_dir = os.path.join(os.sep, "usr", "share", "simengine") - static_path = os.path.join(share_dir, "data") ipmi_templ_path = os.path.join(share_dir, "enginecore", "ipmi_template") + storcli_templ_path = os.path.abspath(share_dir, "enginecore", "storcli_template") lua_script_path = os.path.join(share_dir, "enginecore", "script", "snmppub.lua") os.environ['SIMENGINE_STATIC_DATA'] = os.environ.get('SIMENGINE_STATIC_DATA', static_path) os.environ['SIMENGINE_IPMI_TEMPL'] = os.environ.get('SIMENGINE_IPMI_TEMPL', ipmi_templ_path) + os.environ['SIMENGINE_STORCLI_TEMPL'] = os.environ.get('SIMENGINE_STORCLI_TEMPL', storcli_templ_path) + os.environ['SIMENGINE_SNMP_SHA'] = os.environ.get( 'SIMENGINE_SNMP_SHA', # str(os.popen('/usr/local/bin/redis-cli script load "$(cat {})"'.format(lua_script_path)).read()) diff --git a/enginecore/enginecore/cli/configure_state.py b/enginecore/enginecore/cli/configure_state.py index 2bdbb368..dddcaaf9 100644 --- a/enginecore/enginecore/cli/configure_state.py +++ b/enginecore/enginecore/cli/configure_state.py @@ -2,7 +2,7 @@ import argparse from enginecore.state.assets import Asset -from enginecore.state.sensors import SensorRepository +from enginecore.state.sensor.repository import SensorRepository def configure_command(configure_state_group): """Update some runtime values of the system components""" @@ -45,10 +45,10 @@ def configure_command(configure_state_group): def configure_battery(key, kwargs): """Udpate runtime battery status""" if kwargs['drain_speed'] is not None: - state_manager = Asset.get_state_manager_by_key(key, notify=True) + state_manager = Asset.get_state_manager_by_key(key) state_manager.set_drain_speed_factor(kwargs['drain_speed']) if kwargs['charge_speed'] is not None: - state_manager = Asset.get_state_manager_by_key(key, notify=True) + state_manager = Asset.get_state_manager_by_key(key) state_manager.set_charge_speed_factor(kwargs['charge_speed']) diff --git a/enginecore/enginecore/cli/model.py b/enginecore/enginecore/cli/model.py index 242d0214..31ffeedf 100644 --- a/enginecore/enginecore/cli/model.py +++ b/enginecore/enginecore/cli/model.py @@ -6,11 +6,8 @@ def handle_link(kwargs): """Power connections""" - if kwargs['remove']: - sys_modeler.remove_link(kwargs['source_key'], kwargs['dest_key']) - else: - sys_modeler.link_assets(kwargs['source_key'], kwargs['dest_key']) - + link_action = sys_modeler.remove_link if kwargs['remove'] else sys_modeler.link_assets + link_action(kwargs['source_key'], kwargs['dest_key']) ############# Validations @@ -23,11 +20,6 @@ def validate_server(kwargs): """Server-specific validation""" if kwargs['psu_num'] > 1 and (not kwargs['psu_load'] or len(kwargs['psu_load']) != kwargs['psu_num']): raise argparse.ArgumentTypeError("psu-load is required for server(-bmc) type when there're multiple PSUs") - if not kwargs['domain_name']: - raise argparse.ArgumentTypeError("domain-name is required for server(-bmc) type") - if not kwargs['power_consumption']: - raise argparse.ArgumentTypeError("power-consumption is required for server(-bmc) type") - def model_command(asset_group): @@ -57,7 +49,10 @@ def model_command(asset_group): power_asset_action.add_argument( '-s', '--source-key', type=int, required=True, help="Key of an asset that POWERS dest. asset" ) - power_asset_action.add_argument('-d', '--dest-key', type=int, required=True, help="Key of an powered by the source-key") + power_asset_action.add_argument( + '-d', '--dest-key', type=int, required=True, help="Key of the asset powered by the source-key" + ) + power_asset_action.add_argument('-r', '--remove', action='store_true', help="Delete power conneciton if exists") reload_asset_action.set_defaults( @@ -100,12 +95,12 @@ def update_command(update_asset_group): # server group update_server_parent = argparse.ArgumentParser(add_help=False) - update_server_parent.add_argument('--domain-name', help="VM domain name") + update_server_parent.add_argument('--domain-name', help="VM domain name", required=True) # power consuming assets group update_power_parent = argparse.ArgumentParser(add_help=False) update_power_parent.add_argument('--power-source', type=int) - update_power_parent.add_argument('--power-consumption', type=int, help="Power consumption in Watts") + update_power_parent.add_argument('--power-consumption', type=int, help="Power consumption in Watts", required=True) update_subp = update_asset_group.add_subparsers() @@ -337,36 +332,32 @@ def create_command(create_asset_group): ) create_server_bmc_action.add_argument( - '--psu-num', type=int, default=1, help="Number of PSUs installed in the server" - ) - create_server_bmc_action.add_argument( - '--psu-load', - nargs='+', - type=float, - help="""PSU(s) load distribution (the downstream power is multiplied by the value, e.g. - for 2 PSUs if '--psu-load 0.5 0.5', load is divivided equally) \n""" + '--storcli-port', + type=int, + default=50000, + help="Storcli websocket port used to establish a connection with a vm" ) + create_server_bmc_action.add_argument( - '--psu-power-consumption', - nargs='+', - type=int, - default=6, - help="""Power consumption of idle PSU \n""" + '--sensor-def', + type=str, + help="File containing sensor definitions (defaults to sensors.json file in enginecore/enginecore/model/presets)" ) create_server_bmc_action.add_argument( - '--psu-power-source', - nargs='+', - type=int, - default=120, - help="""PSU Voltage \n""" + '--storage-def', + type=str, + help="""File containing storage definitions + (defaults to storage.json file in enginecore/enginecore/model/presets) + """ ) create_server_bmc_action.add_argument( - '--sensor-def', + '--storage-states', type=str, - help="File containing sensor definitions (defaults to presets.json file in enginecore/enginecore/model/presets)" + help="""File containing storage state mappings (.JSON) + """ ) ## STATIC @@ -407,7 +398,7 @@ def create_command(create_asset_group): ) create_server_bmc_action.set_defaults( - validate=lambda args: [validate_key(args['asset_key']), validate_server(args)], + validate=lambda args: [validate_key(args['asset_key'])], func=lambda args: sys_modeler.create_server( args['asset_key'], args, diff --git a/enginecore/enginecore/cli/power.py b/enginecore/enginecore/cli/power.py index 45ff8f8d..aa907a62 100644 --- a/enginecore/enginecore/cli/power.py +++ b/enginecore/enginecore/cli/power.py @@ -39,6 +39,6 @@ def manage_state(asset_key, mng_action): asset_key (int): supplied asset identifier mng_action (func): callable object (lambda/function etc) that identifies action """ - state_manager = Asset.get_state_manager_by_key(asset_key, notify=True) + state_manager = Asset.get_state_manager_by_key(asset_key) mng_action(state_manager) \ No newline at end of file diff --git a/enginecore/enginecore/cli/storage.py b/enginecore/enginecore/cli/storage.py new file mode 100644 index 00000000..082effd9 --- /dev/null +++ b/enginecore/enginecore/cli/storage.py @@ -0,0 +1,143 @@ +""" +simengine-cli storage set pd --asset-key=5 --controller=0 --drive-id=12 --media-error-count=2 +simengine-cli storage set pd --asset-key=5 --controller=0 --drive-id=12 --other-error-count=2 +simengine-cli storage set pd --asset-key=5 --controller=0 --drive-id=12 --predictive-error-count=2 + +""" +import argparse +from enginecore.state.state_managers import BMCServerStateManager + +def storage_command(storage_group): + """Manage server storage space""" + + storage_subp = storage_group.add_subparsers() + + pd_command(storage_subp.add_parser( + 'pd', + help="Physical drive details and configurations" + )) + + controller_command(storage_subp.add_parser( + 'controller', + help="Update RAID controller related properties" + )) + + cv_command(storage_subp.add_parser( + 'cv', + help="Configure cachevault properties" + )) + + + +def get_ctrl_storage_args(): + # group a few args into a common parent element + server_controller_parent = argparse.ArgumentParser(add_help=False) + server_controller_parent.add_argument( + '-k', '--asset-key', help="Key of the server storage belongs to ", type=int, required=True + ) + server_controller_parent.add_argument( + '-c', '--controller', help="Number of the RAID controller", type=int, required=True + ) + + return server_controller_parent + + +def pd_command(pd_group): + """Endpoints for setting storage props (pd, vd, controller etc.) """ + + pd_subp = pd_group.add_subparsers() + + # group a few args into a common parent element + server_controller_parent = get_ctrl_storage_args() + + # CLI PD setter + set_pd_action = pd_subp.add_parser( + 'set', + help="Configure a physical drive (error count, state etc.)", + parents=[server_controller_parent] + ) + + set_pd_action.add_argument( + '-d', '--drive-id', help="Physical Drive id (DID)", type=int, required=True + ) + + set_pd_action.add_argument( + '-m', '--media-error-count', help="Update media error count for the drive", type=int, required=False + ) + + set_pd_action.add_argument( + '-o', '--other-error-count', help="Update other error count for the drive", type=int, required=False + ) + + set_pd_action.add_argument( + '-p', '--predictive-error-count', help="Update error prediction value for the drive", type=int, required=False + ) + + set_pd_action.add_argument( + '-s', '--state', help="Update state if the physical drive", choices=["Onln", "Offln"], required=False + ) + + set_pd_action.set_defaults( + func=lambda args: BMCServerStateManager.set_physical_drive_prop( + args['asset_key'], args['controller'], args['drive_id'], args + ) + ) + + + +def controller_command(ctrl_group): + """Endpoints for setting storage props (pd, vd, controller etc.) """ + + ctrl_subp = ctrl_group.add_subparsers() + server_controller_parent = get_ctrl_storage_args() + + # CLI controller setter + set_ctrl_action = ctrl_subp.add_parser( + 'set', + help="Configure a specific RAID controller", + parents=[server_controller_parent] + ) + + set_ctrl_action.add_argument( + '-e', '--memory-correctable-errors', help="Correctable RAM errors on disk data", type=int, required=False + ) + + set_ctrl_action.add_argument( + '-u', '--memory-uncorrectable-errors', help="Uncorrectable RAM errors on disk data", type=int, required=False + ) + + set_ctrl_action.add_argument( + '-a', '--alarm-state', help="Controller alarm state", choices=["missing", "off", "on"], required=False + ) + + set_ctrl_action.set_defaults( + func=lambda args: BMCServerStateManager.set_controller_prop( + args['asset_key'], args['controller'], args + ) + ) + + +def cv_command(cv_group): + """Endpoints for CacheVault properties""" + cv_group = cv_group.add_subparsers() + + # CLI PD setter + set_cv_action = cv_group.add_parser( + 'set', + help="Configure CacheVault", + parents=[get_ctrl_storage_args()] + ) + + set_cv_action.add_argument( + '-r', + '--replacement-required', + help="Correctable RAM errors on disk data", + choices=["Yes", "No"], + required=True + ) + + set_cv_action.set_defaults( + func=lambda args: BMCServerStateManager.set_cv_replacement( + args['asset_key'], args['controller'], args['replacement_required'] + ) + ) diff --git a/enginecore/enginecore/cli/thermal.py b/enginecore/enginecore/cli/thermal.py index e65497c1..d3419a54 100644 --- a/enginecore/enginecore/cli/thermal.py +++ b/enginecore/enginecore/cli/thermal.py @@ -5,9 +5,9 @@ import argparse import enginecore.model.system_modeler as sys_modeler -from enginecore.state.state_managers import StateManager, BMCServerStateManager -from enginecore.state.sensors import SensorRepository - +from enginecore.state.api import IStateManager, IBMCServerStateManager +from enginecore.state.sensor.repository import SensorRepository +from enginecore.cli.storage import get_ctrl_storage_args def thermal_command(thermal_group): @@ -30,6 +30,11 @@ def thermal_command(thermal_group): help="Configure/Retrieve sensor state and relationships" )) + storage_command(thermal_subp.add_parser( + 'storage', + help="Configure/Retrieve thermal relationship between sensors & storage elements" + )) + def ambient_command(th_ambient_group): """Aggregate ambient CLI options""" @@ -148,7 +153,7 @@ def cpu_usage_command(th_cpu_usg_group): ) th_set_cpu_usg_action.set_defaults( - func=BMCServerStateManager.update_thermal_cpu_target + func=IBMCServerStateManager.update_thermal_cpu_target ) th_delete_cpu_usg_action.set_defaults( @@ -156,38 +161,20 @@ def cpu_usage_command(th_cpu_usg_group): ) -def sensor_command(th_sensor_group): - """Sensor related thermal commands (listing all, configuring etc...)""" - th_sensor_subp = th_sensor_group.add_subparsers() +def get_thermal_add_args(): + # group a few args into a common parent element + thermal_parent = argparse.ArgumentParser(add_help=False) - # - SET - - th_set_sensor_action = th_sensor_subp.add_parser( - 'set', - help="Update sensor thermal settings", - ) - - th_set_sensor_action.add_argument( - '-k', '--asset-key', help="Key of the server sensors belong to ", type=int, required=True - ) - - th_set_sensor_action.add_argument( + thermal_parent.add_argument( '-s', '--source-sensor', help="Name of the source sensor", type=str, required=True ) - th_set_sensor_action.add_argument( - '-t', - '--target-sensor', - help="Name of the target sensor affected by the event associated with the source sensor", - type=str, - required=True - ) - th_set_sensor_action.add_argument( + thermal_parent.add_argument( '-e', '--event', help="Event associated with the source sensor", choices=['up', 'down'] ) - th_set_sensor_action.add_argument( + thermal_parent.add_argument( '-a', '--action', help="Action associated with the event (for instance, on sensor 0x1 going down, \ @@ -198,7 +185,7 @@ def sensor_command(th_sensor_group): choices=['increase', 'decrease'] ) - th_set_sensor_action.add_argument( + thermal_parent.add_argument( '--model', '-m', help="Simengine will use this .JSON model to determine thermal impact for any given source sensor input; \ @@ -206,27 +193,125 @@ def sensor_command(th_sensor_group): ) - th_set_sensor_action.add_argument( + thermal_parent.add_argument( '-d', '--degrees', type=float, help="Update sensor temperature (in Celsius); \ if time period and event are specified, this value will be added to the previous sensor temp;" ) - th_set_sensor_action.add_argument( + thermal_parent.add_argument( '-p', '--pause-at', help="Increase/Descrease room temperature until this value is reached", type=float, required=True ) - th_set_sensor_action.add_argument( + thermal_parent.add_argument( '-r', '--rate', help="Update temperature value very 'n' seconds", type=int, required=True ) + return thermal_parent + + +def storage_command(th_storage_group): + """configure thermal props of storage componenets""" + th_storage_subp = th_storage_group.add_subparsers() + + # Add new relationship: + th_set_storage_action = th_storage_subp.add_parser( + 'set', + help="Update storage thermal settings", + parents=[get_ctrl_storage_args(), get_thermal_add_args()] + ) + + th_set_storage_action.add_argument( + '--drive', + help="DID of the physical drive this sensor is affecting", + type=str, + ) + + + th_set_storage_action.add_argument( + '--cache-vault', + help="Serial number of CacheVault sensor is affecting", + type=str, + ) + + th_set_storage_action.set_defaults( + validate=lambda attr: attr['drive'] or attr['cache_vault'], + func=handle_set_thermal_storage + ) + + # Delete existing: + + th_delete_storage_action = th_storage_subp.add_parser( + 'delete', + help="Delete thermal connection between a sensor and a storage component (cv or physical drive)", + ) + + th_delete_storage_action.add_argument( + '-k', '--asset-key', help="Key of the server sensor/storage component belongs to ", type=int, required=True + ) + th_delete_storage_action.add_argument( + '-s', '--source-sensor', help="Name of the source sensor", type=str, required=True + ) + + th_delete_storage_action.add_argument( + '-c', '--controller', help="Controller number", type=int, required=True + ) + + + th_delete_storage_action.add_argument( + '--drive', + help="DID of the physical drive this sensor is affecting", + type=str, + ) + + + th_delete_storage_action.add_argument( + '--cache-vault', + help="Serial number of CacheVault sensor is affecting", + type=str, + ) + + th_delete_storage_action.add_argument( + '-e', '--event', help="Event associated with the source sensor", choices=['up', 'down'], required=True + ) + + th_delete_storage_action.set_defaults( + func=IBMCServerStateManager.delete_thermal_storage_target + ) + + +def sensor_command(th_sensor_group): + """Sensor related thermal commands (listing all, configuring etc...)""" + + th_sensor_subp = th_sensor_group.add_subparsers() + + # - SET + + th_set_sensor_action = th_sensor_subp.add_parser( + 'set', + help="Update sensor thermal settings", + parents=[get_thermal_add_args()] + ) + + th_set_sensor_action.add_argument( + '-k', '--asset-key', help="Key of the server sensors belong to ", type=int, required=True + ) + + th_set_sensor_action.add_argument( + '-t', + '--target-sensor', + help="Name of the target sensor affected by the event associated with the source sensor", + type=str, + required=True + ) + # - GET th_get_sensor_action = th_sensor_subp.add_parser( @@ -240,6 +325,7 @@ def sensor_command(th_sensor_group): th_get_sensor_action.add_argument( '-k', '--asset-key', help="Key of the server sensor belongs to ", type=int, required=True ) + th_get_sensor_action.add_argument('-s', '--sensor', help="Name of the sensor", type=str) @@ -271,13 +357,14 @@ def sensor_command(th_sensor_group): th_get_sensor_action.set_defaults(func=handle_get_thermal_sensor) th_set_sensor_action.set_defaults(func=handle_set_thermal_sensor) th_delete_sensor_action.set_defaults( - func=sys_modeler.delete_thermal_sensor_target # TODO: change sys_modeler to BMCServerStateManager + func=sys_modeler.delete_thermal_sensor_target # TODO: change sys_modeler to IBMCServerStateManager ) + def handle_get_thermal_cpu(kwargs): """Display current cpu & sensor relationship""" - th_cpu_details = BMCServerStateManager.get_thermal_cpu_details(kwargs['asset_key']) + th_cpu_details = IBMCServerStateManager.get_thermal_cpu_details(kwargs['asset_key']) if not th_cpu_details: print("There are no cpu/sensor relationships for server {}".format(kwargs['asset_key'])) @@ -285,28 +372,29 @@ def handle_get_thermal_cpu(kwargs): th_cpu_fmt = lambda x: " --> t:[{}] using model '{}'".format(x['sensor']['name'], x['rel']['model']) print('\n'.join(["Server [{}]:".format(kwargs['asset_key'])] + list(map(th_cpu_fmt, th_cpu_details)))) + def handle_set_thermal_ambient(kwargs): """Configure thermal properties for room temperature""" del kwargs['func'] if kwargs['event'] and kwargs['pause_at'] and kwargs['rate']: - StateManager.set_ambient_props(kwargs) + IStateManager.set_ambient_props(kwargs) elif kwargs['event'] or kwargs['pause_at'] or kwargs['rate']: raise argparse.ArgumentTypeError("Event, pause-at and rate must be supplied") else: - StateManager.set_ambient(kwargs['degrees']) + IStateManager.set_ambient(kwargs['degrees']) def handle_get_thermal_ambient(kwargs): """Print some general information about ambient configurations""" if kwargs['value_only']: - print(StateManager.get_ambient()) + print(IStateManager.get_ambient()) else: - print("Ambient: {}° ".format(StateManager.get_ambient())) + print("Ambient: {}° ".format(IStateManager.get_ambient())) - ambient_props = StateManager.get_ambient_props() + ambient_props = IStateManager.get_ambient_props() if not ambient_props: print('Ambient event properties are not configured yet!') return @@ -332,7 +420,16 @@ def handle_set_thermal_sensor(kwargs): if kwargs['model']: kwargs['event'] = 'up' - BMCServerStateManager.update_thermal_sensor_target(kwargs) + IBMCServerStateManager.update_thermal_sensor_target(kwargs) + + +def handle_set_thermal_storage(kwargs): + del kwargs['func'] + if not kwargs['cache_vault'] and not kwargs['drive']: + raise argparse.ArgumentTypeError("Must provide either target drive id or cachevault!") + + IBMCServerStateManager.update_thermal_storage_target(kwargs) + def handle_get_thermal_sensor(kwargs): """Display information about BMC sensors""" diff --git a/enginecore/enginecore/model/graph_reference.py b/enginecore/enginecore/model/graph_reference.py index f913e3a4..c4bf90db 100644 --- a/enginecore/enginecore/model/graph_reference.py +++ b/enginecore/enginecore/model/graph_reference.py @@ -1,6 +1,8 @@ """DB driver (data-layer) that provides access to db sessions and contains commonly used queries """ import os +import json + from neo4j.v1 import GraphDatabase, basic_auth from enginecore.state.utils import format_as_redis_key import enginecore.model.query_helpers as qh @@ -418,6 +420,20 @@ def get_mains_powered_outlets(cls, session): return list(map(lambda x: x.get('key'), results)) + @classmethod + def format_target_elements(cls, results, t_format=None): + """Format neo4j results as target sensors""" + thermal_details = {'source': {}, 'targets': [],} + + for record in results: + thermal_details['source'] = dict(record.get('source')) + + if not t_format: + t_format = lambda r: {**dict(r.get('targets')), **{"rel": list(map(dict, r.get('rel')))}} + thermal_details['targets'].append(t_format(record)) + + return thermal_details + @classmethod def get_affected_sensors(cls, session, server_key, source_name): """Get sensors affected by the source sensor @@ -427,7 +443,7 @@ def get_affected_sensors(cls, session, server_key, source_name): server_key(int): key of the server sensors belong to source_name(str): name of the source sensor Returns: - dict: source and target sensor details + dict: source and target sensor details """ results = session.run( @@ -439,47 +455,70 @@ def get_affected_sensors(cls, session, server_key, source_name): source=source_name ) - thermal_details = {'source': {}, 'targets': [],} + return cls.format_target_elements(results) - for record in results: - thermal_details['source'] = dict(record.get('source')) - - thermal_details['targets'].append( - {**dict(record.get('targets')), **{"rel": list(map(dict, record.get('rel')))}} - ) + @classmethod + def get_affected_hd_elements(cls, session, server_key, source_name): + """Get storage components affected by the source sensor + Args: + session: database session + server_key(int): key of the server sensor & hd elements belong to + source_name(str): name of the source sensor + Returns: + dict: source and target details + """ - # print(source_name, thermal_details) + results = session.run( + """ + MATCH (:ServerWithBMC { key: $server })-[:HAS_SENSOR]->(source:Sensor { name: $source }) + MATCH (source)<-[rel]-(targets) + MATCH (controller)-[:HAS_CACHEVAULT|:HAS_PHYSICAL_DRIVE]->(targets) + WHERE targets:PhysicalDrive or targets:CacheVault + return source, targets, collect(rel) as rel, controller + """, + server=server_key, + source=source_name + ) - return thermal_details + output_format = lambda r: { + **dict(r.get('targets')), + **{"rel": list(map(dict, r.get('rel')))}, + **{"controller": dict(r.get('controller'))} + } + return cls.format_target_elements(results, t_format=output_format) @classmethod def get_sensor_thermal_rel(cls, session, server_key, relationship): - """Get thermal details about target sensor affected by the source sensor - Args: + """Get thermal details about thermal relationship + Args:e session: database session - server_key(int): key of the server sensors belong to + server_key(int): key of the server sensor(s) belong to relationship(dict): source, target and event """ - results = session.run( - """ - MATCH (:ServerWithBMC { key: $server })-[:HAS_SENSOR]->(source:Sensor { name: $source }) - MATCH (source)<-[rel]-(target:Sensor {name: $target}) - WHERE rel.event = $event - RETURN source, target, rel - """, - server=server_key, - source=relationship['source'], - target=relationship['target'], - event=relationship['event'] + query = [] + query.append( + 'MATCH (:ServerWithBMC {{ key: {} }})-[:HAS_SENSOR]->(source:Sensor {{ name: "{}" }})' + .format(server_key, relationship['source']) ) + query.append( + 'MATCH (source)<-[rel :COOLED_BY|:HEATED_BY]-(target {{ {}: {} }})' + .format(relationship['target']['attribute'], relationship['target']['value']) + ) + + query.extend([ + 'WHERE rel.event = "{}"'.format(relationship['event']), + 'RETURN source, target, rel' + ]) + + results = session.run("\n".join(query)) record = results.single() return { - 'source': dict(record.get('source')), - 'target': dict(record.get('target')), - 'rel': dict(record.get('rel')) + 'source': dict(record.get('source')), + 'target': dict(record.get('target')), + 'rel': dict(record.get('rel')) } if record else None @@ -566,4 +605,321 @@ def get_thermal_cpu_details(cls, session, server_key): }) return th_cpu_details - \ No newline at end of file + + + @classmethod + def set_physical_drive_prop(cls, session, server_key, controller, did, properties): + """Update physical drive properties (such as error counts or state) + Args: + session: database session + server_key(int): key of the server physical drive belongs to + controller(int): controller number + did(int): drive id + properties(dict): e.g. 'media_error_count', 'other_error_count', 'predictive_error_count' or 'state' + """ + query = [] + + s_attr = ['media_error_count', 'other_error_count', 'predictive_error_count', 'State'] + + properties['State'] = properties['state'] + + # query as (server)->(storage_controller)->(physical drive) + query.append("MATCH (server:Asset {{ key: {} }})".format(server_key)) + query.append("MATCH (server)-[:HAS_CONTROLLER]->(ctrl:Controller {{ controllerNum: {} }})".format(controller)) + query.append("MATCH (ctrl)-[:HAS_PHYSICAL_DRIVE]->(pd:PhysicalDrive {{ DID: {} }})".format(did)) + + set_stm = qh.get_set_stm(properties, node_name="pd", supported_attr=s_attr) + query.append('SET {}'.format(set_stm)) + + session.run("\n".join(query)) + + + @classmethod + def set_controller_prop(cls, session, server_key, controller, properties): + """Update controller state + Args: + session: database session + server_key(int): key of the server controller belongs to + controller(int): controller number + properties(dict): e.g. 'media_error_count', 'other_error_count', 'predictive_error_count' or 'state' + """ + query = [] + + s_attr = ['memory_correctable_errors', 'memory_uncorrectable_errors', 'alarm_state'] + + # query as (server)->(storage_controller) + query.append("MATCH (server:Asset {{ key: {} }})".format(server_key)) + query.append("MATCH (server)-[:HAS_CONTROLLER]->(ctrl:Controller {{ controllerNum: {} }})".format(controller)) + + set_stm = qh.get_set_stm(properties, node_name="ctrl", supported_attr=s_attr) + query.append('SET {}'.format(set_stm)) + + session.run("\n".join(query)) + + + @classmethod + def get_storcli_details(cls, session, server_key): + """ + Args: + session: database session + server_key(int): key of the server controller belongs to + """ + + results = session.run( + """ + MATCH (:Asset { key: $key })-[:SUPPORTS_STORCLI]->(cli) RETURN cli + """, + key=server_key + ) + + record = results.single() + storcli_details = {} + + if record: + storcli_details = dict(record.get('cli')) + storcli_details['stateConfig'] = json.loads(storcli_details['stateConfig']) + + return storcli_details + + + @classmethod + def get_controller_details(cls, session, server_key, controller): + """Query controller specs + Args: + session: database session + server_key(int): key of the server controller belongs to + controller(int): controller number + Returns: + dict: controller information + """ + + query = "MATCH (:Asset {{ key: {} }})-[:HAS_CONTROLLER]->(ctrl:Controller {{ controllerNum: {} }}) RETURN ctrl" + results = session.run(query.format(server_key, controller)) + record = results.single() + + return dict(record.get('ctrl')) if record else None + + + @classmethod + def get_controller_count(cls, session, server_key): + """Get number of controllers per server + Args: + session: database session + server_key(int): key of the server controller belongs to + Returns: + int: controller count + """ + + results = session.run( + """ + MATCH (:Asset { key: $key })-[:HAS_CONTROLLER]->(ctrl:Controller) RETURN count(ctrl) as ctrl_count + """, + key=server_key + ) + + record = results.single() + return int(record.get('ctrl_count')) if record else None + + + @classmethod + def get_virtual_drive_details(cls, session, server_key, controller): + """Get virtual drive details + Args: + session: database session + server_key(int): key of the server controller belongs to + controller(int): controller number of VDs + Returns: + list: virtual drives + """ + + query = [] + query.append( + "MATCH (:Asset {{ key: {} }})-[:HAS_CONTROLLER]->(ctrl:Controller {{ controllerNum: {} }})" + .format(server_key, controller) + ) + + query.append("MATCH (ctrl)-[:HAS_VIRTUAL_DRIVE]->(vd:VirtualDrive)") + query.append("MATCH (vd)<-[:BELONGS_TO_VIRTUAL_SPACE]-(pd:PhysicalDrive)") + query.append("WITH vd, pd") + query.append("ORDER BY pd.slotNum ASC") + query.append("RETURN vd, collect(pd) as pd ORDER BY vd.vdNum ASC") + + results = session.run("\n".join(query)) + vd_details = [{**dict(r.get('vd')), **{'pd': list(map(dict, list(r.get('pd'))))}} for r in results] + + return vd_details + + + @classmethod + def get_all_drives(cls, session, server_key, controller): + """Get both virtual & physical drives for a particular server/raid controller + Args: + session: database session + server_key(int): key of the server controller belongs to + controller(int): controller num + Returns: + dict: containing list of virtual & physical drives + """ + + query = [] + query.append( + "MATCH (:Asset {{ key: {} }})-[:HAS_CONTROLLER]->(ctrl:Controller {{ controllerNum: {} }})" + .format(server_key, controller) + ) + query.append("MATCH (ctrl)-[:HAS_PHYSICAL_DRIVE]->(pd:PhysicalDrive)") + + query.append("RETURN collect(pd) as pd") + + results = session.run("\n".join(query)) + record = results.single() + + return { + "vd": cls.get_virtual_drive_details(session, server_key, controller), + "pd": list(map(dict, list(record.get('pd')))) + } + + + @classmethod + def get_cachevault(cls, session, server_key, controller): #TODO: cachevault serial NUMBER! + """Cachevault details + Args: + session: database session + server_key(int): key of the server cachevault belongs to + controller(int): controller num + Returns: + dict: information about cachevault + """ + query = [] + query.extend([ + "MATCH (:Asset {{ key: {} }})-[:HAS_CONTROLLER]->(ctrl:Controller {{ controllerNum: {} }})" + .format(server_key, controller), + "MATCH (ctr)-[:HAS_CACHEVAULT]->(cv:CacheVault)", + "RETURN cv" + ]) + + results = session.run("\n".join(query)) + record = results.single() + + return dict(record.get('cv')) if record else None + + + @classmethod + def set_cv_replacement(cls, session, server_key, controller, repl_status): #TODO: cachevault serial NUMBER! + """Update cachevault replacement status + Args: + session: database session + server_key(int): key of the server cachevault belongs to + controller(int): controller num + """ + + query = [] + query.extend([ + "MATCH (:Asset {{ key: {} }})-[:HAS_CONTROLLER]->(ctrl:Controller {{ controllerNum: {} }})" + .format(server_key, controller), + "MATCH (ctrl)-[:HAS_CACHEVAULT]->(cv:CacheVault)", + ]) + + + set_stm = qh.get_set_stm({"replacement": repl_status}, node_name="cv", supported_attr=['replacement']) + query.append('SET {}'.format(set_stm)) + + session.run("\n".join(query)) + + + @classmethod + def add_to_hd_component_temperature(cls, session, target, temp_change, limit): + """Add to cv temperature sensor value + Args: + session: database session + target(dict): target attributes such as key of the server, controller & serial number + temp_change(int): value to be added to the target temperature + limit(dict): indicates that target temp cannot go beyond this limit (upper & lower) + Returns: + tuple: True if the temp value was updated & current temp value (updated) + """ + query = [] + query.extend([ + "MATCH (:Asset {{ key: {} }})-[:HAS_CONTROLLER]->(ctrl:Controller {{ controllerNum: {} }})" + .format(target['server_key'], target['controller']), + "MATCH (ctr)-[:HAS_CACHEVAULT|:HAS_PHYSICAL_DRIVE]->(hd_element:{} {{ {}: {} }})" + .format(target['hd_type'], target['attribute'], target['value']), + "RETURN hd_element.temperature as temp" + ]) + + results = session.run("\n".join(query)) + record = results.single() + current_temp = record.get('temp') + + new_temp = current_temp + temp_change + + new_temp = max(new_temp, limit['lower']) + + if 'upper' in limit and limit['upper']: + new_temp = min(new_temp, limit['upper']) + + if new_temp == current_temp: + return False, current_temp + + query = query[:2] # grab first 2 queries + query.append( + "SET hd_element.temperature={}".format(new_temp) + ) + + session.run("\n".join(query)) + + return True, new_temp + + + @classmethod + def get_all_hd_thermal_elements(cls, session, server_key): + """Retrieve all storage components that support temperature sensors""" + + query = [] + query.extend([ + "MATCH (:ServerWithBMC {{ key: {} }})-[:HAS_CONTROLLER]->(controller:Controller)" + .format(server_key), + "MATCH (controller)-[:HAS_CACHEVAULT|:HAS_PHYSICAL_DRIVE]->(hd_component)", + "WHERE hd_component:PhysicalDrive or hd_component:CacheVault", + "RETURN controller, hd_component" + ]) + + results = session.run("\n".join(query)) + + hd_thermal_elements = [] + + for record in results: + hd_thermal_elements.append({ + "controller": dict(record.get('controller')), + "component": dict(record.get('hd_component')) + }) + + return hd_thermal_elements + + @classmethod + def get_psu_sensor_names(cls, session, server_key, psu_num): + """Retrieve server-specific psu sensor names + Args: + session: database session + server_key(int): key of the server sensors belongs to + psu_num(int): psu num + """ + + query = [] + + sensor_match = "MATCH (:PSU {{ key: {} }})<-[:HAS_COMPONENT]-(:Asset)-[:HAS_SENSOR]->(sensor {{ num: {} }})" + label_match = map('sensor:{}'.format, ['psuCurrent', 'psuTemperature', 'psuStatus', 'psuPower']) + + query.extend([ + sensor_match.format(server_key, psu_num), + "WHERE {}".format(' or '.join(label_match)), + "RETURN sensor" + ]) + + results = session.run("\n".join(query)) + + psu_names = {} + for record in results: + entry = dict(record.get('sensor')) + psu_names[entry['type']] = entry['name'] + + return psu_names diff --git a/enginecore/enginecore/model/presets/sensors.json b/enginecore/enginecore/model/presets/sensors.json index 2e8b9263..61f45886 100644 --- a/enginecore/enginecore/model/presets/sensors.json +++ b/enginecore/enginecore/model/presets/sensors.json @@ -24,7 +24,7 @@ "ucr": 1200 }, "defaultValue": 400, - "offValue": 0, + "offValue": 0, "name": "Frnt_FAN{index}" }, { @@ -36,11 +36,25 @@ }, "defaultValue": 1000, "name": "Frnt_FAN", - "offValue": 0, + "offValue": 0, "address": "0x79" } ] }, + "psu": [ + { + "id": 1, + "draw": 0.5, + "powerConsumption": 30, + "powerSource": 120 + }, + { + "id": 2, + "draw": 0.5, + "powerConsumption": 30, + "powerSource": 120 + } + ], "psuStatus": { "group": "psu", "addressSpace": "0x8", @@ -49,7 +63,7 @@ { "thresholds": {}, "defaultValue": "0x01", - "offValue": "0x08", + "offValue": "0x08", "name": "PSU{index} status" }, { @@ -66,13 +80,13 @@ "sensorDefinitions": [ { "thresholds": {}, - "defaultValue": 2, + "defaultValue": 0, "offValue": 0, "name": "PSU{index} current" }, { "thresholds": {}, - "defaultValue": 2, + "defaultValue": 0, "offValue": 0, "name": "PSU{index} current" } @@ -102,13 +116,13 @@ "sensorDefinitions": [ { "thresholds": {}, - "defaultValue": 20, + "defaultValue": 0, "offValue": 0, "name": "PSU{index} power" }, { "thresholds": {}, - "defaultValue": 20, + "defaultValue": 0, "offValue": 0, "name": "PSU{index} power" } diff --git a/enginecore/enginecore/model/presets/storage.json b/enginecore/enginecore/model/presets/storage.json new file mode 100644 index 00000000..a4d49a94 --- /dev/null +++ b/enginecore/enginecore/model/presets/storage.json @@ -0,0 +1,171 @@ +{ + + "CLIVersion": " 007.0606.0000.0000 Mar 20, 2018", + "operatingSystem": "Linux 2.6.32-754.6.3.el6.x86_64", + "controllers": [ + { + "model": "RAID Ctrl SAS 6G 1GB (D3116C)", + "serialNumber": "0000000043113622", + "SASAddress": "50030057013c6550", + "PCIAddress": "00:01:00:00", + "mfgDate": "00/00/00", + "reworkDate": "00/00/00", + "bgiRate": 30, + "prRate": 20, + "rebuildRate": 30, + "ccRate": 30, + + "CacheVault": { + "model": "CVPM02", + "state": "Optimal", + "mfgDate": "2013/10/25", + "serialNumber": "17703" + }, + "BBU": {}, + "VD": [ + { + "TYPE": "RAID6", + "State": "Optl", + "Access": "RW", + "Consist": "Yes", + "sCC": "OFF", + "Size": 817.312, + "Name": "", + "Cac": "-", + "Cache": "RWBD", + "DID": [9, 10, 8, 7, 11, 14, 12, 13] + } + ], + "PD": [ + { + "EID": 252, + "DID": 9, + "State": "Onln", + "DG": 0, + "Size": "136.218", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "MK1401GRRB", + "Sp": "U", + "Type": "-", + "PDC": "dsbl" + }, + { + "EID": 252, + "DID": 10, + "State": "Onln", + "DG": 0, + "Size": "136.218", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "MK1401GRRB", + "Sp": "U", + "Type": "-", + "PDC": "dsbl" + }, + { + "EID": 252, + "DID": 8, + "State": "Onln", + "DG": 0, + "Size": "136.218", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "MK1401GRRB", + "Sp": "U", + "Type": "-", + "PDC": "dsbl" + }, + { + "EID": 252, + "DID": 7, + "State": "Onln", + "DG": 0, + "Size": "136.218", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "MK1401GRRB", + "Sp": "U", + "Type": "-", + "PDC": "dsbl" + }, + { + "EID": 252, + "DID": 11, + "State": "Onln", + "DG": 0, + "Size": "136.218", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "MK1401GRRB", + "Sp": "U", + "Type": "-", + "PDC": "dsbl" + }, + { + "EID": 252, + "DID": 14, + "State": "Onln", + "DG": 0, + "Size": "136.218", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "MK1401GRRB", + "Sp": "U", + "Type": "-", + "PDC": "dsbl" + }, + { + "EID": 252, + "DID": 12, + "State": "Onln", + "DG": 0, + "Size": "136.218", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "MK1401GRRB", + "Sp": "U", + "Type": "-", + "PDC": "dsbl" + }, + { + "EID": 252, + "DID": 13, + "State": "Onln", + "DG": 0, + "Size": "136.218", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "MK1401GRRB", + "Sp": "U", + "Type": "-", + "PDC": "dsbl" + } + ] + } + ] +} \ No newline at end of file diff --git a/enginecore/enginecore/model/presets/storage_states.json b/enginecore/enginecore/model/presets/storage_states.json new file mode 100644 index 00000000..add3cf2c --- /dev/null +++ b/enginecore/enginecore/model/presets/storage_states.json @@ -0,0 +1,31 @@ +{ + "virtualDrive": { + "Optl": { + "numPdOffline": 0, + "mediaErrorCount": 0, + "otherErrorCount": 0 + }, + "Pdgd": { + "numPdOffline": -1, + "mediaErrorCount": 5, + "otherErrorCount": 7 + }, + "Dgrd": { + "numPdOffline": 1, + "mediaErrorCount": -1, + "otherErrorCount": -1 + } + }, + "controller": { + "Optimal": { + "numPdOffline": 0, + "memoryCorrectableErrors": 0, + "memoryUncorrectableErrors": 0 + }, + "Needs Attention": { + "numPdOffline": 1, + "memoryCorrectableErrors": 100, + "memoryUncorrectableErrors": 20 + } + } +} \ No newline at end of file diff --git a/enginecore/enginecore/model/supported_sensors.py b/enginecore/enginecore/model/supported_sensors.py new file mode 100644 index 00000000..219b99f7 --- /dev/null +++ b/enginecore/enginecore/model/supported_sensors.py @@ -0,0 +1,14 @@ +"""List of supported sensors (defined in presets/sensors.json)""" + +SUPPORTED_SENSORS = [ + 'caseFan', + 'psuStatus', + 'psuVoltage', + 'psuPower', + 'psuCurrent', + 'psuTemperature', + 'memoryTemperature', + 'Ambient', + 'RAIDControllerTemperature', + 'cpuTemperature' +] diff --git a/enginecore/enginecore/model/system_modeler.py b/enginecore/enginecore/model/system_modeler.py index ac6097c8..e04a2a42 100644 --- a/enginecore/enginecore/model/system_modeler.py +++ b/enginecore/enginecore/model/system_modeler.py @@ -7,7 +7,10 @@ from enum import Enum import libvirt + from enginecore.model.graph_reference import GraphReference +from enginecore.model.supported_sensors import SUPPORTED_SENSORS + import enginecore.model.query_helpers as qh GRAPH_REF = GraphReference() @@ -19,6 +22,7 @@ SIMENGINE_NODE_LABELS.extend(["Asset", "StageLayout", "SystemEnvironment", "EnvProp"]) SIMENGINE_NODE_LABELS.extend(["OID", "OIDDesc", "Sensor", "AddressSpace"]) SIMENGINE_NODE_LABELS.extend(["CPU", "Battery"]) +SIMENGINE_NODE_LABELS.extend(["Controller", "Storcli", "BBU", "CacheVault", "VirtualDrive", "PhysicalDrive"]) def _add_psu(key, psu_index, attr): @@ -158,14 +162,15 @@ class ServerVariations(Enum): 'password': 'test', 'host': 'localhost', 'port': 9001, - 'vmport': 9002 + 'vmport': 9002, + 'storcli_port': 50000, } -def _add_sensors(asset_key, preset_file=os.path.join(os.path.dirname(__file__), 'presets/sensors.json')): +def _add_sensors(asset_key, preset_file): """Add sensors based on a preset file""" - with open(preset_file) as preset_handler, GRAPH_REF.get_session() as session: + with open(preset_file) as preset_handler, GRAPH_REF.get_session() as session: query = [] query.append("MATCH (server:Asset {{ key: {} }})".format(asset_key)) @@ -173,6 +178,9 @@ def _add_sensors(asset_key, preset_file=os.path.join(os.path.dirname(__file__), for sensor_type, sensor_specs in data.items(): + if sensor_type not in SUPPORTED_SENSORS: + continue + address_space_exists = 'addressSpace' in sensor_specs and sensor_specs['addressSpace'] if address_space_exists: @@ -196,7 +204,7 @@ def _add_sensors(asset_key, preset_file=os.path.join(os.path.dirname(__file__), raise KeyError("Missing address for a seonsor {}".format(sensor_type)) s_attr = [ - "name", "defaultValue", "offValue", "group", + "name", "defaultValue", "offValue", "group", "num", "lnr", "lcr", "lnc", "unc", "ucr", "unr", "address", "index", "type", "eventReadingType" ] @@ -205,7 +213,7 @@ def _add_sensors(asset_key, preset_file=os.path.join(os.path.dirname(__file__), props = { **sensor['thresholds'], **sensor, **addr, **sensor_specs, - **{'type': sensor_type} + **{'type': sensor_type, "num": idx + 1} } props_stm = qh.get_props_stm(props, supported_attr=s_attr) @@ -219,6 +227,118 @@ def _add_sensors(asset_key, preset_file=os.path.join(os.path.dirname(__file__), query.append("CREATE (server)-[:HAS_SENSOR]->(sensor{})".format(sensor_node)) + + # print("\n".join(query)) + session.run("\n".join(query)) + +def _add_storage(asset_key, preset_file, storage_state_file): + """Add storage to the server with asset_key""" + + with open(preset_file) as preset_h, open(storage_state_file) as state_h, GRAPH_REF.get_session() as session: + query = [] + + query.append("MATCH (server:Asset {{ key: {} }})".format(asset_key)) + storage_data = json.load(preset_h) + state_data = json.load(state_h) + + props_stm = qh.get_props_stm( + {**storage_data, **{'stateConfig': json.dumps(state_data)}}, + supported_attr=["operatingSystem", "CLIVersion", "stateConfig"] + ) + query.append("CREATE (server)-[:SUPPORTS_STORCLI]->(storage:Storcli {{ {} }})".format(props_stm)) + + for idx, controller in enumerate(storage_data['controllers']): + + s_attr = [ + "controllerNum", "model", "serialNumber", + "SASAddress", "PCIAddress", "mfgDate", "reworkDate", + 'memoryCorrectable_errors', 'memoryUncorrectable_errors', 'alarmState', + "bgiRate", "prRate", "rebuildRate", "ccRate" + ] + + default_ctr_prop = { + 'memoryCorrectable_errors': 0, + 'memoryUncorrectable_errors': 0, + 'alarmState': 'off', + "controllerNum": idx + } + props_stm = qh.get_props_stm({**controller, **default_ctr_prop}, supported_attr=s_attr) + + ctrl_node = 'ctrl'+str(idx) + query.append( + "CREATE (server)-[:HAS_CONTROLLER]->({}:Controller {{ {} }})".format(ctrl_node, props_stm) + ) + + # BBU or CacheVault + if "BBU" in controller and controller["BBU"]: + props_stm = qh.get_props_stm( + controller["BBU"], + supported_attr=["model", "serialNumber", "type", "replacementNeeded", "state", "designCapacity"] + ) + query.append("CREATE ({})-[:HAS_BBU]->(bbu:BBU {{ {} }})".format(ctrl_node, props_stm)) + elif "CacheVault" in controller and controller["CacheVault"]: + props_stm = qh.get_props_stm( + {**controller["CacheVault"], **{'temperature': 0, "replacement": "No"}}, + supported_attr=["model", "replacement", "state", "temperature", "mfgDate", "serialNumber"] + ) + query.append( + "CREATE ({})-[:HAS_CACHEVAULT]->(cache:CacheVault {{ {} }})".format(ctrl_node, props_stm) + ) + + # Add physical drives + for pidx, phys_drive in enumerate(controller['PD']): + + pd_node = 'pd'+str(phys_drive["DID"]) + + # define supported attributes + s_attr = [ + "EID", "DID", "State", "DG", "Size", + "Intf", "Med", "SED", "PI", "SeSz", + "Model", "Sp", "Type", "PDC", "slotNum", "temperature", + "mediaErrorCount", "otherErrorCount", "predictiveErrorCount" + ] + + props_stm = qh.get_props_stm( + { + **phys_drive, + **{ + 'slotNum': pidx, + 'mediaErrorCount': 0, + 'otherErrorCount': 0, + 'predictiveErrorCount': 0, + 'temperature': 0 + } + }, + supported_attr=s_attr + ) + + query.append( + "CREATE ({})-[:HAS_PHYSICAL_DRIVE]->({}:PhysicalDrive {{ {} }})" + .format(ctrl_node, pd_node, props_stm) + ) + + for vidx, virt_drive in enumerate(controller['VD']): + vd_node = 'vd'+str(vidx) + + s_attr = [ + "TYPE", "State", "Access", "Cac", "Cache", "Name", "Consist", "sCC", "Size", "vdNum" + ] + + props_stm = qh.get_props_stm({**virt_drive, **{'vdNum': vidx}}, supported_attr=s_attr) + + query.append( + "CREATE ({})-[:HAS_VIRTUAL_DRIVE]->({}:VirtualDrive {{ {} }})" + .format(ctrl_node, vd_node, props_stm) + ) + + # connect PDs & VDs + for pidx in virt_drive["DID"]: + + query.append( + "CREATE ({})<-[:BELONGS_TO_VIRTUAL_SPACE]-(pd{})" + .format(vd_node, pidx) + ) + # print("\n".join(query)) session.run("\n".join(query)) @@ -267,22 +387,35 @@ def create_server(key, attr, server_variation=ServerVariations.Server): if server_variation == ServerVariations.ServerWithBMC: - if 'sensor_def' in attr and attr['sensor_def']: - sensor_file = os.path.expanduser(attr['sensor_def']) - else: - sensor_file = os.path.join(os.path.dirname(__file__), 'presets/sensors.json') + # if preset is provided -> use the user-defined file + f_loc = os.path.dirname(__file__) + s_def_file = lambda p, j: os.path.expanduser(attr[p]) if p in attr and attr[p] else os.path.join(f_loc, 'presets/' + j) + + sensor_file = s_def_file('sensor_def', 'sensors.json') + storage_def_file = s_def_file('storage_def', 'storage.json') + storage_state_file = s_def_file('storage_states', 'storage_states.json') _add_sensors(key, sensor_file) + _add_storage(key, storage_def_file, storage_state_file) - # add PSUs to the model - for i in range(attr['psu_num']): - psu_attr = { - "power_consumption": attr['psu_power_consumption'], - "power_source": attr['psu_power_source'], - "variation": server_variation.name.lower(), - "draw": attr['psu_load'][i] if attr['psu_load'] else 1 - } - _add_psu(key, psu_index=i+1, attr=psu_attr) + if server_variation == ServerVariations.ServerWithBMC: + with open(sensor_file) as preset_handler, GRAPH_REF.get_session() as session: + data = json.load(preset_handler) + for psu in data['psu']: + _add_psu(key, psu_index=psu['id'], attr=psu) + + + else: + # add PSUs to the model + for i in range(attr['psu_num']): + psu_attr = { + "power_consumption": attr['psu_power_consumption'], + "power_source": attr['psu_power_source'], + "variation": server_variation.name.lower(), + "draw": attr['psu_load'][i] if attr['psu_load'] else 1, + "id": i + } + _add_psu(key, psu_index=i+1, attr=psu_attr) def create_ups(key, attr, preset_file=os.path.join(os.path.dirname(__file__), 'presets/apc_ups.json')): @@ -479,15 +612,12 @@ def delete_asset(key): DETACH DELETE a,s,oid,sd,b,sn,as,cp""", key=key) -def set_thermal_sensor_target(attr): - """Set-up a new thermal relationship between 2 sensors or configure the existing one +def set_thermal_storage_target(attr): + """Set up storage components as thermal targets for IPMI sensor Returns: bool: True if a new relationship was created """ - if attr['source_sensor'] == attr['target_sensor']: - raise KeyError('Sensor cannot affect itself!') - query = [] # find the source sensor & server asset @@ -496,13 +626,37 @@ def set_thermal_sensor_target(attr): .format(attr['source_sensor'], attr['asset_key']) ) - # find the destination or target sensor query.append( - 'MATCH (target {{ name: "{}" }} )<-[:HAS_SENSOR]-(server)' - .format(attr['target_sensor']) + "MATCH (server)-[:HAS_CONTROLLER]->(ctrl:Controller {{ controllerNum: {} }})" + .format(attr['controller']) ) - # determine relationship type + if attr['cache_vault']: + query.append( + "MATCH (ctrl)-[:HAS_CACHEVAULT]->(target:CacheVault {{ serialNumber: \"{}\" }})" + .format(attr['cache_vault']) + ) + elif attr['drive']: + query.append( + "MATCH (ctrl)-[:HAS_PHYSICAL_DRIVE]->(target:PhysicalDrive {{ DID: {} }})" + .format(attr['drive']) + ) + else: + raise KeyError('Must provide either target drive or cache_vault') + + return _set_thermal_target(attr, query) + + +def _set_thermal_target(attr, query): + """Set thermal relationship between 2 ndoes + Args: + attr(dict): relationship properties (such as rate, event, degrees etc. ) + query(list): query that includes look up of the 'target' & 'source' nodes + Returns: + bool: True if the relationship is new + """ + + # determine relationship type thermal_rel_type = '' if attr['action'] == 'increase': thermal_rel_type = 'HEATED_BY' @@ -511,27 +665,53 @@ def set_thermal_sensor_target(attr): else: raise KeyError('Unrecognized event type: {}'.format(attr['event'])) - - # set the thermal relationship & relationship attributes - s_attr = ["pause_at", 'rate', 'event', 'degrees', 'jitter', 'action', 'model'] - set_stm = qh.get_set_stm(attr, node_name="rel", supported_attr=s_attr) - - query.append("MERGE (source)<-[rel:{}]-(target)".format(thermal_rel_type)) - query.append("SET {}".format(set_stm)) - + # fist check if the relationship already exists rel_query = [] rel_query.append("MATCH (source)<-[ex_rel:{}]-(target)".format(thermal_rel_type)) rel_query.append("RETURN ex_rel") with GRAPH_REF.get_session() as session: - - result = session.run("\n".join(query[0:2] + rel_query)) + + result = session.run("\n".join(query + rel_query)) rel_exists = result.single() + # set the thermal relationship & relationship attributes + s_attr = ["pause_at", 'rate', 'event', 'degrees', 'jitter', 'action', 'model'] + set_stm = qh.get_set_stm(attr, node_name="rel", supported_attr=s_attr) + + query.append("MERGE (source)<-[rel:{}]-(target)".format(thermal_rel_type)) + query.append("SET {}".format(set_stm)) + session.run("\n".join(query)) return rel_exists is None +def set_thermal_sensor_target(attr): + """Set-up a new thermal relationship between 2 sensors or configure the existing one + Returns: + bool: True if a new relationship was created + """ + + if attr['source_sensor'] == attr['target_sensor']: + raise KeyError('Sensor cannot affect itself!') + + query = [] + + # find the source sensor & server asset + query.append( + 'MATCH (source {{ name: "{}" }} )<-[:HAS_SENSOR]-(server:Asset {{ key: {} }})' + .format(attr['source_sensor'], attr['asset_key']) + ) + + # find the destination or target sensor + query.append( + 'MATCH (target {{ name: "{}" }} )<-[:HAS_SENSOR]-(server)' + .format(attr['target_sensor']) + ) + + return _set_thermal_target(attr, query) + + def set_thermal_cpu_target(attr): """Set-up a new thermal relationship between a sensor and CPU load of the server sensor belongs to @@ -617,4 +797,39 @@ def delete_thermal_cpu_target(attr): with GRAPH_REF.get_session() as session: session.run("\n".join(query)) - \ No newline at end of file + + +def delete_thermal_storage_target(attr): + """Remove a connection between a sensor and storage component""" + query = [] + + # find the source sensor & server asset + query.append( + 'MATCH (source {{ name: "{}" }} )<-[:HAS_SENSOR]-(server:Asset {{ key: {} }})' + .format(attr['source_sensor'], attr['asset_key']) + ) + + # find the destination or target + if 'cache_vault' in attr and attr['cache_vault']: + target_prop = ':CacheVault {{ serialNumber: "{}" }}'.format(attr['cache_vault']) + elif 'drive' in attr and attr['drive']: + target_prop = ':PhysicalDrive {{ DID: {} }}'.format(attr['drive']) + + query.append( + "MATCH (server)-[:HAS_CONTROLLER]->(ctrl:Controller {{ controllerNum: {} }})" + .format(attr['controller']) + ) + + ctrl_str_element_rel = ":HAS_CACHEVAULT|:HAS_PHYSICAL_DRIVE" + + # find either a physical drive or cachevault affected by the source sensor + # that belongs to certain controller + query.append( + 'MATCH (source)<-[thermal_link {{ event: "{}" }}]-({})<-[{}]-(ctrl)' + .format(attr['event'], target_prop, ctrl_str_element_rel) + ) + + query.append('DELETE thermal_link') # delete the connection + + with GRAPH_REF.get_session() as session: + session.run("\n".join(query)) diff --git a/enginecore/enginecore/state/agent/__init__.py b/enginecore/enginecore/state/agent/__init__.py new file mode 100644 index 00000000..e6d6df42 --- /dev/null +++ b/enginecore/enginecore/state/agent/__init__.py @@ -0,0 +1,4 @@ +"""wrappers around 3rd party simulators""" +from enginecore.state.agent.ipmi_agent import IPMIAgent +from enginecore.state.agent.snmp_agent import SNMPAgent +from enginecore.state.agent.storcli_emu import StorCLIEmulator diff --git a/enginecore/enginecore/state/agent/agent.py b/enginecore/enginecore/state/agent/agent.py new file mode 100644 index 00000000..7b77b3e3 --- /dev/null +++ b/enginecore/enginecore/state/agent/agent.py @@ -0,0 +1,37 @@ +"""Interface for 3-rd party programs managed by the assets (e.g. ipmi_sim, snmpsimd)""" +import atexit + + +class Agent(): + """Abstract Agent Class """ + agent_num = 1 + + + def __init__(self): + self._process = None + + + def start_agent(self): + """Logic for starting up the agent """ + raise NotImplementedError + + + @property + def pid(self): + """Get agent process id""" + return self._process.pid + + + def stop_agent(self): + """Logic for agent's termination """ + if not self._process.poll(): + self._process.kill() + + + def register_process(self, process): + """Set process instance + Args: + process(Popen): process to be managed + """ + self._process = process + atexit.register(self.stop_agent) diff --git a/enginecore/enginecore/state/agents.py b/enginecore/enginecore/state/agent/ipmi_agent.py similarity index 62% rename from enginecore/enginecore/state/agents.py rename to enginecore/enginecore/state/agent/ipmi_agent.py index 5a7eceed..05cd4cd7 100644 --- a/enginecore/enginecore/state/agents.py +++ b/enginecore/enginecore/state/agent/ipmi_agent.py @@ -1,67 +1,23 @@ -"""Aggregates 3-rd party programs managed by the assets (including ipmi_sim and snmpsimd instances) +"""IPMI LAN BMC Simulator that can be accessed using the IPMI 1.5 or 2.0 protocols +This wrapper can configure sensor definitions & manage ipmi_sim program instance """ -import subprocess import os -import atexit +import subprocess import logging from distutils import dir_util import sysconfig -import pwd -import grp -import tempfile -from string import Template - -class Agent(): - """Abstract Agent Class """ - agent_num = 1 - - - def __init__(self): - self._process = None - - - def start_agent(self): - """Logic for starting up the agent """ - raise NotImplementedError - - - @property - def pid(self): - """Get agent process id""" - return self._process.pid +from string import Template - def stop_agent(self): - """Logic for agent's termination """ - if not self._process.poll(): - self._process.kill() - +from enginecore.model.supported_sensors import SUPPORTED_SENSORS +from enginecore.state.agent.agent import Agent - def register_process(self, process): - """Set process instance - Args: - process(Popen): process to be managed - """ - self._process = process - atexit.register(self.stop_agent) - class IPMIAgent(Agent): - """IPMIsim instance """ - - supported_sensors = { - 'caseFan': '', - 'psuStatus': '', - 'psuVoltage': '', - 'psuPower': '', - 'psuCurrent': '', - 'psuTemperature': '', - 'memoryTemperature': '', - 'Ambient': '', - 'RAIDControllerTemperature': '', - 'cpuTemperature': '' - } + """Python wrapper around ipmi_sim program""" + + supported_sensors = dict.fromkeys(SUPPORTED_SENSORS, "") def __init__(self, key, ipmi_dir, ipmi_config, sensors): super(IPMIAgent, self).__init__() @@ -82,7 +38,7 @@ def __init__(self, key, ipmi_dir, ipmi_config, sensors): sensor_def = os.path.join(self._ipmi_dir, 'main.sdrs') lib_path = os.path.join(sysconfig.get_config_var('LIBDIR'), "simengine", 'haos_extend.so') - + # Template options lan_conf_opt = { 'asset_key': key, @@ -206,67 +162,3 @@ def start_agent(self): def __exit__(self, exc_type, exc_value, traceback): self.stop_agent() - - -class SNMPAgent(Agent): - """SNMP simulator instance """ - - def __init__(self, key, host, port, public_community='public', private_community='private', lookup_oid='1.3.6'): - - super(SNMPAgent, self).__init__() - self._key_space_id = key - - # set up community strings - self._snmp_rec_public_fname = public_community + '.snmprec' - self._snmp_rec_private_fname = private_community + '.snmprec' - - sys_temp = tempfile.gettempdir() - simengine_temp = os.path.join(sys_temp, 'simengine') - - self._snmp_rec_dir = os.path.join(simengine_temp, str(key)) - os.makedirs(self._snmp_rec_dir) - self._host = '{}:{}'.format(host, port) - - # snmpsimd.py will be run by a user 'nobody' - uid = pwd.getpwnam("nobody").pw_uid - gid = grp.getgrnam("nobody").gr_gid - - # change ownership - os.chown(self._snmp_rec_dir, uid, gid) - snmp_rec_public_filepath = os.path.join(self._snmp_rec_dir, self._snmp_rec_public_fname) - snmp_rec_private_filepath = os.path.join(self._snmp_rec_dir, self._snmp_rec_private_fname) - - # get location of the lua script that will be executed by snmpsimd - redis_script_sha = os.environ.get('SIMENGINE_SNMP_SHA') - snmpsim_config = "{}|:redis|key-spaces-id={},evalsha={}\n".format(lookup_oid, key, redis_script_sha) - - with open(snmp_rec_public_filepath, "a") as tmp_pub, open(snmp_rec_private_filepath, "a") as tmp_priv: - tmp_pub.write(snmpsim_config) - tmp_priv.write(snmpsim_config) - - self.start_agent() - - SNMPAgent.agent_num += 1 - - - def start_agent(self): - """Logic for starting up the agent """ - - log_file = os.path.join(self._snmp_rec_dir, "snmpsimd.log") - - # start a new one - cmd = ["snmpsimd.py", - "--agent-udpv4-endpoint={}".format(self._host), - "--variation-module-options=redis:host:127.0.0.1,port:6379,db:0,key-spaces-id:"+str(self._key_space_id), - "--data-dir="+self._snmp_rec_dir, - "--transport-id-offset="+str(SNMPAgent.agent_num), - "--process-user=nobody", - "--process-group=nobody", - # "--daemonize", - "--logging-method=file:"+log_file - ] - - logging.info('Starting agent: %s', ' '.join(cmd)) - self.register_process(subprocess.Popen( - cmd, stderr=subprocess.DEVNULL, close_fds=True - )) diff --git a/enginecore/enginecore/state/agent/snmp_agent.py b/enginecore/enginecore/state/agent/snmp_agent.py new file mode 100644 index 00000000..1242401a --- /dev/null +++ b/enginecore/enginecore/state/agent/snmp_agent.py @@ -0,0 +1,74 @@ +"""A wrapper for managing snmpsimd progoram +""" + +import subprocess +import os +import logging +import pwd +import grp +import tempfile +from enginecore.state.agent.agent import Agent + + +class SNMPAgent(Agent): + """SNMP simulator """ + + def __init__(self, key, host, port, public_community='public', private_community='private', lookup_oid='1.3.6'): + + super(SNMPAgent, self).__init__() + self._key_space_id = key + + # set up community strings + self._snmp_rec_public_fname = public_community + '.snmprec' + self._snmp_rec_private_fname = private_community + '.snmprec' + + sys_temp = tempfile.gettempdir() + simengine_temp = os.path.join(sys_temp, 'simengine') + + self._snmp_rec_dir = os.path.join(simengine_temp, str(key)) + os.makedirs(self._snmp_rec_dir) + self._host = '{}:{}'.format(host, port) + + # snmpsimd.py will be run by a user 'nobody' + uid = pwd.getpwnam("nobody").pw_uid + gid = grp.getgrnam("nobody").gr_gid + + # change ownership + os.chown(self._snmp_rec_dir, uid, gid) + snmp_rec_public_filepath = os.path.join(self._snmp_rec_dir, self._snmp_rec_public_fname) + snmp_rec_private_filepath = os.path.join(self._snmp_rec_dir, self._snmp_rec_private_fname) + + # get location of the lua script that will be executed by snmpsimd + redis_script_sha = os.environ.get('SIMENGINE_SNMP_SHA') + snmpsim_config = "{}|:redis|key-spaces-id={},evalsha={}\n".format(lookup_oid, key, redis_script_sha) + + with open(snmp_rec_public_filepath, "a") as tmp_pub, open(snmp_rec_private_filepath, "a") as tmp_priv: + tmp_pub.write(snmpsim_config) + tmp_priv.write(snmpsim_config) + + self.start_agent() + + SNMPAgent.agent_num += 1 + + + def start_agent(self): + """Logic for starting up the agent """ + + log_file = os.path.join(self._snmp_rec_dir, "snmpsimd.log") + + # start a new one + cmd = ["snmpsimd.py", + "--agent-udpv4-endpoint={}".format(self._host), + "--variation-module-options=redis:host:127.0.0.1,port:6379,db:0,key-spaces-id:"+str(self._key_space_id), + "--data-dir="+self._snmp_rec_dir, + "--transport-id-offset="+str(SNMPAgent.agent_num), + "--process-user=nobody", + "--process-group=nobody", + # "--daemonize", + "--logging-method=file:"+log_file + ] + + logging.info('Starting agent: %s', ' '.join(cmd)) + self.register_process(subprocess.Popen( + cmd, stderr=subprocess.DEVNULL, close_fds=True + )) diff --git a/enginecore/enginecore/state/agent/storcli_emu.py b/enginecore/enginecore/state/agent/storcli_emu.py new file mode 100644 index 00000000..6710812d --- /dev/null +++ b/enginecore/enginecore/state/agent/storcli_emu.py @@ -0,0 +1,529 @@ + +"""storcli64 emulator that provides CLI access to (virtual) storage +""" + +import os +import logging +from distutils import dir_util + +import json +import socket +import threading +import copy +from string import Template + +from enginecore.model.graph_reference import GraphReference +from enginecore.model.query_helpers import to_camelcase + + +class StorCLIEmulator(): + """This component emulates storcli behaviour + - runs a websocket server that listens to any incoming commands from a vm + """ + + pd_header = [ + "EID:Slt", "DID", "State", "DG", "Size", "Intf", "Med", "SED", "PI", "SeSz", "Model", "Sp", "Type" + ] + + vd_header = ["DG/VD", "TYPE", "State", "Access", "Consist", "Cache", "Cac", "sCC", "Size", "Name"] + + topology_header = [ + "DG", # disk group idx + "Arr", # array idx + "Row", + "EID:Slot", # enclosure device ID + "DID", + "Type", + "State", + "BT", # background task + "Size", + "PDC", # pd cache + "PI", # protection info + "SED", # self encrypting drive + "DS3", # Dimmer Switch 3 + "FSpace", # free space present + "TR" # transport ready + ] + + def __init__(self, asset_key, server_dir, socket_port): + + self._graph_ref = GraphReference() + self._server_key = asset_key + + with self._graph_ref.get_session() as session: + self._storcli_details = GraphReference.get_storcli_details(session, asset_key) + + self._storcli_dir = os.path.join(server_dir, "storcli") + + os.makedirs(self._storcli_dir) + dir_util.copy_tree(os.environ.get('SIMENGINE_STORCLI_TEMPL'), self._storcli_dir) + + self._socket_t = threading.Thread( + target=self._listen_cmds, + args=(socket_port,), + name="storcli64:{}".format(asset_key) + ) + + self._socket_t.daemon = True + self._socket_t.start() + + + def _strcli_header(self, ctrl_num=0, status='Success'): + """Reusable header for storcli output""" + + with open(os.path.join(self._storcli_dir, 'header')) as templ_h: + options = { + 'cli_version': self._storcli_details['CLIVersion'], + 'op_sys': self._storcli_details['operatingSystem'], + 'status': status, + 'description': 'None', + 'controller_line': 'Controller = {}\n'.format(ctrl_num) if ctrl_num else '' + } + + template = Template(templ_h.read()) + return template.substitute(options) + + + + def _strcli_ctrlcount(self): + """Number of adapters per server """ + + template_f_path = os.path.join(self._storcli_dir, 'adapter_count') + with open(template_f_path) as templ_h, self._graph_ref.get_session() as session: + + options = { + 'header': self._strcli_header(), + 'ctrl_count': GraphReference.get_controller_count(session, self._server_key) + } + + template = Template(templ_h.read()) + return template.substitute(options) + + def _strcli_ctrl_perf_mode(self, controller_num): + """Current performance mode (hardcoded)""" + with open(os.path.join(self._storcli_dir, 'performance_mode')) as templ_h: + + options = { + 'header': self._strcli_header(controller_num), + 'mode_num': 0, + 'mode_description': 'tuned to provide Best IOPS' + } + + template = Template(templ_h.read()) + return template.substitute(options) + + + def _strcli_ctrl_alarm_state(self, controller_num): + """Get controller alarm state""" + + alarm_state_f_path = os.path.join(self._storcli_dir, 'alarm_state') + with open(alarm_state_f_path) as templ_h, self._graph_ref.get_session() as session: + + ctrl_info = GraphReference.get_controller_details(session, self._server_key, controller_num) + + options = { + 'header': self._strcli_header(controller_num), + 'alarm_state': ctrl_info['alarmState'] + } + + template = Template(templ_h.read()) + return template.substitute(options) + + + def _strcli_ctrl_bbu(self, controller_num): + """Battery backup unit output for storcli""" + + with open(os.path.join(self._storcli_dir, 'bbu_data')) as templ_h: + + options = { + 'header': self._strcli_header(controller_num), + 'ctrl_num': controller_num, + 'status': 'Failed', + 'property': '-', + 'err_msg': 'use /cx/cv', + 'err_code': 255 + } + + template = Template(templ_h.read()) + return template.substitute(options) + + + def _get_rate_prop(self, controller_num, rate_type): + """Get controller rate property (rate type matches rate template file and the rate template value)""" + + rate_file = os.path.join(self._storcli_dir, rate_type) + with open(rate_file) as templ_h, self._graph_ref.get_session() as session: + + ctrl_info = GraphReference.get_controller_details(session, self._server_key, controller_num) + + options = {} + options['header'] = self._strcli_header(controller_num) + options[rate_type] = ctrl_info[to_camelcase(rate_type)] + + template = Template(templ_h.read()) + return template.substitute(options) + + + def _strcli_ctrl_info(self, controller_num): + """Return aggregated information for a particular controller (show all)""" + + ctrl_info_f = os.path.join(self._storcli_dir, 'controller_info') + ctrl_entry_f = os.path.join(self._storcli_dir, 'controller_entry') + + with open(ctrl_info_f) as info_h, open(ctrl_entry_f) as entry_h, self._graph_ref.get_session() as session: + + ctrl_info = GraphReference.get_controller_details(session, self._server_key, controller_num) + + ctrl_info_templ_keys = [ + 'serial_number', 'model', 'serial_number', 'mfg_date', + 'SAS_address', 'PCI_address', 'rework_date', + 'memory_correctable_errors', 'memory_uncorrectable_errors', + 'rebuild_rate', 'pr_rate', 'bgi_rate', 'cc_rate' + ] + + + entry_options = { + 'controller_num': controller_num, + 'controller_date': '', + 'system_date': '', + 'status': 'Optimal', + } + + for key in ctrl_info_templ_keys: + entry_options[key] = ctrl_info[to_camelcase(key)] + + drives = GraphReference.get_all_drives(session, self._server_key, controller_num) + topology = [] + + ctrl_state = copy.deepcopy(self._storcli_details['stateConfig']['controller']['Optimal']) + ctrl_state['memoryCorrectableErrors'] = ctrl_info['memoryCorrectableErrors'] + ctrl_state['memoryUncorrectableErrors'] = ctrl_info['memoryUncorrectableErrors'] + + for i, v_drive in enumerate(drives['vd']): + + vd_state = self._storcli_details['stateConfig']['virtualDrive']['Optl'] + + # Add Virtual Drive output + v_drive['DG/VD'] = '0/' + str(i) + v_drive['Size'] = str(v_drive['Size']) + ' GB' + + # Add physical drive output (do some formatting plus check pd states) + for p_drive in v_drive['pd']: + vd_state['mediaErrorCount'] += p_drive['mediaErrorCount'] + vd_state['otherErrorCount'] += p_drive['otherErrorCount'] + + if p_drive['State'] == 'Offln': + vd_state['numPdOffline'] += 1 + + v_drive['State'] = self._get_state_from_config('virtualDrive', vd_state, 'Optl') + + topology.append({ + 'DG': 0, + 'Arr': '-', + 'Row': '-', + 'EID:Slot': '-', + 'DID': '-', + 'Type': v_drive['TYPE'], + 'State': v_drive['State'], + 'BT': 'N', + 'Size': v_drive['Size'], + 'PDC': 'disable', + 'PI': 'N', + 'SED': 'N', + 'DS3': 'none', + 'FSpace': 'N', + 'TR': 'N' + }) + + # Add physical drive output (do some formatting plus check pd states) + p_topology = [] + for p_drive in drives['pd']: + p_drive['EID:Slt'] = '{}:{}'.format(p_drive['EID'], p_drive['slotNum']) + p_drive['Size'] = str(p_drive['Size']) + ' GB' + + if p_drive['State'] == 'Offln': + ctrl_state['numPdOffline'] += 1 + + p_topology.append({ + 'DG': 0, + 'Arr': 0, + 'Row': p_drive['slotNum'], + 'EID:Slot': p_drive['EID:Slt'], + 'DID': '-', + 'Type': 'DRIVE', + 'State': p_drive['State'], + 'BT': 'N', + 'Size': p_drive['Size'], + 'PDC': 'disable', + 'PI': 'N', + 'SED': 'N', + 'DS3': 'none', + 'FSpace': 'N', + 'TR': 'N' + }) + + + topology.extend(sorted(p_topology, key=lambda k: k['Row'])) + entry_options['status'] = self._get_state_from_config('controller', ctrl_state, 'Optimal') + + # get cachevault details: + cv_info = GraphReference.get_cachevault(session, self._server_key, controller_num) + cv_table = { + "Model": cv_info['model'], + "State": cv_info['state'], + "Temp": str(cv_info['temperature']) + "C", + "Mode": "-", + "MfgDate": cv_info['mfgDate'] + } + + + info_options = { + 'header': self._strcli_header(controller_num), + 'controller_entry': Template(entry_h.read()).substitute(entry_options), + 'num_virt_drives': len(drives['vd']), + 'num_phys_drives': len(drives['pd']), + 'topology': self._format_as_table(StorCLIEmulator.topology_header, topology), + 'virtual_drives': self._format_as_table(StorCLIEmulator.vd_header, drives['vd']), + 'physical_drives': self._format_as_table(StorCLIEmulator.pd_header, drives['pd']), + 'cachevault': self._format_as_table(cv_table.keys(), [cv_table]) + } + + info_template = Template(info_h.read()) + return info_template.substitute(info_options) + + + def _strcli_ctrl_cachevault(self, controller_num): + """Cachevault output for storcli""" + + cv_f = os.path.join(self._storcli_dir, 'cachevault_data') + with open(os.path.join(self._storcli_dir, cv_f)) as templ_h, self._graph_ref.get_session() as session: + + cv_info = GraphReference.get_cachevault(session, self._server_key, controller_num) + cv_info['mfgDate'] = '/'.join(reversed(cv_info['mfgDate'].split('/'))) # dumb storcli (change date format) + options = { + **{'header': self._strcli_header(controller_num)}, + **cv_info + } + + template = Template(templ_h.read()) + return template.substitute(options) + + + def _strcli_ctrl_phys_disks(self, controller_num): + """Storcli physical drive details""" + + pd_info_f = os.path.join(self._storcli_dir, 'physical_disk_data') + pd_entry_f = os.path.join(self._storcli_dir, 'physical_disk_entry') + pd_output = [] + + info_options = { + 'header': self._strcli_header(controller_num), + 'physical_drives': '' + } + + with open(pd_info_f) as info_h, open(pd_entry_f) as entry_h, self._graph_ref.get_session() as session: + drives = GraphReference.get_all_drives(session, self._server_key, controller_num) + pd_drives = sorted(drives['pd'], key=lambda k: k['slotNum']) + pd_template = entry_h.read() + + for drive in pd_drives: + + drive['EID:Slt'] = '{}:{}'.format(drive['EID'], drive['slotNum']) + drive['Size'] = str(drive['Size']) + ' GB' + + entry_options = { + 'drive_path': '/c{}/e{}/s{}'.format(controller_num, drive['EID'], drive['slotNum']), + 'drive_table': self._format_as_table(StorCLIEmulator.pd_header, [drive]), + 'media_error_count': drive['mediaErrorCount'], + 'other_error_count': drive['otherErrorCount'], + 'predictive_failure_count': drive['predictiveErrorCount'], + 'drive_temp_c': drive['temperature'], + 'drive_temp_f': (drive['temperature'] * 9/5) + 32, + 'drive_model': drive['Model'], + 'drive_size': drive['Size'] + } + + pd_output.append(Template(pd_template).substitute(entry_options)) + + + info_options['physical_drives'] = '\n'.join(pd_output) + return Template(info_h.read()).substitute(info_options) + + + def _format_as_table(self, headers, table_options): + """Formats data as storcli table + Args: + headers(list): table header + table_options(dict): table values + Returns: + str: storcli table populated with data + """ + + value_rows = [] + + # store row with the max char count in a column + header_lengths = {key: len(str(key)) for key in headers} + + for table_row in table_options: + + row_str = "" + for col_key in headers: + val_len = len(str(table_row[col_key])) + # whitespace padding + val_len = val_len if val_len >= len(col_key) else len(col_key) + + if val_len > header_lengths[col_key]: + header_lengths[col_key] = val_len + + row_str += '{val:<{width}}'.format(val=table_row[col_key], width=val_len+1) + + value_rows.append(row_str) + + header = ' '.join(['{val:<{width}}'.format(val=key, width=header_lengths[key]) for key in header_lengths]) + divider = '-'*len(header) + '\n' + + return str( + divider + + header + '\n' + + divider + + '\n'.join(value_rows) + '\n' + + divider + ) + + + def _get_state_from_config(self, config_entry, current_state, optimal): + """Configure state based on the configuration json file""" + s_conf = self._storcli_details['stateConfig'][config_entry] + for status in s_conf: + for prop in current_state: + if current_state[prop] >= s_conf[status][prop] and s_conf[status][prop] != -1 and status != optimal: + return status + + return optimal + + + + def _get_virtual_drives(self, controller_num): + """Retrieve virtual drive data""" + + drives = [] + + with self._graph_ref.get_session() as session: + + vd_details = GraphReference.get_virtual_drive_details(session, self._server_key, controller_num) + + # iterate over virtual drives + for i, v_drive in enumerate(vd_details): + vd_state = copy.deepcopy(self._storcli_details['stateConfig']['virtualDrive']['Optl']) + + # Add Virtual Drive output + v_drive['DG/VD'] = '0/' + str(i) + v_drive['Size'] = str(v_drive['Size']) + ' GB' + + # Add physical drive output (do some formatting plus check pd states) + for p_drive in v_drive['pd']: + p_drive['EID:Slt'] = '{}:{}'.format(p_drive['EID'], p_drive['slotNum']) + p_drive['Size'] = str(p_drive['Size']) + ' GB' + + vd_state['mediaErrorCount'] += p_drive['mediaErrorCount'] + vd_state['otherErrorCount'] += p_drive['otherErrorCount'] + + if p_drive['State'] == 'Offln': + vd_state['numPdOffline'] += 1 + + v_drive['State'] = self._get_state_from_config('virtualDrive', vd_state, 'Optl') + + drives.append({ + 'physical_drives': self._format_as_table(StorCLIEmulator.pd_header, v_drive['pd']), + 'virtual_drives': self._format_as_table(StorCLIEmulator.vd_header, [v_drive]), + 'virtual_drives_num': i + }) + + return drives + + def _strcli_ctrl_virt_disk(self, controller_num): + """Display virtual disk details """ + + vd_file = os.path.join(self._storcli_dir, 'virtual_drive_data') + with open(vd_file) as templ_h: + + template = Template(templ_h.read()) + + # get virtual & physical drive details + drives = self._get_virtual_drives(controller_num) + vd_output = map(lambda d: template.substitute({**d, **{'controller': controller_num}}), drives) + + return self._strcli_header(controller_num) + '\n' + '\n'.join(vd_output) + + + def _listen_cmds(self, socket_port): + """Start storcli websocket server """ + + serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + serversocket.bind(('', socket_port)) + serversocket.listen(5) + + conn, _ = serversocket.accept() + + with conn: + while True: + + data = conn.recv(1024) + if not data: + break + + try: + received = json.loads(data) + except json.decoder.JSONDecodeError as parse_err: + logging.info(data) + logging.info('Invalid JSON: ') + logging.info(parse_err) + + argv = received['argv'] + + logging.info('Data received: %s', str(received)) + + reply = {"stdout": "", "stderr": "", "status": 0} + + # Process non-default return cases + if len(argv) == 2: + if argv[1] == "--version": + reply['stdout'] = "Version 0.01" + + elif len(argv) == 3: + if argv[1] == "show" and argv[2] == "ctrlcount": + reply['stdout'] = self._strcli_ctrlcount() + + # Controller Commands + elif len(argv) == 4 and argv[1].startswith("/c"): + if argv[2] == "show" and argv[3] == "perfmode": + reply['stdout'] = self._strcli_ctrl_perf_mode(argv[1][-1]) + elif argv[2] == "show" and argv[3] == "bgirate": + reply['stdout'] = self._get_rate_prop(argv[1][-1], 'bgi_rate') + elif argv[2] == "show" and argv[3] == "ccrate": + reply['stdout'] = self._get_rate_prop(argv[1][-1], 'cc_rate') + elif argv[2] == "show" and argv[3] == "rebuildrate": + reply['stdout'] = self._get_rate_prop(argv[1][-1], 'rebuild_rate') + elif argv[2] == "show" and argv[3] == "prrate": + reply['stdout'] = self._get_rate_prop(argv[1][-1], 'pr_rate') + elif argv[2] == "show" and argv[3] == "alarm": + reply['stdout'] = self._strcli_ctrl_alarm_state(argv[1][-1]) + elif argv[2] == "show" and argv[3] == "all": + reply['stdout'] = self._strcli_ctrl_info(argv[1][-1]) + + elif len(argv) == 5 and argv[1].startswith("/c"): + if argv[2] == "/bbu" and argv[3] == "show" and argv[4] == "all": + reply['stdout'] = self._strcli_ctrl_bbu(argv[1][-1]) + elif argv[2] == "/cv" and argv[3] == "show" and argv[4] == "all": + reply['stdout'] = self._strcli_ctrl_cachevault(argv[1][-1]) + elif argv[2] == "/vall" and argv[3] == "show" and argv[4] == "all": + reply['stdout'] = self._strcli_ctrl_virt_disk(argv[1][-1]) + elif len(argv) == 6 and argv[1].startswith("/c"): + if argv[2] == "/eall" and argv[3] == "/sall" and argv[4] == "show" and argv[5] == "all": + reply['stdout'] = self._strcli_ctrl_phys_disks(argv[1][-1]) + else: + reply = {"stdout": "", "stderr": "Usage: " + argv[0] +" --version", "status": 1} + + # Send the message + conn.sendall(bytes(json.dumps(reply) +"\n", "UTF-8")) diff --git a/enginecore/enginecore/state/api/__init__.py b/enginecore/enginecore/state/api/__init__.py new file mode 100644 index 00000000..a27fcf65 --- /dev/null +++ b/enginecore/enginecore/state/api/__init__.py @@ -0,0 +1,6 @@ +from enginecore.state.api.state import IStateManager +from enginecore.state.api.ups import IUPSStateManager +from enginecore.state.api.pdu import IPDUStateManager +from enginecore.state.api.outlet import IOutletStateManager +from enginecore.state.api.static import IStaticDeviceManager +from enginecore.state.api.server import IServerStateManager, IBMCServerStateManager diff --git a/enginecore/enginecore/state/api/outlet.py b/enginecore/enginecore/state/api/outlet.py new file mode 100644 index 00000000..ce1e463b --- /dev/null +++ b/enginecore/enginecore/state/api/outlet.py @@ -0,0 +1,5 @@ +from enginecore.state.api.state import IStateManager + + +class IOutletStateManager(IStateManager): + """Exposes state logic for Outlet asset """ diff --git a/enginecore/enginecore/state/api/pdu.py b/enginecore/enginecore/state/api/pdu.py new file mode 100644 index 00000000..4df219ec --- /dev/null +++ b/enginecore/enginecore/state/api/pdu.py @@ -0,0 +1,6 @@ +from enginecore.state.api.state import IStateManager + + +class IPDUStateManager(IStateManager): + """Handles state logic for PDU asset """ + \ No newline at end of file diff --git a/enginecore/enginecore/state/api/server.py b/enginecore/enginecore/state/api/server.py new file mode 100644 index 00000000..f9d26f4d --- /dev/null +++ b/enginecore/enginecore/state/api/server.py @@ -0,0 +1,176 @@ +import libvirt +import json + + +from enginecore.model.graph_reference import GraphReference +import enginecore.model.system_modeler as sys_modeler + +from enginecore.state.redis_channels import RedisChannels +from enginecore.state.api.state import IStateManager + + +class IServerStateManager(IStateManager): + + def __init__(self, asset_info): + super(IServerStateManager, self).__init__(asset_info) + self._vm_conn = libvirt.open("qemu:///system") + # TODO: error handling if the domain is missing (throws libvirtError) & close the connection + self._vm = self._vm_conn.lookupByName(asset_info['domainName']) + + + def vm_is_active(self): + """Check if vm is powered up""" + return self._vm.isActive() + + + def shut_down(self): + if self._vm.isActive(): + self._vm.destroy() + self._update_load(0) + return super().shut_down() + + + def power_off(self): + if self._vm.isActive(): + self._vm.destroy() + self._update_load(0) + return super().power_off() + + + def power_up(self): + powered = super().power_up() + if not self._vm.isActive() and powered: + self._vm.create() + self._update_load(self.power_usage) + return powered + + + +class IBMCServerStateManager(IServerStateManager): + + + def get_cpu_stats(self): + """Get VM cpu stats (user_time, cpu_time etc. (see libvirt api)) """ + return self._vm.getCPUStats(True) + + + @property + def cpu_load(self): + """Get latest recorded CPU load in percentage""" + cpu_load = IStateManager.get_store().get(self.redis_key + ":cpu_load") + return int(cpu_load.decode()) if cpu_load else 0 + + + @classmethod + def get_sensor_definitions(cls, asset_key): + """Get sensor definitions """ + graph_ref = GraphReference() + with graph_ref.get_session() as session: + return GraphReference.get_asset_sensors(session, asset_key) + + + @classmethod + def update_thermal_sensor_target(cls, attr): + """Create new or update existing thermal relationship between 2 sensors""" + new_rel = sys_modeler.set_thermal_sensor_target(attr) + if not new_rel: + return + + IStateManager.get_store().publish( + RedisChannels.sensor_conf_th_channel, + json.dumps({ + 'key': attr['asset_key'], + 'relationship': { + 'source': attr['source_sensor'], + 'target': attr['target_sensor'], + 'event': attr['event'] + } + }) + ) + + + @classmethod + def update_thermal_storage_target(cls, attr): + """Add new storage entity affected by a sensor + Notify sensors if the relationship is new""" + + new_rel = sys_modeler.set_thermal_storage_target(attr) + if not new_rel: + return + + target_data = { + 'key': attr['asset_key'], + 'relationship': { + 'source': attr['source_sensor'], + 'event': attr['event'], + 'controller': attr['controller'], + } + } + + if 'drive' in attr and attr['drive']: + channel = RedisChannels.str_drive_conf_th_channel + target_data['relationship']['drive'] = attr['drive'] + else: + channel = RedisChannels.str_cv_conf_th_channel + target_data['relationship']['cv'] = attr['cache_vault'] + + IStateManager.get_store().publish( + channel, + json.dumps(target_data) + ) + + + @classmethod + def delete_thermal_storage_target(cls, attr): + """Remove existing relationship between a sensor and a storage element""" + return sys_modeler.delete_thermal_storage_target(attr) + + + @classmethod + def update_thermal_cpu_target(cls, attr): + """Create new or update existing thermal relationship between CPU usage and sensor""" + new_rel = sys_modeler.set_thermal_cpu_target(attr) + if not new_rel: + return + + IStateManager.get_store().publish( + RedisChannels.cpu_usg_conf_th_channel, + json.dumps({ + 'key': attr['asset_key'], + 'relationship': { + 'target': attr['target_sensor'], + } + }) + ) + + + @classmethod + def get_thermal_cpu_details(cls, asset_key): + """Query existing cpu->sensor relationship""" + graph_ref = GraphReference() + with graph_ref.get_session() as session: + return GraphReference.get_thermal_cpu_details(session, asset_key) + + + @classmethod + def set_physical_drive_prop(cls, asset_key, controller, did, properties): + """Update physical drive""" + graph_ref = GraphReference() + with graph_ref.get_session() as session: + return GraphReference.set_physical_drive_prop(session, asset_key, controller, did, properties) + + + @classmethod + def set_controller_prop(cls, asset_key, controller, properties): + """Update RAID controller """ + graph_ref = GraphReference() + with graph_ref.get_session() as session: + return GraphReference.set_controller_prop(session, asset_key, controller, properties) + + + @classmethod + def set_cv_replacement(cls, asset_key, controller, repl_status): + """Update Cachevault details""" + graph_ref = GraphReference() + with graph_ref.get_session() as session: + return GraphReference.set_cv_replacement(session, asset_key, controller, repl_status) diff --git a/enginecore/enginecore/state/api/state.py b/enginecore/enginecore/state/api/state.py new file mode 100644 index 00000000..f6610e48 --- /dev/null +++ b/enginecore/enginecore/state/api/state.py @@ -0,0 +1,423 @@ + +import time +import os +import tempfile + +import redis + +from enginecore.model.graph_reference import GraphReference +from enginecore.state.utils import format_as_redis_key +from enginecore.state.redis_channels import RedisChannels + +from enginecore.state.asset_definition import SUPPORTED_ASSETS + + +class IStateManager(): + """Base class for all the state managers """ + + redis_store = None + + def __init__(self, asset_info): + self._graph_ref = GraphReference() + self._asset_key = asset_info['key'] + self._asset_info = asset_info + + + @property + def key(self): + """Asset Key """ + return self._asset_key + + + @property + def redis_key(self): + """Asset key in redis format as '{key}-{type}' """ + return "{}-{}".format(str(self.key), self.asset_type) + + + @property + def asset_type(self): + """Asset Type """ + return self._asset_info['type'] + + + @property + def power_usage(self): + """Normal power usage in AMPS when powered up""" + return 0 + + + @property + def draw_percentage(self): + """How much power the asset draws""" + return self._asset_info['draw'] if 'draw' in self._asset_info else 1 + + + @property + def load(self): + """Get current load stored in redis (in AMPs)""" + return float(IStateManager.get_store().get(self.redis_key + ":load")) + + + @property + def wattage(self): + """Asset wattage (assumes power-source to be 120v)""" + return self.load * 120 + + + @property + def status(self): + """Operational State + + Returns: + int: 1 if on, 0 if off + """ + return int(IStateManager.get_store().get(self.redis_key)) + + + @property + def agent(self): + """Agent instance details (if supported) + + Returns: + tuple: process id and status of the process (if it's running) + """ + pid = IStateManager.get_store().get(self.redis_key + ":agent") + return (int(pid), os.path.exists("/proc/" + pid.decode("utf-8"))) if pid else None + + + def shut_down(self): + """Implements state logic for graceful power-off event, sleeps for the pre-configured time + + Returns: + int: Asset's status after power-off operation + """ + print('Graceful shutdown') + self._sleep_shutdown() + if self.status: + self._set_state_off() + return self.status + + + def power_off(self): + """Implements state logic for abrupt power loss + + Returns: + int: Asset's status after power-off operation + """ + print("Powering down {}".format(self._asset_key)) + if self.status: + self._set_state_off() + return self.status + + + def power_up(self): + """Implements state logic for power up, sleeps for the pre-configured time & resets boot time + + Returns: + int: Asset's status after power-on operation + """ + print("Powering up {}".format(self._asset_key)) + if self._parents_available() and not self.status: + self._sleep_powerup() + # udpate machine start time & turn on + self._reset_boot_time() + self._set_state_on() + return self.status + + + def _update_load(self, load): + """Update amps""" + load = load if load >= 0 else 0 + IStateManager.get_store().set(self.redis_key + ":load", load) + self._publish_load() + + + def _publish_load(self): + """Publish load changes """ + IStateManager.get_store().publish(RedisChannels.load_update_channel, self.redis_key) + + + def _sleep_delay(self, delay_type): + """Sleep for n number of ms determined by the delay_type""" + if delay_type in self._asset_info: + time.sleep(self._asset_info[delay_type] / 1000.0) # ms to sec + + + def _sleep_shutdown(self): + """Hardware-specific shutdown delay""" + self._sleep_delay('offDelay') + + + def _sleep_powerup(self): + """Hardware-specific powerup delay""" + self._sleep_delay('onDelay') + + + def _set_redis_asset_state(self, state, publish=True): + """Update redis value of the asset power status""" + IStateManager.get_store().set(self.redis_key, state) + if publish: + self._publish_power() + + + def _set_state_on(self): + """Set state to online""" + self._set_redis_asset_state('1') + + + def _set_state_off(self): + """Set state to offline""" + self._set_redis_asset_state('0') + + + def _publish_power(self): + """Notify daemon of power updates""" + IStateManager.get_store().publish(RedisChannels.state_update_channel, self.redis_key) + + + def _get_oid_value(self, oid, key): + """Retrieve value for a specific OID """ + redis_store = IStateManager.get_store() + rkey = format_as_redis_key(str(key), oid, key_formatted=False) + return redis_store.get(rkey).decode().split('|')[1] + + + def _reset_boot_time(self): + """Reset device start time (used to calculate uptime)""" + IStateManager.get_store().set(str(self._asset_key) + ":start_time", int(time.time())) + + + def _update_oid_by_name(self, oid_name, oid_type, value, use_spec=False): + """Update a specific oid + Args: + oid_name(str): oid name defined in device preset file + oid_type(rfc1902): oid type (rfc1902 specs) + value(str): oid value or spec parameter if use_spec is set to True + use_spec: + Returns: + bool: true if oid was successfully updated + """ + + with self._graph_ref.get_session() as db_s: + oid, data_type, oid_spec = GraphReference.get_asset_oid_by_name( + db_s, int(self._asset_key), oid_name + ) + + if oid: + new_oid_value = oid_spec[value] if use_spec and oid_spec else value + self._update_oid_value(oid, data_type, oid_type(new_oid_value)) + return True + + return False + + + def _update_oid_value(self, oid, data_type, oid_value): + """Update oid with a new value + + Args: + oid(str): SNMP object id + data_type(int): Data type in redis format + oid_value(object): OID value in rfc1902 format + """ + redis_store = IStateManager.get_store() + + rvalue = "{}|{}".format(data_type, oid_value) + rkey = format_as_redis_key(str(self._asset_key), oid, key_formatted=False) + + redis_store.set(rkey, rvalue) + + + def _check_parents(self, keys, parent_down, msg='Cannot perform the action: [{}] parent is off'): + """Check that redis values pass certain condition + + Args: + keys (list): Redis keys (formatted as required) + parent_down (callable): lambda clause + msg (str, optional): Error message to be printed + + Returns: + bool: True if parent keys are missing or all parents were verified with parent_down clause + """ + if not keys: + return True + + parent_values = IStateManager.get_store().mget(keys) + pdown = 0 + pdown_msg = '' + for rkey, rvalue in zip(keys, parent_values): + if parent_down(rvalue, rkey): + pdown_msg += msg.format(rkey) + '\n' + pdown += 1 + + if pdown == len(keys): + print(pdown_msg) + return False + + return True + + + def _parents_available(self): + """Indicates whether a state action can be performed; + checks if parent nodes are up & running and all OIDs indicate 'on' status + + Returns: + bool: True if parents are available + """ + + not_affected_by_mains = True + asset_keys, oid_keys = GraphReference.get_parent_keys(self._graph_ref.get_session(), self._asset_key) + + if not asset_keys and not IStateManager.mains_status(): + not_affected_by_mains = False + + assets_up = self._check_parents(asset_keys, lambda rvalue, _: rvalue == b'0') + oid_clause = lambda rvalue, rkey: rvalue.split(b'|')[1].decode() == oid_keys[rkey]['switchOff'] + oids_on = self._check_parents(oid_keys.keys(), oid_clause) + + return assets_up and oids_on and not_affected_by_mains + + + @classmethod + def get_temp_workplace_dir(cls): + """Get location of the temp directory""" + sys_temp = tempfile.gettempdir() + simengine_temp = os.path.join(sys_temp, 'simengine') + return simengine_temp + + + @classmethod + def get_store(cls): + """Get redis db handler """ + if not cls.redis_store: + cls.redis_store = redis.StrictRedis(host='localhost', port=6379) + + return cls.redis_store + + + @classmethod + def _get_assets_states(cls, assets, flatten=True): + """Query redis store and find states for each asset + + Args: + flatten(bool): If false, the returned assets in the dict will have their child-components nested + Returns: + dict: Current information on assets including their states, load etc. + """ + asset_keys = assets.keys() + + if not asset_keys: + return None + + asset_values = cls.get_store().mget( + list(map(lambda k: "{}-{}".format(k, assets[k]['type']), asset_keys)) + ) + + for rkey, rvalue in zip(assets, asset_values): + + asset_state = cls.get_state_manager_by_key(rkey, SUPPORTED_ASSETS) + + assets[rkey]['status'] = int(rvalue) + assets[rkey]['load'] = asset_state.load + + if assets[rkey]['type'] == 'ups': + assets[rkey]['battery'] = asset_state.battery_level + + if not flatten and 'children' in assets[rkey]: + # call recursively on children + assets[rkey]['children'] = cls._get_assets_states(assets[rkey]['children']) + + return assets + + + @classmethod + def get_system_status(cls, flatten=True): + """Get states of all system components + + Args: + flatten(bool): If false, the returned assets in the dict will have their child-components nested + + Returns: + dict: Current information on assets including their states, load etc. + """ + graph_ref = GraphReference() + with graph_ref.get_session() as session: + + # cache assets + assets = GraphReference.get_assets_and_connections(session, flatten) + assets = cls._get_assets_states(assets, flatten) + return assets + + + @classmethod + def reload_model(cls): + """Request daemon reloading""" + cls.get_store().publish(RedisChannels.model_update_channel, 'reload') + + + @classmethod + def power_outage(cls): + """Simulate complete power outage/restoration""" + cls.get_store().set('mains-source', '0') + cls.get_store().publish(RedisChannels.mains_update_channel, '0') + + + @classmethod + def power_restore(cls): + """Simulate complete power restoration""" + cls.get_store().set('mains-source', '1') + cls.get_store().publish(RedisChannels.mains_update_channel, '1') + + + @classmethod + def mains_status(cls): + """Get wall power status""" + return int(cls.get_store().get('mains-source').decode()) + + + @classmethod + def get_ambient(cls): + """Retrieve current ambient value""" + temp = cls.get_store().get('ambient') + return int(temp.decode()) if temp else 0 + + + @classmethod + def set_ambient(cls, value): + """Update ambient value""" + old_temp = cls.get_ambient() + cls.get_store().set('ambient', str(int(value))) + cls.get_store().publish(RedisChannels.ambient_update_channel, '{}-{}'.format(old_temp, value)) + + + @classmethod + def get_ambient_props(cls): + """Get runtime ambient properties (ambient behaviour description)""" + graph_ref = GraphReference() + with graph_ref.get_session() as session: + props = GraphReference.get_ambient_props(session) + return props + + + @classmethod + def set_ambient_props(cls, props): + """Update runtime thermal properties of the room temperature""" + + graph_ref = GraphReference() + with graph_ref.get_session() as session: + GraphReference.set_ambient_props(session, props) + + + @classmethod + def get_state_manager_by_key(cls, key, supported_assets): + """Infer asset manager from key""" + + graph_ref = GraphReference() + + with graph_ref.get_session() as session: + + asset_info = GraphReference.get_asset_and_components(session, key) + sm_mro = supported_assets[asset_info['type']].StateManagerCls.mro() + + module = 'enginecore.state.api' + return next(filter(lambda x: x.__module__.startswith(module), sm_mro))(asset_info) diff --git a/enginecore/enginecore/state/api/static.py b/enginecore/enginecore/state/api/static.py new file mode 100644 index 00000000..1962c8ba --- /dev/null +++ b/enginecore/enginecore/state/api/static.py @@ -0,0 +1,16 @@ +from enginecore.state.api.state import IStateManager + + +class IStaticDeviceManager(IStateManager): + """Exposes state logic for static(dummy) asset """ + + @property + def power_usage(self): + return self._asset_info['powerConsumption'] / self._asset_info['powerSource'] + + + def power_up(self): + powered = super().power_up() + if powered: + self._update_load(self.power_usage) + return powered diff --git a/enginecore/enginecore/state/api/ups.py b/enginecore/enginecore/state/api/ups.py new file mode 100644 index 00000000..226f9224 --- /dev/null +++ b/enginecore/enginecore/state/api/ups.py @@ -0,0 +1,143 @@ +"""UPS asset interface """ +import time +from enum import Enum + +import pysnmp.proto.rfc1902 as snmp_data_types +from enginecore.model.graph_reference import GraphReference + +from enginecore.state.redis_channels import RedisChannels +from enginecore.state.api.state import IStateManager + + + + +class IUPSStateManager(IStateManager): + """Handles UPS state logic """ + + + class OutputStatus(Enum): + """UPS output status """ + onLine = 1 + onBattery = 2 + off = 3 + + + class InputLineFailCause(Enum): + """Reason for the occurrence of the last transfer to UPS """ + noTransfer = 1 + blackout = 2 + deepMomentarySag = 3 + + + def __init__(self, asset_info): + super().__init__(asset_info) + self._max_battery_level = 1000#% + + + @property + def battery_level(self): + """Get current level (high-precision)""" + return int(IStateManager.get_store().get(self.redis_key + ":battery").decode()) + + + @property + def battery_max_level(self): + """Max battery level""" + return self._max_battery_level + + + @property + def wattage(self): + return (self.load + self.idle_ups_amp) * self._asset_info['powerSource'] + + + @property + def idle_ups_amp(self): + """How much a UPS draws""" + return self._asset_info['powerConsumption'] / self._asset_info['powerSource'] + + + @property + def min_restore_charge_level(self): + """min level of battery charge before UPS can be powered on""" + return self._asset_info['minPowerOnBatteryLevel'] + + + @property + def full_recharge_time(self): + """hours taken to recharge the battery when it's completely depleted""" + return self._asset_info['fullRechargeTime'] + + + @property + def output_capacity(self): + """UPS rated capacity""" + return self._asset_info['outputPowerCapacity'] + + + def shut_down(self): + time.sleep(self.get_config_off_delay()) + powered = super().shut_down() + return powered + + + def power_up(self): + print("Powering up {}".format(self._asset_key)) + + if self.battery_level and not self.status: + self._sleep_powerup() + time.sleep(self.get_config_on_delay()) + # udpate machine start time & turn on + self._reset_boot_time() + self._set_state_on() + + powered = self.status + if powered: + self._reset_power_off_oid() + + return powered + + + def get_config_off_delay(self): + """Delay for power-off operation + (unlike 'hardware'-determined delay, this value can be configured by the user) + """ + with self._graph_ref.get_session() as db_s: + oid, _, _ = GraphReference.get_asset_oid_by_name(db_s, int(self._asset_key), 'AdvConfigShutoffDelay') + return int(self._get_oid_value(oid, key=self._asset_key)) + + + def get_config_on_delay(self): + """Power-on delay + (unlike 'hardware'-determined delay, this value can be configured by the user) + """ + + with self._graph_ref.get_session() as db_s: + oid, _, _ = GraphReference.get_asset_oid_by_name(db_s, int(self._asset_key), 'AdvConfigReturnDelay') + return int(self._get_oid_value(oid, key=self._asset_key)) + + + def _update_battery_process_speed(self, process_channel, factor): + """Speed up/slow down battery related process""" + rkey = "{}|{}".format(self.redis_key, factor) + IStateManager.get_store().publish(process_channel, rkey) + + + def set_drain_speed_factor(self, factor): + """Speed up/slow down UPS battery draining process + (note that this will produce 'unreal' behaviour) + """ + self._update_battery_process_speed(RedisChannels.battery_conf_drain_channel, factor) + + + def set_charge_speed_factor(self, factor): + """Speed up/slow down UPS battery charging + (note that this will produce 'unreal' behaviour) + """ + self._update_battery_process_speed(RedisChannels.battery_conf_charge_channel, factor) + + + def _reset_power_off_oid(self): + """Reset upsAdvControlUpsOff to 1 """ + # TODO different vendors may assign other values (not 1) + self._update_oid_by_name('PowerOff', snmp_data_types.Integer, 1) diff --git a/enginecore/enginecore/state/assets.py b/enginecore/enginecore/state/assets.py index 2b23025f..241eed77 100644 --- a/enginecore/enginecore/state/assets.py +++ b/enginecore/enginecore/state/assets.py @@ -24,8 +24,8 @@ from circuits import Component, handler import enginecore.state.state_managers as sm from enginecore.state.asset_definition import register_asset, SUPPORTED_ASSETS -from enginecore.state.agents import IPMIAgent, SNMPAgent -from enginecore.state.sensors import SensorRepository +from enginecore.state.agent import IPMIAgent, SNMPAgent, StorCLIEmulator +from enginecore.state.sensor.repository import SensorRepository PowerEventResult = namedtuple("PowerEventResult", "old_state new_state asset_key asset_type") PowerEventResult.__new__.__defaults__ = (None,) * len(PowerEventResult._fields) @@ -124,22 +124,26 @@ def _launch_temp_cooling(self): class Asset(Component): """Abstract Asset Class """ + def __init__(self, state): super(Asset, self).__init__() self._state = state self.state.reset_boot_time() self.state.update_load(0) + @property def key(self): """ Get ID assigned to the asset """ return self.state.key + @property def state(self): """State manager instance""" return self._state + def power_up(self): """Power up this asset Returns: @@ -153,6 +157,7 @@ def power_up(self): new_state=self.state.power_up() ) + def shut_down(self): """Shut down this asset Returns: @@ -166,6 +171,7 @@ def shut_down(self): new_state=self.state.shut_down() ) + def power_off(self): """Power down this asset Returns: @@ -179,6 +185,7 @@ def power_off(self): new_state=self.state.power_off() ) + def _update_load(self, load_change, arithmetic_op, msg=''): """React to load changes by updating asset load @@ -206,6 +213,7 @@ def _update_load(self, load_change, arithmetic_op, msg=''): asset_key=self.state.key ) + @handler("ChildAssetPowerUp", "ChildAssetLoadIncreased") def on_load_increase(self, event, *args, **kwargs): """Load is ramped up if child is powered up or child asset's load is increased @@ -217,6 +225,7 @@ def on_load_increase(self, event, *args, **kwargs): msg = 'Asset:[{}] - orig load {} was increased by "{}", new load will be set to "{}"' return self._update_load(increased_by, lambda old, change: old+change, msg) + @handler("ChildAssetPowerDown", "ChildAssetLoadDecreased") def on_load_decrease(self, event, *args, **kwargs): """Load is decreased if child is powered off or child asset's load is decreased @@ -236,19 +245,20 @@ def get_supported_assets(cls): @classmethod - def get_state_manager_by_key(cls, key, notify=True): + def get_state_manager_by_key(cls, key): """Get a state manager specific to the asset type Args: key(int): asset key Returns: StateManager: instance of the StateManager sub-class """ - return sm.StateManager.get_state_manager_by_key(key, cls.get_supported_assets(), notify) + return sm.StateManager.get_state_manager_by_key(key, cls.get_supported_assets()) class SNMPSim(): - + """Snmp simulator running snmpsim program""" + def __init__(self, key, host, port): self._snmp_agent = SNMPAgent(key, host, port) @@ -276,6 +286,7 @@ class PDU(Asset, SNMPSim): channel = "engine-pdu" StateManagerCls = sm.PDUStateManager + def __init__(self, asset_info): Asset.__init__(self, PDU.StateManagerCls(asset_info)) # Run snmpsim instance @@ -286,7 +297,7 @@ def __init__(self, asset_info): port=asset_info['port'] if 'port' in asset_info else 161 ) - self.state.agent = self._snmp_agent.pid + self.state.update_agent(self._snmp_agent.pid) agent_info = self.state.agent if not agent_info[1]: @@ -294,6 +305,7 @@ def __init__(self, asset_info): else: logging.info('Asset:[%s] - agent process (%s) is up & running', self.state.key, agent_info[0]) + ##### React to any events of the connected components ##### @handler("ParentAssetPowerDown") def on_parent_asset_power_down(self, event, *args, **kwargs): @@ -339,7 +351,7 @@ def __init__(self, asset_info): port=asset_info['port'] if 'port' in asset_info else 161 ) - self.state.agent = self._snmp_agent.pid + self.state.update_agent(self._snmp_agent.pid) # Store known { wattage: time_remaining } key/value pairs (runtime graph) self._runtime_details = json.loads(asset_info['runtime']) @@ -519,7 +531,8 @@ def on_parent_asset_power_down(self, event, *args, **kwargs): event.success = e_result.new_state != e_result.old_state return e_result - + + @handler("SignalDown") def on_signal_down_received(self, event, *args, **kwargs): """UPS can be powered down by snmp command""" @@ -534,6 +547,7 @@ def on_signal_down_received(self, event, *args, **kwargs): return e_result + @handler("ButtonPowerUpPressed") def on_ups_signal_up(self): if self._parent_up: @@ -563,24 +577,29 @@ def on_power_up_request_received(self, event, *args, **kwargs): def on_ambient_updated(self, event, *args, **kwargs): self._state.update_temperature(7) + @property def charge_speed_factor(self): """Estimated charge/sec will be multiplied by this value""" return self._charge_speed_factor + @charge_speed_factor.setter def charge_speed_factor(self, speed): self._charge_speed_factor = speed + @property def drain_speed_factor(self): """Estimated drain/sec will be multiplied by this value""" return self._drain_speed_factor + @drain_speed_factor.setter def drain_speed_factor(self, speed): self._drain_speed_factor = speed + def _update_load(self, load_change, arithmetic_op, msg=''): upd_result = super()._update_load(load_change, arithmetic_op, msg) # re-calculate time left based on updated load @@ -676,9 +695,6 @@ class Lamp(StaticAsset): """A simple demonstration type """ channel = "engine-lamp" - def __init__(self, asset_info): - super(Lamp, self).__init__(asset_info) - @register_asset class Server(StaticAsset): @@ -689,8 +705,8 @@ class Server(StaticAsset): def __init__(self, asset_info): super(Server, self).__init__(asset_info) self.state.power_up() - - + + @register_asset class ServerWithBMC(Server): """Asset controlling a VM with BMC/IPMI support """ @@ -708,10 +724,13 @@ def __init__(self, asset_info): sensors = self.StateManagerCls.get_sensor_definitions(asset_info['key']) self._sensor_repo = SensorRepository(asset_info['key'], enable_thermal=True) + # TODO: pass sensor repo to IPMIAgent instead of kv self._ipmi_agent = IPMIAgent(asset_info['key'], ipmi_dir, ipmi_config=asset_info, sensors=sensors) + self._storcli_emu = StorCLIEmulator(asset_info['key'], ipmi_dir, socket_port=asset_info['storcliPort']) super(ServerWithBMC, self).__init__(asset_info) + - self.state.agent = self._ipmi_agent.pid + self.state.update_agent(self._ipmi_agent.pid) agent_info = self.state.agent if not agent_info[1]: @@ -719,7 +738,7 @@ def __init__(self, asset_info): else: logging.info('Asset:[%s] - agent process (%s) is up & running', self.state.key, agent_info[0]) - self.state.cpu_load = 0 + self.state.update_cpu_load(0) self._cpu_load_t = None self._launch_monitor_cpu_load() @@ -738,7 +757,7 @@ def _launch_monitor_cpu_load(self): def _monitor_load(self): - """ """ + """Sample cpu load every 5 seconds """ cpu_time_1 = 0 sample_rate = 5 @@ -753,13 +772,13 @@ def _monitor_load(self): ns_to_sec = lambda x: x / 1e9 if cpu_time_1: - self.state.cpu_load = 100 * (ns_to_sec(cpu_time_2) - ns_to_sec(cpu_time_1)) / sample_rate + self.state.update_cpu_load(100 * abs(ns_to_sec(cpu_time_2) - ns_to_sec(cpu_time_1)) / sample_rate) logging.info("New CPU load (percentage): %s%% for server[%s]", self.state.cpu_load, self.state.key) cpu_time_1 = cpu_time_2 else: cpu_time_1 = 0 - self.state.cpu_load = 0 + self.state.update_cpu_load(0) time.sleep(sample_rate) @@ -775,11 +794,32 @@ def add_cpu_thermal_impact(self, target): self._sensor_repo.get_sensor_by_name(target).add_cpu_thermal_impact() + def add_storage_cv_thermal_impact(self, source, controller, cv, event): + """Add new sensor & cachevault thermal relationship + Args: + source(str): name of the source sensor causing thermal changes + cv(str): serial number of the cachevault + """ + sensor = self._sensor_repo.get_sensor_by_name(source) + sensor.add_cv_thermal_impact(controller, cv, event) + + + def add_storage_pd_thermal_impact(self, source, controller, drive, event): + """Add new sensor & physical drive thermal relationship + Args: + source(str): name of the source sensor causing thermal changes + drive(int): serial number of the cachevault + """ + sensor = self._sensor_repo.get_sensor_by_name(source) + sensor.add_pd_thermal_impact(controller, drive, event) + + @handler("AmbientDecreased", "AmbientIncreased") def on_ambient_updated(self, event, *args, **kwargs): """Update thermal sensor readings on ambient changes """ self._sensor_repo.adjust_thermal_sensors(new_ambient=kwargs['new_value'], old_ambient=kwargs['old_value']) - + self.state.update_storage_temperature(new_ambient=kwargs['new_value'], old_ambient=kwargs['old_value']) + @handler("ParentAssetPowerDown") def on_parent_asset_power_down(self, event, *args, **kwargs): @@ -814,24 +854,62 @@ def on_asset_did_power_on(self): class PSU(StaticAsset): """PSU """ - channel = "engine-psu" StateManagerCls = sm.PSUStateManager def __init__(self, asset_info): - if asset_info['variation'] == 'server': - PSU.StateManagerCls = sm.SimplePSUStateManager - self._var = asset_info['variation'] super(PSU, self).__init__(asset_info) + self._sensor_repo = SensorRepository(str(asset_info['key'])[:-1], enable_thermal=True) + self._psu_sensor_names = self._state.get_psu_sensor_names() + + + def _set_psu_status(self, value): + """Update psu status if sensor is supported""" + if 'psuStatus' in self._psu_sensor_names: + psu_status = self._sensor_repo.get_sensor_by_name(self._psu_sensor_names['psuStatus']) + print(psu_status) + psu_status.sensor_value = value @handler("ButtonPowerDownPressed") def on_asset_did_power_off(self): - if self._var != 'server': - self.state.set_psu_status(0x08) + """PSU status was set to failed""" + self._set_psu_status('0x08') + - @handler("ButtonPowerUpPressed") def on_asset_did_power_on(self): - if self._var != 'server': - self.state.set_psu_status(0x01) + """PSU was brought back up""" + self._set_psu_status('0x01') + + + def _update_load_sensors(self, load, arith_op): + """Update psu sensors associated with load + Args: + load: amperage change + arith_op(operator): operation on old & new load to be performed + """ + + if 'psuCurrent' in self._psu_sensor_names: + psu_current = self._sensor_repo.get_sensor_by_name(self._psu_sensor_names['psuCurrent']) + + psu_current.sensor_value = int(arith_op(self._state.load, load)) + + if 'psuPower' in self._psu_sensor_names: + psu_current = self._sensor_repo.get_sensor_by_name(self._psu_sensor_names['psuPower']) + + psu_current.sensor_value = int((arith_op(self._state.load, load)) * 10) + + + @handler("ChildAssetPowerUp", "ChildAssetLoadIncreased", priority=1) + def increase_load_sensors(self, event, *args, **kwargs): + """Load is ramped up if child is powered up or child asset's load is increased + """ + self._update_load_sensors(kwargs['child_load'], operator.add) + + + @handler("ChildAssetPowerDown", "ChildAssetLoadDecreased", priority=1) + def decrease_load_sensors(self, event, *args, **kwargs): + """Load is ramped up if child is powered up or child asset's load is increased + """ + self._update_load_sensors(kwargs['child_load'], operator.sub) diff --git a/enginecore/enginecore/state/events.py b/enginecore/enginecore/state/events.py index d804b9fe..c653af82 100644 --- a/enginecore/enginecore/state/events.py +++ b/enginecore/enginecore/state/events.py @@ -1,64 +1,81 @@ """Contains list of events dispatched by the main listener """ from circuits import Event + # Power Events ------- + class ButtonPowerDownPressed(Event): """On Asset Did Power Down (equivalent to power button press) """ + class ButtonPowerUpPressed(Event): """On Asset Did Power Up (equivalent to power button press) """ + class ParentAssetPowerDown(Event): """On Parent Did Go Down """ success = True + class ParentAssetPowerUp(Event): """On Parent Did Go Up """ success = True - + + class ChildAssetPowerDown(Event): """On Child Did Go Down """ success = True - + + class ChildAssetPowerUp(Event): """On Child Did Go Up """ success = True + class ChildAssetLoadIncreased(Event): """On Child Load Change""" success = True + class ChildAssetLoadDecreased(Event): """On Child Load Change""" success = True + class SignalDown(Event): """Asset Received power down request/command """ success = True + class SignalUp(Event): """Asset Received power Up request/command """ success = True + class SignalReboot(Event): """Asset Received reboot request/command """ success = True + class PowerOutage(Event): """On Power Outage""" pass + class PowerRestored(Event): """On power (mains source) restored""" pass + # Thermal Events ------- + class AmbientIncreased(Event): """Ambient went up""" pass + class AmbientDecreased(Event): """Ambient temperature dropped""" pass diff --git a/enginecore/enginecore/state/redis_channels.py b/enginecore/enginecore/state/redis_channels.py index 54ac4bd3..612ebd82 100644 --- a/enginecore/enginecore/state/redis_channels.py +++ b/enginecore/enginecore/state/redis_channels.py @@ -24,6 +24,8 @@ class RedisChannels(): ambient_update_channel = 'ambient-upd' sensor_conf_th_channel = 'sensor-th-upd' cpu_usg_conf_th_channel = 'cpu-th-upd' + str_drive_conf_th_channel = 'drive-th-upd' + str_cv_conf_th_channel = 'cv-th-upd' # misc oid_update_channel = 'oid-upd' diff --git a/enginecore/enginecore/state/sensor/file_locks.py b/enginecore/enginecore/state/sensor/file_locks.py new file mode 100644 index 00000000..61ee18c0 --- /dev/null +++ b/enginecore/enginecore/state/sensor/file_locks.py @@ -0,0 +1,19 @@ +"""Threa-safe locks for sensors & sensor repository""" +import threading + +class SensorFileLocks(): + """File locks for sensor files for safe access""" + + def __init__(self): + self._s_file_locks = {} + + def __str__(self): + return str(self._s_file_locks) + + def add_sensor_file_lock(self, sensor_name): + """Add new lock""" + self._s_file_locks[sensor_name] = threading.Lock() + + def get_lock(self, sensor_name): + """Get file lock by sensor name""" + return self._s_file_locks[sensor_name] diff --git a/enginecore/enginecore/state/sensor/repository.py b/enginecore/enginecore/state/sensor/repository.py new file mode 100644 index 00000000..365287c5 --- /dev/null +++ b/enginecore/enginecore/state/sensor/repository.py @@ -0,0 +1,120 @@ +"""Sensor repository provides a centralised access to the BMC/IPMI sensors +present in a particular asset/machine +""" +import os +import logging + +import enginecore.state.state_managers as sm +from enginecore.model.graph_reference import GraphReference + +from enginecore.state.sensor.file_locks import SensorFileLocks +from enginecore.state.sensor.sensor import Sensor + +class SensorRepository(): + """A sensor repository for a particular IPMI device""" + + def __init__(self, server_key, enable_thermal=False): + self._server_key = server_key + + self._graph_ref = GraphReference() + self._sensor_file_locks = SensorFileLocks() + + self._sensor_dir = os.path.join( + sm.StateManager.get_temp_workplace_dir(), + str(server_key), + 'sensor_dir' + ) + + self._sensors = {} + + with self._graph_ref.get_session() as session: + sensors = GraphReference.get_asset_sensors(session, server_key) + for sensor_info in sensors: + sensor = Sensor(self._sensor_dir, server_key, sensor_info, self._sensor_file_locks) + self._sensors[sensor.name] = sensor + + if enable_thermal: + self._load_thermal = True + + if not os.path.isdir(self._sensor_dir): + os.mkdir(self._sensor_dir) + + for s_name in self._sensors: + self._sensors[s_name].set_to_defaults() + + + def __str__(self): + + repo_str = [] + repo_str.append("Sensor Repository for Server {}".format(self._server_key)) + repo_str.append(" - files for sensor readings are located at '{}'".format(self._sensor_dir)) + + return '\n\n'.join(repo_str + list(map(lambda sn: str(self._sensors[sn]), self._sensors))) + + + def enable_thermal_impact(self): + """Set thermal event switch """ + list(map(lambda sn: self._sensors[sn].enable_thermal_impact(), self._sensors)) + + + def disable_thermal_impact(self): + """Clear thermal event switch""" + list(map(lambda sn: self._sensors[sn].disable_thermal_impact(), self._sensors)) + + + def shut_down_sensors(self): + """Set all sensors to offline""" + for s_name in self._sensors: + sensor = self._sensors[s_name] + if sensor.group != 'temperature': + sensor.set_to_off() + + self.disable_thermal_impact() + + + def power_up_sensors(self): + """Set all sensors to online""" + for s_name in self._sensors: + sensor = self._sensors[s_name] + if sensor.group != 'temperature': + sensor.set_to_defaults() + self.enable_thermal_impact() + + + def get_sensor_by_name(self, name): + """Get a specific sensor by name""" + return self._sensors[name] + + + def adjust_thermal_sensors(self, old_ambient, new_ambient): + """Indicate an ambient update""" + + for s_name in self._sensors: + sensor = self._sensors[s_name] + if sensor.group == 'temperature': + with self._sensor_file_locks.get_lock(sensor.name): + + old_sensor_value = int(sensor.sensor_value) + new_sensor_value = old_sensor_value - old_ambient + new_ambient if old_sensor_value else new_ambient + + logging.info( + "Sensor:[%s] - value will be updated from %s° to %s° due to ambient changes (%s° -> %s°)", + sensor.name, + old_sensor_value, + new_sensor_value, + old_ambient, + new_ambient + ) + + sensor.sensor_value = int(new_sensor_value) + + if self._load_thermal is True: + self._load_thermal = False + + for s_name in self._sensors: + self._sensors[s_name].start_thermal_impact() + + @property + def sensor_dir(self): + """Get temp IPMI state dir""" + return self._sensor_dir diff --git a/enginecore/enginecore/state/sensors.py b/enginecore/enginecore/state/sensor/sensor.py similarity index 69% rename from enginecore/enginecore/state/sensors.py rename to enginecore/enginecore/state/sensor/sensor.py index c08d2e4b..92f599b8 100644 --- a/enginecore/enginecore/state/sensors.py +++ b/enginecore/enginecore/state/sensor/sensor.py @@ -1,4 +1,6 @@ -"""Aggregates sensor management tools """ +"""Sensor provides access to ipmi/bmc sensors & manages thermal relationships between +sensors & sensors, sensors & storage components, cpu & sensors +""" import os import threading @@ -6,28 +8,15 @@ import time import json import operator -from random import randint +from enum import Enum import enginecore.state.state_managers as sm from enginecore.model.graph_reference import GraphReference - -class SensorFileLocks(): - """File locks for sensor files for safe access""" - - def __init__(self): - self._s_file_locks = {} - - def __str__(self): - return str(self._s_file_locks) - - def add_sensor_file_lock(self, sensor_name): - """Add new lock""" - self._s_file_locks[sensor_name] = threading.Lock() - - def get_lock(self, sensor_name): - """Get file lock by sensor name""" - return self._s_file_locks[sensor_name] +class HDComponents(Enum): + """Thermal storage target types""" + CacheVault = 1 + PhysicalDrive = 2 class Sensor(): @@ -44,9 +33,12 @@ def __init__(self, sensor_dir, server_key, s_details, s_locks): self._s_group = self._s_specs['group'] self._th_sensor_t = {} + self._th_storage_t = {} self._th_cpu_t = None self._th_sensor_t_name_fmt = "({event})s:[{source}]->t:[{target}]" + self._th_storage_t_name_fmt = "({event})s:[{source}]->STORAGE:t:[c{ctrl}/{target}]" + self._th_cpu_t_name_fmt = "s:[cpu_load]->t:[{target}]" self._graph_ref = GraphReference() @@ -62,7 +54,7 @@ def __init__(self, sensor_dir, server_key, s_details, s_locks): self._s_file_locks = s_locks self._s_thermal_event = threading.Event() - + def __str__(self): with self._graph_ref.get_session() as session: @@ -109,7 +101,7 @@ def _launch_thermal_sensor_thread(self, target, event): self._th_sensor_t[target] = {} self._th_sensor_t[target][event] = threading.Thread( - target=self._target_sensor_impact, + target=self._target_sensor, args=(target, event, ), name=self._th_sensor_t_name_fmt.format( source=self._s_name, target=target, event=event @@ -132,17 +124,58 @@ def _launch_thermal_cpu_thread(self): self._th_cpu_t.start() - def _init_thermal_impact(self): + def _launch_thermal_storage_thread(self, controller, hd_element, hd_type, event): + + thread_name = '{}-{}'.format(hd_type.name, hd_element) + if thread_name in self._th_storage_t and event in self._th_storage_t[thread_name]: + raise ValueError('Thread already exists') + if hd_element not in self._th_storage_t: + self._th_storage_t[hd_element] = {} + + self._th_storage_t[hd_element][event] = threading.Thread( + target=self._target_storage, + args=(controller, hd_element, hd_type, event,), + name=self._th_storage_t_name_fmt.format( + ctrl=controller, + source=self._s_name, + target=hd_element, + event=event + ) + ) + + self._th_storage_t[hd_element][event].daemon = True + self._th_storage_t[hd_element][event].start() + + + def _init_thermal_impact(self): """Initialize thermal imact based on the saved inter-connections""" with self._graph_ref.get_session() as session: - thermal_rel_details = GraphReference.get_affected_sensors(session, self._server_key, self._s_name) + thermal_sensor_rel_details = GraphReference.get_affected_sensors(session, self._server_key, self._s_name) # for each target & for each set of relationships with the target - for target in thermal_rel_details['targets']: + for target in thermal_sensor_rel_details['targets']: for rel in target['rel']: self._launch_thermal_sensor_thread(target['name'], rel['event']) + thermal_storage_rel_details = GraphReference.get_affected_hd_elements( + session, self._server_key, self._s_name + ) + + for target in thermal_storage_rel_details['targets']: + if 'DID' in target and target['DID']: + hd_type = HDComponents.PhysicalDrive + hd_element = target['DID'] + else: + hd_type = HDComponents.CacheVault + hd_element = target['serialNumber'] + + for rel in target['rel']: + self._launch_thermal_storage_thread( + target['controller']['controllerNum'], hd_element, hd_type, rel['event'] + ) + + self._launch_thermal_cpu_thread() @@ -159,7 +192,7 @@ def _calc_approx_value(self, model, current_value, inverse=False): def _cpu_impact(self): - """Keep updating this sensor based on cpu load changes + """Keep updating *this sensor based on cpu load changes This function waits for the thermal event switch and exits when the connection between this sensor & cpu load is removed; """ @@ -205,8 +238,75 @@ def _cpu_impact(self): time.sleep(5) + def _target_storage(self, controller, target, hd_type, event): + with self._graph_ref.get_session() as session: + while True: + + self._s_thermal_event.wait() + + # target + if hd_type == HDComponents.CacheVault: + target_attr = 'serialNumber' + target_value = '"{}"'.format(target) + elif hd_type == HDComponents.PhysicalDrive: + target_attr = 'DID' + target_value = target + else: + raise ValueError('Unknown hardware component!') + + rel_details = GraphReference.get_sensor_thermal_rel( + session, + self._server_key, + relationship={ + 'source': self._s_name, + 'target': { + "attribute": target_attr, + 'value': target_value + }, + 'event': event + } + ) + + if not rel_details: + del self._th_storage_t[target][event] + return + + rel = rel_details['rel'] + causes_heating = rel['action'] == 'increase' + source_sensor_status = operator.eq if rel['event'] == 'down' else operator.ne + + # if model is specified -> use the runtime mappings + if 'model' in rel and rel['model']: + rel['degrees'] = self._calc_approx_value( + json.loads(rel['model']), int(self.sensor_value)*10 + ) + + source_sensor_status = operator.ne + + if source_sensor_status(int(self.sensor_value), 0): + updated, new_temp = GraphReference.add_to_hd_component_temperature( + session, + target={ + 'server_key': self._server_key, + 'controller': controller, + "attribute": target_attr, + 'value': target_value, + 'hd_type': hd_type.name + }, + temp_change=rel['degrees'] * 1 if causes_heating else -1, + limit={ + 'lower': sm.StateManager.get_ambient(), + 'upper': rel['pauseAt'] if causes_heating else None + } + ) + + if updated: + logging.info('temperature sensor was updated to %s°', new_temp) - def _target_sensor_impact(self, target, event): + time.sleep(rel['rate']) + + + def _target_sensor(self, target, event): """Keep updating the target sensor based on the relationship between this sensor and the target; This function waits for the thermal event switch and exits when the connection between source & target is removed; @@ -218,10 +318,19 @@ def _target_sensor_impact(self, target, event): with self._graph_ref.get_session() as session: while True: - self._s_thermal_event.wait() + self._s_thermal_event.wait() rel_details = GraphReference.get_sensor_thermal_rel( - session, self._server_key, relationship={'source': self.name, 'target': target, 'event': event} + session, + self._server_key, + relationship={ + 'source': self.name, + 'target': { + 'attribute': "name", + 'value': target + }, + 'event': event + } ) # shut down thread upon relationship removal @@ -279,8 +388,6 @@ def _target_sensor_impact(self, target, event): sf_handler.truncate() sf_handler.write(str(new_sensor_value)) - - time.sleep(int(rel['rate'])) @@ -294,13 +401,21 @@ def _get_sensor_filename(self): return self.name + def add_cv_thermal_impact(self, controller, cv, event): + self._launch_thermal_storage_thread(controller, cv, HDComponents.CacheVault, event) + + + def add_pd_thermal_impact(self, controller, pd, event): + self._launch_thermal_storage_thread(controller, pd, HDComponents.PhysicalDrive, event) + + def add_sensor_thermal_impact(self, target, event): """Set a target sensor that will be affected by the current source sensor values Args: target(str): Name of the target sensor event(str): Source event causing the thermal impact to trigger """ - if target in self._th_sensor_t and event in self._th_sensor_t: + if target in self._th_sensor_t and event in self._th_sensor_t[target]: raise ValueError('Thread already exists') with self._graph_ref.get_session() as session: @@ -349,8 +464,8 @@ def start_thermal_impact(self): logging.info("Sensor:[%s] - initializing thermal processes", self._s_name) self._init_thermal_impact() self.enable_thermal_impact() - - + + def enable_thermal_impact(self): logging.info("Sensor:[%s] - enabling thermal impact", self._s_name) self._s_thermal_event.set() @@ -381,115 +496,3 @@ def set_to_defaults(self): off_value = self._s_specs['offValue'] if 'offValue' in self._s_specs else 0 filein.write(str(default_value if 'defaultValue' in self._s_specs else off_value)) - - - -class SensorRepository(): - """A sensor repository for a particular IPMI device""" - - def __init__(self, server_key, enable_thermal=False): - self._server_key = server_key - - self._graph_ref = GraphReference() - self._sensor_file_locks = SensorFileLocks() - - self._sensor_dir = os.path.join( - sm.StateManager.get_temp_workplace_dir(), - str(server_key), - 'sensor_dir' - ) - - self._sensors = {} - - with self._graph_ref.get_session() as session: - sensors = GraphReference.get_asset_sensors(session, server_key) - for sensor_info in sensors: - sensor = Sensor(self._sensor_dir, server_key, sensor_info, self._sensor_file_locks) - self._sensors[sensor.name] = sensor - - if enable_thermal: - self._load_thermal = True - - if not os.path.isdir(self._sensor_dir): - os.mkdir(self._sensor_dir) - - for s_name in self._sensors: - self._sensors[s_name].set_to_defaults() - - - def __str__(self): - - repo_str = [] - repo_str.append("Sensor Repository for Server {}".format(self._server_key)) - repo_str.append(" - files for sensor readings are located at '{}'".format(self._sensor_dir)) - - return '\n\n'.join(repo_str + list(map(lambda sn: str(self._sensors[sn]), self._sensors))) - - - def enable_thermal_impact(self): - """Set thermal event switch """ - list(map(lambda sn: self._sensors[sn].enable_thermal_impact(), self._sensors)) - - - def disable_thermal_impact(self): - """Clear thermal event switch""" - list(map(lambda sn: self._sensors[sn].disable_thermal_impact(), self._sensors)) - - - def shut_down_sensors(self): - """Set all sensors to offline""" - for s_name in self._sensors: - sensor = self._sensors[s_name] - if sensor.group != 'temperature': - sensor.set_to_off() - - self.disable_thermal_impact() - - - def power_up_sensors(self): - """Set all sensors to online""" - for s_name in self._sensors: - sensor = self._sensors[s_name] - if sensor.group != 'temperature': - sensor.set_to_defaults() - self.enable_thermal_impact() - - - def get_sensor_by_name(self, name): - """Get a specific sensor by name""" - return self._sensors[name] - - - def adjust_thermal_sensors(self, old_ambient, new_ambient): - """Indicate an ambient update""" - - for s_name in self._sensors: - sensor = self._sensors[s_name] - if sensor.group == 'temperature': - with self._sensor_file_locks.get_lock(sensor.name): - - old_sensor_value = int(sensor.sensor_value) - new_sensor_value = old_sensor_value - old_ambient + new_ambient if old_sensor_value else new_ambient - - logging.info( - "Sensor:[%s] - value will be updated from %s° to %s° due to ambient changes (%s° -> %s°)", - sensor.name, - old_sensor_value, - new_sensor_value, - old_ambient, - new_ambient - ) - - sensor.sensor_value = int(new_sensor_value) - - if self._load_thermal is True: - self._load_thermal = False - - for s_name in self._sensors: - self._sensors[s_name].start_thermal_impact() - - @property - def sensor_dir(self): - """Get temp IPMI state dir""" - return self._sensor_dir - \ No newline at end of file diff --git a/enginecore/enginecore/state/state_listener.py b/enginecore/enginecore/state/state_listener.py index 1dfbbefd..e1e23aff 100644 --- a/enginecore/enginecore/state/state_listener.py +++ b/enginecore/enginecore/state/state_listener.py @@ -74,7 +74,7 @@ def __init__(self, debug=False, force_snmp_init=False): ### Register Assets ### self._subscribe_to_channels() self._reload_model(force_snmp_init) - + def _subscribe_to_channels(self): """Subscribe to redis channels""" @@ -98,9 +98,11 @@ def _subscribe_to_channels(self): # Thermal Channels self._thermal_pubsub.psubscribe( - RedisChannels.ambient_update_channel, # on ambient changes - RedisChannels.sensor_conf_th_channel, # new relationship - RedisChannels.cpu_usg_conf_th_channel, # new cpu-usage relationship + RedisChannels.ambient_update_channel, # on ambient changes + RedisChannels.sensor_conf_th_channel, # new sensor->sensor relationship + RedisChannels.cpu_usg_conf_th_channel, # new cpu_usage->sensor relationship + RedisChannels.str_cv_conf_th_channel, # new sensor->cache_vault relationship + RedisChannels.str_drive_conf_th_channel # new sensor->phys_drive relationship ) @@ -133,11 +135,13 @@ def _reload_model(self, force_snmp_init=True): # initialize load by dispatching load update for key in leaf_nodes: + asset_key = int(key) new_load = self._assets[key].state.power_usage + # notify parents of load changes - self._chain_load_update( + self._chain_load_update( LoadEventResult( load_change=new_load, new_load=new_load, @@ -179,7 +183,7 @@ def _handle_oid_update(self, asset_key, oid, value): logging.info('oid changed:') logging.info(">" + oid + ": " + oid_value) - + def _handle_ambient_update(self, new_temp, old_temp): """React to ambient update by notifying all the assets in the sys topology @@ -192,7 +196,6 @@ def _handle_ambient_update(self, new_temp, old_temp): for a_key in self._assets: self.fire(PowerEventManager.map_ambient_event(old_temp, new_temp), self._assets[a_key]) - def _handle_state_update(self, asset_key): """React to asset state updates in redis store @@ -244,7 +247,7 @@ def _chain_load_update(self, event_result, increased=True): parent_load_change = load_change * parent.state.draw_percentage # logging.info( - # "child [%s] load update: %s; updating %s load for [%s]", + # "child [%s] load update: %s; updating %s load for [%s]", # child_key, load_change, parent.state.load, parent.key # ) @@ -353,7 +356,7 @@ def _chain_power_update(self, event_result): # logging.info('Child load : {}'.format(node_load)) if int(new_state) == 0: - alt_branch_event = PowerEventManager.map_load_increased_by(node_load, child_asset.key) + alt_branch_event = PowerEventManager.map_load_increased_by(node_load, child_asset.key) else: alt_branch_event = PowerEventManager.map_load_decreased_by(node_load, child_asset.key) @@ -409,7 +412,7 @@ def monitor_battery(self): _, speed = data.split('|') self._assets[int(asset_key)].drain_speed_factor = float(speed) - + # TODO: this error is ttoo generic (doesn't apply to all the monitoring functions) except KeyError as error: logging.error("Detected unregistered asset under key [%s]", error) @@ -498,6 +501,13 @@ def monitor_thermal(self): elif channel == RedisChannels.cpu_usg_conf_th_channel: new_rel = json.loads(data) self._assets[new_rel['key']].add_cpu_thermal_impact(**new_rel['relationship']) + elif channel == RedisChannels.str_cv_conf_th_channel: + new_rel = json.loads(data) + self._assets[new_rel['key']].add_storage_cv_thermal_impact(**new_rel['relationship']) + elif channel == RedisChannels.str_drive_conf_th_channel: + new_rel = json.loads(data) + self._assets[new_rel['key']].add_storage_pd_thermal_impact(**new_rel['relationship']) + except KeyError as error: logging.error("Detected unregistered asset under key [%s]", error) @@ -531,15 +541,15 @@ def ChildAssetPowerDown_success(self, evt, event_result): self._load_success(event_result, increased=False) def ChildAssetPowerUp_success(self, evt, event_result): - """When child is powered up -> get the new load value of child asset""" + """When child is powered up -> get the new load value of child asset""" self._load_success(event_result, increased=True) def ChildAssetLoadDecreased_success(self, evt, event_result): - """When load decreases down the power stream """ + """When load decreases down the power stream """ self._load_success(event_result, increased=False) def ChildAssetLoadIncreased_success(self, evt, event_result): - """When load increases down the power stream """ + """When load increases down the power stream """ self._load_success(event_result, increased=True) @@ -578,7 +588,7 @@ def SignalReboot_success(self, evt, e_result): self._chain_power_update(e_result) self._notify_client(ClientRequests.asset, { - 'key': e_result.asset_key, + 'key': e_result.asset_key, 'status': e_result.new_state }) diff --git a/enginecore/enginecore/state/state_managers.py b/enginecore/enginecore/state/state_managers.py index 02d1b1af..9c67b90e 100644 --- a/enginecore/enginecore/state/state_managers.py +++ b/enginecore/enginecore/state/state_managers.py @@ -6,452 +6,53 @@ a Server State Manager will contain server-specific logic for powering up/down a VM """ -import time -import os -import tempfile -import json from enum import Enum -import redis -import libvirt import pysnmp.proto.rfc1902 as snmp_data_types from enginecore.model.graph_reference import GraphReference -import enginecore.model.system_modeler as sys_modeler -from enginecore.state.utils import format_as_redis_key from enginecore.state.redis_channels import RedisChannels +import enginecore.state.api as state_api -class StateManager(): - """Base class for all the state managers """ - redis_store = None +class StateManager(state_api.IStateManager): - def __init__(self, asset_info, notify=False): - self._graph_ref = GraphReference() - self._asset_key = asset_info['key'] - self._asset_info = asset_info - self._notify = notify - - @property - def key(self): - """Asset Key """ - return self._asset_key - - @property - def redis_key(self): - """Asset key in redis format as '{key}-{type}' """ - return "{}-{}".format(str(self.key), self.asset_type) - - @property - def asset_type(self): - """Asset Type """ - return self._asset_info['type'] - - @property - def power_usage(self): - """Normal power usage in AMPS when powered up""" - return 0 - - @property - def draw_percentage(self): - """How much power the asset draws""" - return self._asset_info['draw'] if 'draw' in self._asset_info else 1 - - @property - def load(self): - """Get current load stored in redis (in AMPs)""" - return float(StateManager.get_store().get(self.redis_key + ":load")) - - @property - def wattage(self): - return self.load * 120 - - @property - def status(self): - """Operational State - - Returns: - int: 1 if on, 0 if off - """ - return int(StateManager.get_store().get(self.redis_key)) - - @property - def agent(self): - """Agent instance details (if supported) - - Returns: - tuple: process id and status of the process (if it's running) - """ - pid = StateManager.get_store().get(self.redis_key + ":agent") - return (int(pid), os.path.exists("/proc/" + pid.decode("utf-8"))) if pid else None - - - @agent.setter - def agent(self, pid): + def update_agent(self, pid): + """Set agent PID""" StateManager.get_store().set(self.redis_key + ":agent", pid) - def shut_down(self): - """Implements state logic for graceful power-off event, sleeps for the pre-configured time - - Returns: - int: Asset's status after power-off operation - """ - print('Graceful shutdown') - self._sleep_shutdown() - if self.status: - self._set_state_off() - return self.status - - - def power_off(self): - """Implements state logic for abrupt power loss - - Returns: - int: Asset's status after power-off operation - """ - print("Powering down {}".format(self._asset_key)) - if self.status: - self._set_state_off() - return self.status - - def power_up(self): - """Implements state logic for power up, sleeps for the pre-configured time & resets boot time - - Returns: - int: Asset's status after power-on operation - """ - print("Powering up {}".format(self._asset_key)) - if self._parents_available() and not self.status: - self._sleep_powerup() - # udpate machine start time & turn on - self.reset_boot_time() - self._set_state_on() - return self.status - - def update_load(self, load): """Update load """ - load = load if load >= 0 else 0 - StateManager.get_store().set(self.redis_key + ":load", load) - self._publish_load() + super()._update_load(load) def reset_boot_time(self): """Reset the boot time to now""" - StateManager.get_store().set(str(self._asset_key) + ":start_time", int(time.time())) - - - def get_config_off_delay(self): - return NotImplementedError - - - def get_config_on_delay(self): - return NotImplementedError - - - def _sleep_shutdown(self): - if 'offDelay' in self._asset_info: - time.sleep(self._asset_info['offDelay'] / 1000.0) # ms to sec - - - def _sleep_powerup(self): - if 'onDelay' in self._asset_info: - time.sleep(self._asset_info['onDelay'] / 1000.0) # ms to sec - - - def _set_state_on(self): - StateManager.get_store().set(self.redis_key, '1') - if self._notify: - self.publish_power() - - - def _set_state_off(self): - StateManager.get_store().set(self.redis_key, '0') - if self._notify: - self.publish_power() + super()._reset_boot_time() def publish_power(self): - """ publish state changes """ - StateManager.get_store().publish(RedisChannels.state_update_channel, self.redis_key) - - def _publish_load(self): - """ publish load changes """ - StateManager.get_store().publish(RedisChannels.load_update_channel, self.redis_key) - + """Publish state changes (expose method to the assets) """ + super()._publish_power() - def _update_oid_value(self, oid, data_type, oid_value): - """Update oid with a new value - - Args: - oid(str): SNMP object id - data_type(int): Data type in redis format - oid_value(object): OID value in rfc1902 format - """ - redis_store = StateManager.get_store() - - rvalue = "{}|{}".format(data_type, oid_value) - rkey = format_as_redis_key(str(self._asset_key), oid, key_formatted=False) - redis_store.set(rkey, rvalue) - + def _set_redis_asset_state(self, state): + """Update redis value of the asset power status""" + super()._set_redis_asset_state(state, publish=False) - def _get_oid_value(self, oid, key): - redis_store = StateManager.get_store() - rkey = format_as_redis_key(str(key), oid, key_formatted=False) - return redis_store.get(rkey).decode().split('|')[1] - def _check_parents(self, keys, parent_down, msg='Cannot perform the action: [{}] parent is off'): - """Check that redis values pass certain condition - - Args: - keys (list): Redis keys (formatted as required) - parent_down (callable): lambda clause - msg (str, optional): Error message to be printed - - Returns: - bool: True if parent keys are missing or all parents were verified with parent_down clause - """ - if not keys: - return True - - parent_values = StateManager.get_store().mget(keys) - pdown = 0 - pdown_msg = '' - for rkey, rvalue in zip(keys, parent_values): - if parent_down(rvalue, rkey): - pdown_msg += msg.format(rkey) + '\n' - pdown += 1 - - if pdown == len(keys): - print(pdown_msg) - return False - else: - return True - - - def _parents_available(self): - """Indicates whether a state action can be performed; - checks if parent nodes are up & running and all OIDs indicate 'on' status - - Returns: - bool: True if parents are available - """ - - not_affected_by_mains = True - asset_keys, oid_keys = GraphReference.get_parent_keys(self._graph_ref.get_session(), self._asset_key) - - if not asset_keys and not StateManager.mains_status(): - not_affected_by_mains = False - - assets_up = self._check_parents(asset_keys, lambda rvalue, _: rvalue == b'0') - oid_clause = lambda rvalue, rkey: rvalue.split(b'|')[1].decode() == oid_keys[rkey]['switchOff'] - oids_on = self._check_parents(oid_keys.keys(), oid_clause) - - return assets_up and oids_on and not_affected_by_mains - - - @classmethod - def get_temp_workplace_dir(cls): - """Get location of the temp directory""" - sys_temp = tempfile.gettempdir() - simengine_temp = os.path.join(sys_temp, 'simengine') - return simengine_temp - - - @classmethod - def get_store(cls): - """Get redis db handler """ - if not cls.redis_store: - cls.redis_store = redis.StrictRedis(host='localhost', port=6379) - - return cls.redis_store - - - @classmethod - def _get_assets_states(cls, assets, flatten=True): - """Query redis store and find states for each asset - - Args: - flatten(bool): If false, the returned assets in the dict will have their child-components nested - . - Returns: - dict: Current information on assets including their states, load etc. - """ - asset_keys = assets.keys() - - if not asset_keys: - return None - - asset_values = cls.get_store().mget( - list(map(lambda k: "{}-{}".format(k, assets[k]['type']), asset_keys)) - ) - - for rkey, rvalue in zip(assets, asset_values): - asset_state = StateManager(assets[rkey], assets[rkey]['type']) if assets[rkey]['type'] != 'ups' else UPSStateManager(assets[rkey]) - assets[rkey]['status'] = int(rvalue) - assets[rkey]['load'] = asset_state.load - if assets[rkey]['type'] == 'ups': - assets[rkey]['battery'] = asset_state.battery_level - - if not flatten and 'children' in assets[rkey]: - # call recursively on children - assets[rkey]['children'] = cls._get_assets_states(assets[rkey]['children']) - - return assets - - - @classmethod - def get_system_status(cls, flatten=True): - """Get states of all system components - - Args: - flatten(bool): If false, the returned assets in the dict will have their child-components nested - - Returns: - dict: Current information on assets including their states, load etc. - """ - graph_ref = GraphReference() - with graph_ref.get_session() as session: - - # cache assets - assets = GraphReference.get_assets_and_connections(session, flatten) - assets = cls._get_assets_states(assets, flatten) - return assets - - - @classmethod - def reload_model(cls): - """Request daemon reloading""" - StateManager.get_store().publish(RedisChannels.model_update_channel, 'reload') - - - @classmethod - def power_outage(cls): - """Simulate complete power outage/restoration""" - StateManager.get_store().set('mains-source', '0') - StateManager.get_store().publish(RedisChannels.mains_update_channel, '0') - - - @classmethod - def power_restore(cls): - """Simulate complete power restoration""" - StateManager.get_store().set('mains-source', '1') - StateManager.get_store().publish(RedisChannels.mains_update_channel, '1') - - - @classmethod - def mains_status(cls): - """Get wall power status""" - return int(StateManager.get_store().get('mains-source').decode()) - - - @classmethod - def get_ambient(cls): - """Retrieve current ambient value""" - temp = StateManager.get_store().get('ambient') - return int(temp.decode()) if temp else 0 - - - @classmethod - def set_ambient(cls, value): - """Update ambient value""" - old_temp = cls.get_ambient() - StateManager.get_store().set('ambient', str(value)) - StateManager.get_store().publish(RedisChannels.ambient_update_channel, '{}-{}'.format(old_temp, value)) - - - @classmethod - def get_ambient_props(cls): - graph_ref = GraphReference() - with graph_ref.get_session() as session: - props = GraphReference.get_ambient_props(session) - return props - - - @classmethod - def set_ambient_props(cls, props): - """Update runtime thermal properties of the room temperature""" - - graph_ref = GraphReference() - with graph_ref.get_session() as session: - GraphReference.set_ambient_props(session, props) - - - @classmethod - def get_state_manager_by_key(cls, key, supported_assets, notify=True): - """Infer asset manager from key""" - - graph_ref = GraphReference() - - with graph_ref.get_session() as session: - asset_info = GraphReference.get_asset_and_components(session, key) - return supported_assets[asset_info['type']].StateManagerCls(asset_info, notify=notify) - - -class UPSStateManager(StateManager): +class UPSStateManager(state_api.IUPSStateManager, StateManager): """Handles UPS state logic """ - class OutputStatus(Enum): - """UPS output status """ - onLine = 1 - onBattery = 2 - off = 3 - - class InputLineFailCause(Enum): - """Reason for the occurrence of the last transfer to UPS """ - noTransfer = 1 - blackout = 2 - deepMomentarySag = 3 - - def __init__(self, asset_info, notify=False): - super(UPSStateManager, self).__init__(asset_info, notify) - self._max_battery_level = 1000#% - self._min_restore_charge_level = self._asset_info['minPowerOnBatteryLevel'] - self._full_recharge_time = self._asset_info['fullRechargeTime'] - - - @property - def battery_level(self): - """Get current level (high-precision)""" - return int(StateManager.get_store().get(self.redis_key + ":battery").decode()) - - - @property - def battery_max_level(self): - """Max battery level""" - return self._max_battery_level - - @property - def wattage(self): - return (self.load + self.idle_ups_amp) * self._asset_info['powerSource'] - - @property - def idle_ups_amp(self): - """How much a UPS draws""" - return self._asset_info['powerConsumption'] / self._asset_info['powerSource'] - - @property - def min_restore_charge_level(self): - """min level of battery charge before UPS can be powered on""" - return self._min_restore_charge_level - - @property - def full_recharge_time(self): - """hours taken to recharge the battery when it's completely depleted""" - return self._full_recharge_time - - @property - def output_capacity(self): - """UPS rated capacity""" - return self._asset_info['outputPowerCapacity'] - def update_temperature(self, temp): - self._update_battery_temp_oid(temp + StateManager.get_ambient()) + """Set battery temperature of the device""" + oid_value = (temp + StateManager.get_ambient()) * 10 + self._update_oid_by_name('HighPrecBatteryTemperature', snmp_data_types.Gauge32, oid_value) + def update_battery(self, charge_level): """Updates battery level, checks for the charge level being in valid range, sets battery-related OIDs @@ -478,7 +79,7 @@ def update_load(self, load): if 'outputPowerCapacity' in self._asset_info: self._update_load_perc_oids(load) self._update_current_oids(load) - + def update_time_on_battery(self, timeticks): """Update OIDs associated with UPS time on battery @@ -486,54 +87,35 @@ def update_time_on_battery(self, timeticks): Args: timeticks(int): time-on battery (seconds*100) """ - with self._graph_ref.get_session() as db_s: - oid, data_type, _ = GraphReference.get_asset_oid_by_name( - db_s, int(self._asset_key), 'TimeOnBattery' - ) + self._update_oid_by_name('TimeOnBattery', snmp_data_types.TimeTicks, timeticks) - if oid: - self._update_oid_value(oid, data_type, snmp_data_types.TimeTicks(timeticks)) - def update_time_left(self, timeticks): - """Update OIDs associated with UPS runtime (estimation) + """Update OIDs associated with UPS runtime (estimation of how long UPS will be operating) Args: - load(float): new load in AMPs + timeticks(int): time left """ - with self._graph_ref.get_session() as db_s: - oid, data_type, _ = GraphReference.get_asset_oid_by_name( - db_s, int(self._asset_key), 'BatteryRunTimeRemaining' - ) + self._update_oid_by_name('BatteryRunTimeRemaining', snmp_data_types.TimeTicks, timeticks) - if oid: - self._update_oid_value(oid, data_type, snmp_data_types.TimeTicks(timeticks)) def update_ups_output_status(self, status): - with self._graph_ref.get_session() as db_s: - oid, data_type, oid_spec = GraphReference.get_asset_oid_by_name( - db_s, int(self._asset_key), 'BasicOutputStatus' - ) - - if oid: - self._update_oid_value(oid, data_type, snmp_data_types.Integer(oid_spec[status.name])) + """Status for output -- either on, off or running on battery + Args: + status(OutputStatus): new output status + """ + self._update_oid_by_name('BasicOutputStatus', snmp_data_types.Integer, status.name, use_spec=True) + def update_transfer_reason(self, status): - with self._graph_ref.get_session() as db_s: - oid, data_type, oid_spec = GraphReference.get_asset_oid_by_name( - db_s, int(self._asset_key), 'InputLineFailCause' - ) - - if oid: - self._update_oid_value(oid, data_type, snmp_data_types.Integer(oid_spec[status.name])) - - def _reset_power_off_oid(self): - """Reset upsAdvControlUpsOff to 1 """ - with self._graph_ref.get_session() as session: - oid, data_type, _ = GraphReference.get_asset_oid_by_name(session, int(self._asset_key), 'PowerOff') - if oid: - self._update_oid_value(oid, data_type, 1) # TODO: Can be something else - + """Update UPS transfer reason; UPS can switch its mode to 'on battery' for multiple reasons + (e.g. Voltage drop, upstream power failure etc.) + Args: + status(InputLineFailCause): new transfer cause + """ + self._update_oid_by_name('InputLineFailCause', snmp_data_types.Integer, status.name, use_spec=True) + + def _update_current_oids(self, load): """Update OIDs associated with UPS Output - Current in AMPs @@ -555,6 +137,7 @@ def _update_current_oids(self, load): self._update_oid_value(oid_hp, dt_hp, snmp_data_types.Gauge32(load*10)) + # TODO: refactor both _update_load_perc_oids & _update_battery_oids, functions seem quite similair def _update_load_perc_oids(self, load): """Update OIDs associated with UPS Output - % of the power capacity @@ -562,7 +145,6 @@ def _update_load_perc_oids(self, load): load(float): new load in AMPs """ - power_capacity = self.output_capacity with self._graph_ref.get_session() as db_s: # 100% oid_adv, dt_adv, _ = GraphReference.get_asset_oid_by_name(db_s, int(self._asset_key), 'AdvOutputLoad') @@ -572,20 +154,14 @@ def _update_load_perc_oids(self, load): db_s, int(self._asset_key), 'HighPrecOutputLoad' ) - value_hp = (1000*(load*120)) / power_capacity + value_hp = (1000*(load*120)) / self.output_capacity if oid_adv: self._update_oid_value(oid_adv, dt_adv, snmp_data_types.Gauge32(value_hp/10)) if oid_hp: self._update_oid_value(oid_hp, dt_hp, snmp_data_types.Gauge32(value_hp)) - - def _update_battery_temp_oid(self, temp): - with self._graph_ref.get_session() as db_s: - oid_hp, oid_dt, _ = GraphReference.get_asset_oid_by_name( - db_s, int(self._asset_key), 'HighPrecBatteryTemperature' - ) - self._update_oid_value(oid_hp, oid_dt, snmp_data_types.Gauge32(temp*10)) + def _update_battery_oids(self, charge_level, old_level): """Update OIDs associated with UPS Battery @@ -619,63 +195,15 @@ def _update_battery_oids(self, charge_level, old_level): self._update_oid_value(oid_basic, dt_basic, snmp_data_types.Integer32(norm_bat_value)) - def shut_down(self): - time.sleep(self.get_config_off_delay()) - powered = super().shut_down() - return powered - - def power_up(self): - print("Powering up {}".format(self._asset_key)) - - if self.battery_level and not self.status: - self._sleep_powerup() - time.sleep(self.get_config_on_delay()) - # udpate machine start time & turn on - self.reset_boot_time() - self._set_state_on() - - powered = self.status - if powered: - self._reset_power_off_oid() - - return powered - - - def get_config_off_delay(self): - with self._graph_ref.get_session() as db_s: - oid, _, _ = GraphReference.get_asset_oid_by_name(db_s, int(self._asset_key), 'AdvConfigShutoffDelay') - return int(self._get_oid_value(oid, key=self._asset_key)) - - - def get_config_on_delay(self): - with self._graph_ref.get_session() as db_s: - oid, _, _ = GraphReference.get_asset_oid_by_name(db_s, int(self._asset_key), 'AdvConfigReturnDelay') - return int(self._get_oid_value(oid, key=self._asset_key)) - - def _publish_battery(self): """Publish battery update""" StateManager.get_store().publish(RedisChannels.battery_update_channel, self.redis_key) - def set_drain_speed_factor(self, factor): - """Publish battery update""" - rkey = "{}|{}".format(self.redis_key, factor) - StateManager.get_store().publish(RedisChannels.battery_conf_drain_channel, rkey) - - - def set_charge_speed_factor(self, factor): - """Publish battery update""" - rkey = "{}|{}".format(self.redis_key, factor) - StateManager.get_store().publish(RedisChannels.battery_conf_charge_channel, rkey) -class PDUStateManager(StateManager): +class PDUStateManager(state_api.IPDUStateManager, StateManager): """Handles state logic for PDU asset """ - - def __init__(self, asset_info, notify=False): - super(PDUStateManager, self).__init__(asset_info, notify) - - + def _update_current(self, load): """Update OID associated with the current amp value """ with self._graph_ref.get_session() as session: @@ -706,7 +234,7 @@ def update_load(self, load): -class OutletStateManager(StateManager): +class OutletStateManager(state_api.IOutletStateManager, StateManager): """Handles state logic for outlet asset """ class OutletState(Enum): @@ -714,8 +242,6 @@ class OutletState(Enum): switchOff = 1 switchOn = 2 - def __init__(self, asset_info, notify=False): - super(OutletStateManager, self).__init__(asset_info, notify) def _get_oid_value_by_name(self, oid_name): """Get value under object id name""" @@ -726,6 +252,7 @@ def _get_oid_value_by_name(self, oid_name): return int(self._get_oid_value(oid, key=parent_key)) return 0 + def set_parent_oid_states(self, state): """Bulk-set parent oid values @@ -744,6 +271,8 @@ def set_parent_oid_states(self, state): StateManager.get_store().mset(parents_new_states) + + # TODO: move to interface def get_config_off_delay(self): return self._get_oid_value_by_name("OutletConfigPowerOffTime") @@ -752,189 +281,70 @@ def get_config_on_delay(self): return self._get_oid_value_by_name("OutletConfigPowerOnTime") -class StaticDeviceStateManager(StateManager): - """Dummy Device that doesn't do much except drawing power """ - - def __init__(self, asset_info, notify=False): - super(StaticDeviceStateManager, self).__init__(asset_info, notify) - - - @property - def power_usage(self): - return self._asset_info['powerConsumption'] / self._asset_info['powerSource'] - - def power_up(self): - powered = super().power_up() - if powered: - self.update_load(self.power_usage) - return powered +class StaticDeviceStateManager(state_api.IStaticDeviceManager, StateManager): + """Dummy Device that doesn't do much except drawing power """ + pass -class ServerStateManager(StaticDeviceStateManager): +class ServerStateManager(state_api.IServerStateManager, StaticDeviceStateManager): """Server state manager offers control over VM's state """ - def __init__(self, asset_info, notify=False): - super(ServerStateManager, self).__init__(asset_info, notify) - self._vm_conn = libvirt.open("qemu:///system") - # TODO: error handling if the domain is missing (throws libvirtError) & close the connection - self._vm = self._vm_conn.lookupByName(asset_info['domainName']) - - - def vm_is_active(self): - return self._vm.isActive() - - def shut_down(self): - if self._vm.isActive(): - self._vm.destroy() - self.update_load(0) - return super().shut_down() - - - def power_off(self): - if self._vm.isActive(): - self._vm.destroy() - self.update_load(0) - return super().power_off() - - - def power_up(self): - powered = super().power_up() - if not self._vm.isActive() and powered: - self._vm.create() - self.update_load(self.power_usage) - return powered - - -class BMCServerStateManager(ServerStateManager): +class BMCServerStateManager(state_api.IBMCServerStateManager, ServerStateManager): """Manage Server with BMC """ - def __init__(self, asset_info, notify=False): - ServerStateManager.__init__(self, asset_info, notify) - - def power_up(self): - powered = super().power_up() - return powered - - - def shut_down(self): - return super().shut_down() - - - def power_off(self): - return super().power_off() - - - def get_cpu_stats(self): - """Get VM cpu stats (user_time, cpu_time etc. (see libvirt api)) """ - return self._vm.getCPUStats(True) - - - @property - def cpu_load(self): - """Get latest recorded CPU load in percentage""" - cpu_load = StateManager.get_store().get(self.redis_key + ":cpu_load") - return int(cpu_load.decode()) if cpu_load else 0 - - - @cpu_load.setter - def cpu_load(self, value): + def update_cpu_load(self, value): + """Set CPU load""" StateManager.get_store().set(self.redis_key + ":cpu_load", str(int(value))) - @classmethod - def get_sensor_definitions(cls, asset_key): - """Get sensor definitions """ - graph_ref = GraphReference() - with graph_ref.get_session() as session: - return GraphReference.get_asset_sensors(session, asset_key) - - @classmethod - def update_thermal_sensor_target(cls, attr): - """Create new or update existing thermal relationship between 2 sensors""" - new_rel = sys_modeler.set_thermal_sensor_target(attr) - if new_rel: - StateManager.get_store().publish( - RedisChannels.sensor_conf_th_channel, - json.dumps({ - 'key': attr['asset_key'], - 'relationship': { - 'source': attr['source_sensor'], - 'target': attr['target_sensor'], - 'event': attr['event'] - } - }) - ) - - @classmethod - def update_thermal_cpu_target(cls, attr): - """Create new or update existing thermal relationship between CPU usage and sensor""" - new_rel = sys_modeler.set_thermal_cpu_target(attr) - if new_rel: - StateManager.get_store().publish( - RedisChannels.cpu_usg_conf_th_channel, - json.dumps({ - 'key': attr['asset_key'], - 'relationship': { - 'target': attr['target_sensor'], - } - }) - ) + def update_storage_temperature(self, old_ambient, new_ambient): - @classmethod - def get_thermal_cpu_details(cls, asset_key): - """Query existing cpu->sensor relationship""" - graph_ref = GraphReference() - with graph_ref.get_session() as session: - return GraphReference.get_thermal_cpu_details(session, asset_key) - - -class SimplePSUStateManager(StateManager): - def __init__(self, asset_info, notify=False): - StateManager.__init__(self, asset_info, notify) + with self._graph_ref.get_session() as db_s: + hd_elements = GraphReference.get_all_hd_thermal_elements(db_s, self.key) + + for hd_e in hd_elements: + + if 'DID' in hd_e['component']: + target_attr = 'DID' + target_value = hd_e['component']['DID'] + target_type = 'PhysicalDrive' + else: + target_attr = 'serialNumber' + target_value = '"{}"'.format(hd_e['component']['serialNumber']) + target_type = 'CacheVault' + + updated, new_temp = GraphReference.add_to_hd_component_temperature( + db_s, + target={ + 'server_key': self.key, + 'controller': hd_e['controller']['controllerNum'], + "attribute": target_attr, + 'value': target_value, + 'hd_type': target_type + }, + temp_change=new_ambient - old_ambient, + limit={ + 'lower': new_ambient, + 'upper': None + } + ) class PSUStateManager(StateManager): + """Power Supply""" - - def __init__(self, asset_info, notify=False): - StateManager.__init__(self, asset_info, notify) + def __init__(self, asset_info): + StateManager.__init__(self, asset_info) self._psu_number = int(repr(asset_info['key'])[-1]) # self._sensor = SensorRepository(int(repr(asset_info['key'])[:-1])).get - - def _update_current(self, load): - """Update current inside state file """ - load = load if load >= 0 else 0 - # super()._write_sensor_file(super()._get_psu_current_file(self._psu_number), load) - - - def _update_wattage(self, wattage): - """Update wattage inside state file """ - wattage = wattage if wattage >= 0 else 0 - # super()._write_sensor_file(super()._get_psu_wattage_file(self._psu_number), wattage) - - - def _update_fan_speed(self, value): - """Speed In RPMs""" - value = value if value >= 0 else 0 - # super()._write_sensor_file(super()._get_psu_fan_file(self._psu_number), value) - - - def set_psu_status(self, value): - """0x08 indicates AC loss""" - pass - # if super().get_state_dir(): - # super()._write_sensor_file(super()._get_psu_status_file(self._psu_number), value) - - - def update_load(self, load): - super().update_load(load) - min_load = self._asset_info['powerConsumption'] / self._asset_info['powerSource'] - - # if super().get_state_dir(): - # self._update_current(load + min_load) - # self._update_waltage((load + min_load) * 10) - # self._update_fan_speed(100 if load > 0 else 0) + def get_psu_sensor_names(self): + """Find out BMC-specific psu keys (voltage, status etc.) + Returns: + dict: key value pairs of sensor type / sensor name for the psu + """ + with self._graph_ref.get_session() as db_s: + return GraphReference.get_psu_sensor_names(db_s, self.key, self._psu_number) diff --git a/enginecore/enginecore/state/utils.py b/enginecore/enginecore/state/utils.py index eec495bf..ee2cb7cc 100644 --- a/enginecore/enginecore/state/utils.py +++ b/enginecore/enginecore/state/utils.py @@ -4,7 +4,7 @@ def format_as_redis_key(key, oid, key_formatted=True): """Convert asset key & OID into SNMPSim format as - `{asset-key}-{oid}` where each OID digits are padded with 9 zeros + `{asset-key}-{oid}` where each OID digit is padded with 9 zeros Args: key(str): asset key oid(str): unformatted OID e.g. 1.3.6.1.4.1.13742.4.1.2.2.1.3.3 diff --git a/enginecore/enginecore/state/web_socket.py b/enginecore/enginecore/state/web_socket.py index 96ceead1..0a206d43 100644 --- a/enginecore/enginecore/state/web_socket.py +++ b/enginecore/enginecore/state/web_socket.py @@ -6,7 +6,7 @@ from circuits import handler, Component from circuits.net.events import write from enginecore.state.assets import SUPPORTED_ASSETS -from enginecore.state.state_managers import StateManager +from enginecore.state.api import IStateManager from enginecore.model.graph_reference import GraphReference @@ -35,7 +35,7 @@ def connect(self, sock, host, port): print("WebSocket Client Connected:", host, port) # Return assets and their states to the new client - assets = StateManager.get_system_status(flatten=False) + assets = IStateManager.get_system_status(flatten=False) graph_ref = GraphReference() with graph_ref.get_session() as session: @@ -52,7 +52,7 @@ def connect(self, sock, host, port): self.fire(write(sock, json.dumps({ 'request': ClientRequests.ambient.name, 'data': { - 'ambient': StateManager.get_ambient(), + 'ambient': IStateManager.get_ambient(), 'rising': False } }))) @@ -60,7 +60,7 @@ def connect(self, sock, host, port): self.fire(write(sock, json.dumps({ 'request': ClientRequests.mains.name, 'data': { - 'mains': StateManager.mains_status() + 'mains': IStateManager.mains_status() } }))) @@ -75,11 +75,9 @@ def read(self, _, data): if data['request'] == 'power': asset_key = data['key'] power_up = data['data']['status'] - asset_type = data['data']['type'] - - asset_info = GraphReference.get_asset_and_components(session, asset_key) - state_manager = SUPPORTED_ASSETS[asset_type].StateManagerCls(asset_info, notify=True) - + + state_manager = IStateManager.get_state_manager_by_key(asset_key, SUPPORTED_ASSETS) + if power_up: state_manager.power_up() else: @@ -88,9 +86,9 @@ def read(self, _, data): GraphReference.save_layout(session, data['data']['assets'], stage=data['data']['stage']) elif data['request'] == 'mains': if data['mains'] == 0: - StateManager.power_outage() + IStateManager.power_outage() else: - StateManager.power_restore() + IStateManager.power_restore() def disconnect(self, sock): diff --git a/enginecore/simengine-cli b/enginecore/simengine-cli index 68af7ff4..8c6b98ce 100755 --- a/enginecore/simengine-cli +++ b/enginecore/simengine-cli @@ -3,12 +3,16 @@ # pylint: disable=C0103 import argparse +import sys + +import neo4j.exceptions as db_error from enginecore.cli.status import status_command from enginecore.cli.power import power_command from enginecore.cli.thermal import thermal_command from enginecore.cli.configure_state import configure_command from enginecore.cli.model import model_command +from enginecore.cli.storage import storage_command ################ Define Command line options & arguments @@ -30,6 +34,9 @@ power_command( thermal_command( subparsers.add_parser('thermal', help="Manage temperature/thermal settings of the system") ) +storage_command( + subparsers.add_parser('storage', help="Manage storage state of the system") +) configure_command( subparsers.add_parser('configure-state', help="Update runtime state of the assets/sensors") ) @@ -48,5 +55,8 @@ try: options.func(vars(options)) else: argparser.print_help() +except db_error.ConstraintError as e: + print('Database constraint was violated: ') + print(e, file=sys.stderr) except argparse.ArgumentTypeError as e: - print(e) + print(e, file=sys.stderr) diff --git a/enginecore/storcli_template/adapter_count b/enginecore/storcli_template/adapter_count new file mode 100644 index 00000000..2099a426 --- /dev/null +++ b/enginecore/storcli_template/adapter_count @@ -0,0 +1,3 @@ +${header} +Controller Count = ${ctrl_count} + diff --git a/enginecore/storcli_template/alarm_state b/enginecore/storcli_template/alarm_state new file mode 100644 index 00000000..412893b5 --- /dev/null +++ b/enginecore/storcli_template/alarm_state @@ -0,0 +1,11 @@ +Controller Properties : +===================== + +---------------- +Ctrl_Prop Value +---------------- +Alarm ${alarm_state} +---------------- + + + diff --git a/enginecore/storcli_template/bbu_data b/enginecore/storcli_template/bbu_data new file mode 100644 index 00000000..a9cd271b --- /dev/null +++ b/enginecore/storcli_template/bbu_data @@ -0,0 +1,12 @@ +${header} +Detailed Status : +=============== + +-------------------------------------- +Ctrl Status Property ErrMsg ErrCd +-------------------------------------- +${ctrl_num} ${status} ${property} ${err_msg} ${err_code} +-------------------------------------- + + + diff --git a/enginecore/storcli_template/bgi_rate b/enginecore/storcli_template/bgi_rate new file mode 100644 index 00000000..ef0be203 --- /dev/null +++ b/enginecore/storcli_template/bgi_rate @@ -0,0 +1,12 @@ +${header} +Controller Properties : +===================== + +---------------- +Ctrl_Prop Value +---------------- +BGI Rate ${bgi_rate}% +---------------- + + + diff --git a/enginecore/storcli_template/cachevault_data b/enginecore/storcli_template/cachevault_data new file mode 100644 index 00000000..a046fe7e --- /dev/null +++ b/enginecore/storcli_template/cachevault_data @@ -0,0 +1,80 @@ +${header} +Cachevault_Info : +=============== + +-------------------- +Property Value +-------------------- +Type ${model} +Temperature ${temperature} C +State ${state} +-------------------- + + +Firmware_Status : +=============== + +--------------------------------------- +Property Value +--------------------------------------- +Replacement required ${replacement} +No space to cache offload No +Module microcode update required No +--------------------------------------- + + +GasGaugeStatus : +============== + +------------------------------ +Property Value +------------------------------ +Pack Energy 378 J +Capacitance 101 % +Remaining Reserve Space 91 +------------------------------ + + +Design_Info : +=========== + +------------------------------------ +Property Value +------------------------------------ +Date of Manufacture ${mfgDate} +Serial Number ${serialNumber} +Manufacture Name LSI +Design Capacity 283 J +Device Name CVPM02 +tmmFru N/A +CacheVault Flash Size N/A +tmmBatversionNo 0x0 +tmmSerialNo 0x69f4 +tmm Date of Manufacture 24/10/2013 +tmmPcbAssmNo L22541903A +tmmPCBversionNo 0x03 +tmmBatPackAssmNo 49571-02A +scapBatversionNo 0x0 +scapSerialNo 0x4527 +scap Date of Manufacture 25/10/2013 +scapPcbAssmNo 1700054483 +scapPCBversionNo H +scapBatPackAssmNo 49571-02A +Module Version 25849-03 +------------------------------------ + + +Properties : +========== + +-------------------------------------------------------------- +Property Value +-------------------------------------------------------------- +Auto Learn Period 28d (2419200 seconds) +Next Learn time 2018/12/31 19:28:52 (599599732 seconds) +Learn Delay Interval 0 hour(s) +Auto-Learn Mode Transparent +-------------------------------------------------------------- + + + diff --git a/enginecore/storcli_template/cc_rate b/enginecore/storcli_template/cc_rate new file mode 100644 index 00000000..6728d206 --- /dev/null +++ b/enginecore/storcli_template/cc_rate @@ -0,0 +1,12 @@ +${header} +Controller Properties : +===================== + +---------------- +Ctrl_Prop Value +---------------- +CC Rate ${cc_rate}% +---------------- + + + diff --git a/enginecore/storcli_template/controller_entry b/enginecore/storcli_template/controller_entry new file mode 100644 index 00000000..0ca332b4 --- /dev/null +++ b/enginecore/storcli_template/controller_entry @@ -0,0 +1,366 @@ +Basics : +====== +Controller = ${controller_num} +Model = ${model} +Serial Number = ${serial_number} +Current Controller Date/Time = ${controller_date} +Current System Date/time = ${system_date} +SAS Address = ${SAS_address} +PCI Address = ${PCI_address} +Mfg Date = ${mfg_date} +Rework Date = ${rework_date} +Revision No = + + +Version : +======= +Firmware Package Build = 23.34.0-0007 +Firmware Version = 3.460.15-5052 +Bios Version = 5.50.03.0_4.17.08.00_0x06110200 +WebBIOS Version = 6.1-76-e_76-Rel +Preboot CLI Version = 05.07-00:#%00011 +NVDATA Version = 2.1507.03-0155 +Boot Block Version = 2.05.00.00-0010 +Bootloader Version = 07.26.26.219 +Driver Name = megaraid_sas +Driver Version = 07.700.00.00-rc1 + + +Bus : +=== +Vendor Id = 0x1000 +Device Id = 0x5B +SubVendor Id = 0x1734 +SubDevice Id = 0x11E4 +Host Interface = PCI-E +Device Interface = SAS-6G +Bus Number = 1 +Device Number = 0 +Function Number = 0 + + +Pending Images in Flash : +======================= +Image name = No pending images + + +Status : +====== +Controller Status = ${status} +Memory Correctable Errors = ${memory_correctable_errors} +Memory Uncorrectable Errors = ${memory_uncorrectable_errors} +ECC Bucket Count = 0 +Any Offline VD Cache Preserved = No +BBU Status = 0 +PD Firmware Download in progress = No +Lock Key Assigned = No +Failed to get lock key on bootup = No +Lock key has not been backed up = No +Bios was not detected during boot = No +Controller must be rebooted to complete security operation = No +A rollback operation is in progress = No +At least one PFK exists in NVRAM = Yes +SSC Policy is WB = No +Controller has booted into safe mode = No + + +Supported Adapter Operations : +============================ +Rebuild Rate = Yes +CC Rate = Yes +BGI Rate = Yes +Reconstruct Rate = Yes +Patrol Read Rate = Yes +Alarm Control = Yes +Cluster Support = No +BBU = Yes +Spanning = Yes +Dedicated Hot Spare = Yes +Revertible Hot Spares = Yes +Foreign Config Import = Yes +Self Diagnostic = Yes +Allow Mixed Redundancy on Array = No +Global Hot Spares = Yes +Deny SCSI Passthrough = No +Deny SMP Passthrough = No +Deny STP Passthrough = No +Support more than 8 Phys = Yes +FW and Event Time in GMT = No +Support Enhanced Foreign Import = Yes +Support Enclosure Enumeration = Yes +Support Allowed Operations = Yes +Abort CC on Error = Yes +Support Multipath = Yes +Support Odd & Even Drive count in RAID1E = No +Support Security = No +Support Config Page Model = Yes +Support the OCE without adding drives = Yes +Support EKM = No +Snapshot Enabled = No +Support PFK = Yes +Support PI = Yes +Support LDPI Type1 = No +Support LDPI Type2 = No +Support LDPI Type3 = No +Support Ld BBM Info = No +Support Shield State = Yes +Block SSD Write Disk Cache Change = No +Support Suspend Resume BG ops = Yes +Support Emergency Spares = No +Support Set Link Speed = Yes +Support Boot Time PFK Change = No +Support JBOD = Yes +Disable Online PFK Change = No +Support Perf Tuning = Yes +Support SSD PatrolRead = Yes +Real Time Scheduler = Yes +Support Reset Now = Yes +Support Emulated Drives = Yes +Headless Mode = Yes +Dedicated HotSpares Limited = No +Point In Time Progress = Yes +Extended LD = Yes +Boot Volume Supported = No +Support Uneven span = No +Support Config Auto Balance = No +Support Maintenance Mode = No +Support Diagnostic results = No +Support Ext Enclosure = Yes +Support Sesmonitoring = No +Support SecurityonJBOD = No +Support ForceFlash = No +Support DisableImmediateIO = No +Support LargeIOSupport = No +Support DrvActivityLEDSetting = Yes +Support FlushWriteVerify = Yes +Support CPLDUpdate = No +Support ForceTo512e = Yes +Support discardCacheDuringLDDelete = No +Support JBOD Write cache = No +Support Large QD Support = No +Support Ctrl Info Extended = No +Support IButton less = No +Support AES Encryption Algorithm = No +Support Encrypted MFC = No + + +Supported PD Operations : +======================= +Force Online = Yes +Force Offline = Yes +Force Rebuild = Yes +Deny Force Failed = No +Deny Force Good/Bad = No +Deny Missing Replace = No +Deny Clear = No +Deny Locate = No +Support Power State = Yes +Set Power State For Cfg = No +Support T10 Power State = No +Support Temperature = Yes +NCQ = Yes +Support Max Rate SATA = No +Support Degraded Media = No +Support Parallel FW Update = No +Support Drive Crypto Erase = No + + +Supported VD Operations : +======================= +Read Policy = Yes +Write Policy = Yes +IO Policy = Yes +Access Policy = Yes +Disk Cache Policy = Yes +Reconstruction = Yes +Deny Locate = No +Deny CC = No +Allow Ctrl Encryption = No +Enable LDBBM = Yes +Support FastPath = Yes +Performance Metrics = Yes +Power Savings = No +Support Powersave Max With Cache = No +Support Breakmirror = No +Support SSC WriteBack = No +Support SSC Association = No +Support VD Hide = No +Support VD Cachebypass = No +Support VD discardCacheDuringLDDelete = No +Support VD Scsi Unmap = No + + +Advanced Software Option : +======================== + +---------------------------------------- +Adv S/W Opt Time Remaining Mode +---------------------------------------- +MegaRAID FastPath Unlimited - +MegaRAID RAID6 Unlimited - +MegaRAID RAID5 Unlimited - +Cache Offload Unlimited - +---------------------------------------- + +Safe ID = V4ASBCV2HMN46Q6CHC8A9JH5JRCUE5MGLTQ87WAZ + +HwCfg : +===== +ChipRevision = D1 +BatteryFRU = N/A +Front End Port Count = 0 +Backend Port Count = 8 +BBU = Present +Alarm = Disable +Serial Debugger = Present +NVRAM Size = 32KB +Flash Size = 16MB +On Board Memory Size = 1024MB +CacheVault Flash Size = NA +TPM = Absent +Upgrade Key = Absent +On Board Expander = Absent +Temperature Sensor for ROC = Present +Temperature Sensor for Controller = Absent +Upgradable CPLD = Absent +Current Size of CacheCade (GB) = 0 +Current Size of FW Cache (MB) = 866 +ROC temperature(Degree Celsius) = 90 + + +Policies : +======== + +Policies Table : +============== + +------------------------------------------------ +Policy Current Default +------------------------------------------------ +Predictive Fail Poll Interval 300 sec +Interrupt Throttle Active Count 16 +Interrupt Throttle Completion 50 us +Rebuild Rate ${rebuild_rate} % 30% +PR Rate ${pr_rate} % 30% +BGI Rate ${bgi_rate} % 30% +Check Consistency Rate ${cc_rate} % 30% +Reconstruction Rate 30 % 30% +Cache Flush Interval 4s +------------------------------------------------ + +Flush Time(Default) = 4s +Drive Coercion Mode = none +Auto Rebuild = On +Battery Warning = On +ECC Bucket Size = 15 +ECC Bucket Leak Rate (hrs) = 24 +Restore HotSpare on Insertion = On +Expose Enclosure Devices = Off +Maintain PD Fail History = On +Reorder Host Requests = On +Auto detect BackPlane = SGPIO/i2c SEP +Load Balance Mode = Auto +Security Key Assigned = Off +Disable Online Controller Reset = Off +Use drive activity for locate = Off + + +Boot : +==== +BIOS Enumerate VDs = 1 +Stop BIOS on Error = Off +Delay during POST = 0 +Spin Down Mode = None +Enable Ctrl-R = No +Enable Web BIOS = Yes +Enable PreBoot CLI = Yes +Enable BIOS = Yes +Max Drives to Spinup at One Time = 2 +Maximum number of direct attached drives to spin up in 1 min = 20 +Delay Among Spinup Groups (sec) = 6 +Allow Boot with Preserved Cache = Off + + +High Availability : +================= +Topology Type = None +Cluster Permitted = No +Cluster Active = No + + +Defaults : +======== +Phy Polarity = 0 +Phy PolaritySplit = 0 +Strip Size = 64 KB +Write Policy = WB +Read Policy = RA +Cache When BBU Bad = Off +Cached IO = Off +VD PowerSave Policy = Controller Defined +Default spin down time (mins) = 30 +Coercion Mode = None +ZCR Config = Unknown +Max Chained Enclosures = 16 +Direct PD Mapping = No +Restore Hot Spare on Insertion = Yes +Expose Enclosure Devices = No +Maintain PD Fail History = Yes +Zero Based Enclosure Enumeration = No +Disable Puncturing = No +EnableLDBBM = Yes +DisableHII = No +Un-Certified Hard Disk Drives = Allow +SMART Mode = Mode 6 +Enable LED Header = No +LED Show Drive Activity = Yes +Dirty LED Shows Drive Activity = No +EnableCrashDump = No +Disable Online Controller Reset = No +Treat Single span R1E as R10 = No +Power Saving option = Enabled +TTY Log In Flash = Yes +Auto Enhanced Import = No +BreakMirror RAID Support = No +Disable Join Mirror = No +Enable Shield State = Yes +Time taken to detect CME = 60 sec + + +Capabilities : +============ +Supported Drives = SAS, SATA +Boot Volume Supported = NO +RAID Level Supported = RAID0, RAID1(2 or more drives), RAID5, RAID6, RAID00, RAID10(2 or more drives per span), RAID50, RAID60 +Enable JBOD = No +Mix in Enclosure = Allowed +Mix of SAS/SATA of HDD type in VD = Not Allowed +Mix of SAS/SATA of SSD type in VD = Not Allowed +Mix of SSD/HDD in VD = Not Allowed +SAS Disable = No +Max Arms Per VD = 32 +Max Spans Per VD = 8 +Max Arrays = 128 +Max VD per array = 16 +Max Number of VDs = 64 +Max Parallel Commands = 1008 +Max SGE Count = 60 +Max Data Transfer Size = 8192 sectors +Max Strips PerIO = 42 +Max Configurable CacheCade Size(GB) = 0 +Max Transportable DGs = 0 +Min Strip Size = 8 KB +Max Strip Size = 1.0 MB + + +Scheduled Tasks : +=============== +Consistency Check Reoccurrence = 168 hrs +Next Consistency check launch = NA +Patrol Read Reoccurrence = 168 hrs +Next Patrol Read launch = 11/24/2018, 03:00:00 +Battery learn Reoccurrence = 672 hrs +Next Battery Learn = 12/31/2018, 19:00:00 +OEMID = FSC + +Drive Groups = 1 \ No newline at end of file diff --git a/enginecore/storcli_template/controller_info b/enginecore/storcli_template/controller_info new file mode 100644 index 00000000..e501196d --- /dev/null +++ b/enginecore/storcli_template/controller_info @@ -0,0 +1,55 @@ +Generating detailed summary of the adapter, it may take a while to complete. + +${header} + +${controller_entry} + + +TOPOLOGY : +======== + +${topology} + +DG=Disk Group Index|Arr=Array Index|Row=Row Index|EID=Enclosure Device ID +DID=Device ID|Type=Drive Type|Onln=Online|Rbld=Rebuild|Dgrd=Degraded +Pdgd=Partially degraded|Offln=Offline|BT=Background Task Active +PDC=PD Cache|PI=Protection Info|SED=Self Encrypting Drive|Frgn=Foreign +DS3=Dimmer Switch 3|dflt=Default|Msng=Missing|FSpace=Free Space Present +TR=Transport Ready + +Virtual Drives = ${num_virt_drives} + +VD LIST : +======= + +${virtual_drives} + +Cac=CacheCade|Rec=Recovery|OfLn=OffLine|Pdgd=Partially Degraded|Dgrd=Degraded +Optl=Optimal|RO=Read Only|RW=Read Write|HD=Hidden|TRANS=TransportReady|B=Blocked| +Consist=Consistent|R=Read Ahead Always|NR=No Read Ahead|WB=WriteBack| +AWB=Always WriteBack|WT=WriteThrough|C=Cached IO|D=Direct IO|sCC=Scheduled +Check Consistency + +Physical Drives = ${num_phys_drives} + +PD LIST : +======= + +${physical_drives} + +EID-Enclosure Device ID|Slt-Slot No.|DID-Device ID|DG-DriveGroup +DHS-Dedicated Hot Spare|UGood-Unconfigured Good|GHS-Global Hotspare +UBad-Unconfigured Bad|Onln-Online|Offln-Offline|Intf-Interface +Med-Media Type|SED-Self Encryptive Drive|PI-Protection Info +SeSz-Sector Size|Sp-Spun|U-Up|D-Down/PowerSave|T-Transition|F-Foreign +UGUnsp-Unsupported|UGShld-UnConfigured shielded|HSPShld-Hotspare shielded +CFShld-Configured shielded|Cpybck-CopyBack|CBShld-Copyback Shielded + + +Cachevault_Info : +=============== + +${cachevault} + + + diff --git a/enginecore/storcli_template/drive_data b/enginecore/storcli_template/drive_data new file mode 100644 index 00000000..b93615c3 --- /dev/null +++ b/enginecore/storcli_template/drive_data @@ -0,0 +1,762 @@ +${header} +Drive /c0/e252/s0 : +================= + +------------------------------------------------------------------------------ +EID:Slt DID State DG Size Intf Med SED PI SeSz Model Sp Type +------------------------------------------------------------------------------ +252:0 9 Onln 0 136.218 GB SAS HDD N N 512B MK1401GRRB U - +------------------------------------------------------------------------------ + +EID-Enclosure Device ID|Slt-Slot No.|DID-Device ID|DG-DriveGroup +DHS-Dedicated Hot Spare|UGood-Unconfigured Good|GHS-Global Hotspare +UBad-Unconfigured Bad|Onln-Online|Offln-Offline|Intf-Interface +Med-Media Type|SED-Self Encryptive Drive|PI-Protection Info +SeSz-Sector Size|Sp-Spun|U-Up|D-Down/PowerSave|T-Transition|F-Foreign +UGUnsp-Unsupported|UGShld-UnConfigured shielded|HSPShld-Hotspare shielded +CFShld-Configured shielded|Cpybck-CopyBack|CBShld-Copyback Shielded + + +Drive /c0/e252/s0 - Detailed Information : +======================================== + +Drive /c0/e252/s0 State : +======================= +Shield Counter = 0 +Media Error Count = 0 +Other Error Count = 0 +BBM Error Count = 0 +Drive Temperature = 23C (73.40 F) +Predictive Failure Count = 0 +S.M.A.R.T alert flagged by drive = No + + +Drive /c0/e252/s0 Device attributes : +=================================== +SN = Y3D0A0FDFSR2 +Manufacturer Id = TOSHIBA +Model Number = MK1401GRRB +NAND Vendor = NA +WWN = 500003952819DBF9 +Firmware Revision = 5205 +Firmware Release Number = N/A +Raw size = 136.732 GB [0x11177330 Sectors] +Coerced size = 136.218 GB [0x11070000 Sectors] +Non Coerced size = 136.232 GB [0x11077330 Sectors] +Device Speed = 6.0Gb/s +Link Speed = 6.0Gb/s +Write Cache = N/A +Logical Sector Size = 512B +Physical Sector Size = 512B +Connector Name = Port 0 - 3 x1 + + +Drive /c0/e252/s0 Policies/Settings : +=================================== +Drive position = DriveGroup:0, Span:0, Row:0 +Enclosure position = 1 +Connected Port Number = 3(path0) +Sequence Number = 2 +Commissioned Spare = No +Emergency Spare = No +Last Predictive Failure Event Sequence Number = 0 +Successful diagnostics completion on = N/A +SED Capable = No +SED Enabled = No +Secured = No +Cryptographic Erase Capable = No +Locked = No +Needs EKM Attention = No +PI Eligible = No +Certified = No +Wide Port Capable = No + +Port Information : +================ + +----------------------------------------- +Port Status Linkspeed SAS address +----------------------------------------- + 0 Active 6.0Gb/s 0x500003952819dbfa + 1 Active 6.0Gb/s 0x0 +----------------------------------------- + + +Inquiry Data = +00 00 05 12 5b 00 10 02 54 4f 53 48 49 42 41 20 +4d 4b 31 34 30 31 47 52 52 42 20 20 20 20 20 20 +35 32 30 35 59 33 44 30 41 30 46 44 46 53 52 32 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + + + +Drive /c0/e252/s1 : +================= + +------------------------------------------------------------------------------ +EID:Slt DID State DG Size Intf Med SED PI SeSz Model Sp Type +------------------------------------------------------------------------------ +252:1 10 Onln 0 136.218 GB SAS HDD N N 512B MK1401GRRB U - +------------------------------------------------------------------------------ + +EID-Enclosure Device ID|Slt-Slot No.|DID-Device ID|DG-DriveGroup +DHS-Dedicated Hot Spare|UGood-Unconfigured Good|GHS-Global Hotspare +UBad-Unconfigured Bad|Onln-Online|Offln-Offline|Intf-Interface +Med-Media Type|SED-Self Encryptive Drive|PI-Protection Info +SeSz-Sector Size|Sp-Spun|U-Up|D-Down/PowerSave|T-Transition|F-Foreign +UGUnsp-Unsupported|UGShld-UnConfigured shielded|HSPShld-Hotspare shielded +CFShld-Configured shielded|Cpybck-CopyBack|CBShld-Copyback Shielded + + +Drive /c0/e252/s1 - Detailed Information : +======================================== + +Drive /c0/e252/s1 State : +======================= +Shield Counter = 0 +Media Error Count = 0 +Other Error Count = 0 +BBM Error Count = 0 +Drive Temperature = 22C (71.60 F) +Predictive Failure Count = 0 +S.M.A.R.T alert flagged by drive = No + + +Drive /c0/e252/s1 Device attributes : +=================================== +SN = Y3E0A00DFSR2 +Manufacturer Id = TOSHIBA +Model Number = MK1401GRRB +NAND Vendor = NA +WWN = 50000395281A08A9 +Firmware Revision = 5205 +Firmware Release Number = N/A +Raw size = 136.732 GB [0x11177330 Sectors] +Coerced size = 136.218 GB [0x11070000 Sectors] +Non Coerced size = 136.232 GB [0x11077330 Sectors] +Device Speed = 6.0Gb/s +Link Speed = 6.0Gb/s +Write Cache = N/A +Logical Sector Size = 512B +Physical Sector Size = 512B +Connector Name = Port 0 - 3 x1 + + +Drive /c0/e252/s1 Policies/Settings : +=================================== +Drive position = DriveGroup:0, Span:0, Row:1 +Enclosure position = 1 +Connected Port Number = 2(path0) +Sequence Number = 2 +Commissioned Spare = No +Emergency Spare = No +Last Predictive Failure Event Sequence Number = 0 +Successful diagnostics completion on = N/A +SED Capable = No +SED Enabled = No +Secured = No +Cryptographic Erase Capable = No +Locked = No +Needs EKM Attention = No +PI Eligible = No +Certified = No +Wide Port Capable = No + +Port Information : +================ + +----------------------------------------- +Port Status Linkspeed SAS address +----------------------------------------- + 0 Active 6.0Gb/s 0x50000395281a08aa + 1 Active 6.0Gb/s 0x0 +----------------------------------------- + + +Inquiry Data = +00 00 05 12 5b 00 10 02 54 4f 53 48 49 42 41 20 +4d 4b 31 34 30 31 47 52 52 42 20 20 20 20 20 20 +35 32 30 35 59 33 45 30 41 30 30 44 46 53 52 32 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + + + +Drive /c0/e252/s2 : +================= + +------------------------------------------------------------------------------ +EID:Slt DID State DG Size Intf Med SED PI SeSz Model Sp Type +------------------------------------------------------------------------------ +252:2 8 Onln 0 136.218 GB SAS HDD N N 512B MK1401GRRB U - +------------------------------------------------------------------------------ + +EID-Enclosure Device ID|Slt-Slot No.|DID-Device ID|DG-DriveGroup +DHS-Dedicated Hot Spare|UGood-Unconfigured Good|GHS-Global Hotspare +UBad-Unconfigured Bad|Onln-Online|Offln-Offline|Intf-Interface +Med-Media Type|SED-Self Encryptive Drive|PI-Protection Info +SeSz-Sector Size|Sp-Spun|U-Up|D-Down/PowerSave|T-Transition|F-Foreign +UGUnsp-Unsupported|UGShld-UnConfigured shielded|HSPShld-Hotspare shielded +CFShld-Configured shielded|Cpybck-CopyBack|CBShld-Copyback Shielded + + +Drive /c0/e252/s2 - Detailed Information : +======================================== + +Drive /c0/e252/s2 State : +======================= +Shield Counter = 0 +Media Error Count = 0 +Other Error Count = 0 +BBM Error Count = 0 +Drive Temperature = 22C (71.60 F) +Predictive Failure Count = 0 +S.M.A.R.T alert flagged by drive = No + + +Drive /c0/e252/s2 Device attributes : +=================================== +SN = Y3D0A0FNFSR2 +Manufacturer Id = TOSHIBA +Model Number = MK1401GRRB +NAND Vendor = NA +WWN = 500003952819DD39 +Firmware Revision = 5205 +Firmware Release Number = N/A +Raw size = 136.732 GB [0x11177330 Sectors] +Coerced size = 136.218 GB [0x11070000 Sectors] +Non Coerced size = 136.232 GB [0x11077330 Sectors] +Device Speed = 6.0Gb/s +Link Speed = 6.0Gb/s +Write Cache = N/A +Logical Sector Size = 512B +Physical Sector Size = 512B +Connector Name = Port 0 - 3 x1 + + +Drive /c0/e252/s2 Policies/Settings : +=================================== +Drive position = DriveGroup:0, Span:0, Row:2 +Enclosure position = 1 +Connected Port Number = 1(path0) +Sequence Number = 2 +Commissioned Spare = No +Emergency Spare = No +Last Predictive Failure Event Sequence Number = 0 +Successful diagnostics completion on = N/A +SED Capable = No +SED Enabled = No +Secured = No +Cryptographic Erase Capable = No +Locked = No +Needs EKM Attention = No +PI Eligible = No +Certified = No +Wide Port Capable = No + +Port Information : +================ + +----------------------------------------- +Port Status Linkspeed SAS address +----------------------------------------- + 0 Active 6.0Gb/s 0x500003952819dd3a + 1 Active 6.0Gb/s 0x0 +----------------------------------------- + + +Inquiry Data = +00 00 05 12 5b 00 10 02 54 4f 53 48 49 42 41 20 +4d 4b 31 34 30 31 47 52 52 42 20 20 20 20 20 20 +35 32 30 35 59 33 44 30 41 30 46 4e 46 53 52 32 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + + + +Drive /c0/e252/s3 : +================= + +------------------------------------------------------------------------------ +EID:Slt DID State DG Size Intf Med SED PI SeSz Model Sp Type +------------------------------------------------------------------------------ +252:3 7 Onln 0 136.218 GB SAS HDD N N 512B MK1401GRRB U - +------------------------------------------------------------------------------ + +EID-Enclosure Device ID|Slt-Slot No.|DID-Device ID|DG-DriveGroup +DHS-Dedicated Hot Spare|UGood-Unconfigured Good|GHS-Global Hotspare +UBad-Unconfigured Bad|Onln-Online|Offln-Offline|Intf-Interface +Med-Media Type|SED-Self Encryptive Drive|PI-Protection Info +SeSz-Sector Size|Sp-Spun|U-Up|D-Down/PowerSave|T-Transition|F-Foreign +UGUnsp-Unsupported|UGShld-UnConfigured shielded|HSPShld-Hotspare shielded +CFShld-Configured shielded|Cpybck-CopyBack|CBShld-Copyback Shielded + + +Drive /c0/e252/s3 - Detailed Information : +======================================== + +Drive /c0/e252/s3 State : +======================= +Shield Counter = 0 +Media Error Count = 0 +Other Error Count = 0 +BBM Error Count = 0 +Drive Temperature = 22C (71.60 F) +Predictive Failure Count = 0 +S.M.A.R.T alert flagged by drive = No + + +Drive /c0/e252/s3 Device attributes : +=================================== +SN = Y3D0A0FAFSR2 +Manufacturer Id = TOSHIBA +Model Number = MK1401GRRB +NAND Vendor = NA +WWN = 500003952819DBDD +Firmware Revision = 5205 +Firmware Release Number = N/A +Raw size = 136.732 GB [0x11177330 Sectors] +Coerced size = 136.218 GB [0x11070000 Sectors] +Non Coerced size = 136.232 GB [0x11077330 Sectors] +Device Speed = 6.0Gb/s +Link Speed = 6.0Gb/s +Write Cache = N/A +Logical Sector Size = 512B +Physical Sector Size = 512B +Connector Name = Port 0 - 3 x1 + + +Drive /c0/e252/s3 Policies/Settings : +=================================== +Drive position = DriveGroup:0, Span:0, Row:3 +Enclosure position = 1 +Connected Port Number = 0(path0) +Sequence Number = 2 +Commissioned Spare = No +Emergency Spare = No +Last Predictive Failure Event Sequence Number = 0 +Successful diagnostics completion on = N/A +SED Capable = No +SED Enabled = No +Secured = No +Cryptographic Erase Capable = No +Locked = No +Needs EKM Attention = No +PI Eligible = No +Certified = No +Wide Port Capable = No + +Port Information : +================ + +----------------------------------------- +Port Status Linkspeed SAS address +----------------------------------------- + 0 Active 6.0Gb/s 0x500003952819dbde + 1 Active 6.0Gb/s 0x0 +----------------------------------------- + + +Inquiry Data = +00 00 05 12 5b 00 10 02 54 4f 53 48 49 42 41 20 +4d 4b 31 34 30 31 47 52 52 42 20 20 20 20 20 20 +35 32 30 35 59 33 44 30 41 30 46 41 46 53 52 32 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + + + +Drive /c0/e252/s4 : +================= + +------------------------------------------------------------------------------ +EID:Slt DID State DG Size Intf Med SED PI SeSz Model Sp Type +------------------------------------------------------------------------------ +252:4 11 Onln 0 136.218 GB SAS HDD N N 512B MK1401GRRB U - +------------------------------------------------------------------------------ + +EID-Enclosure Device ID|Slt-Slot No.|DID-Device ID|DG-DriveGroup +DHS-Dedicated Hot Spare|UGood-Unconfigured Good|GHS-Global Hotspare +UBad-Unconfigured Bad|Onln-Online|Offln-Offline|Intf-Interface +Med-Media Type|SED-Self Encryptive Drive|PI-Protection Info +SeSz-Sector Size|Sp-Spun|U-Up|D-Down/PowerSave|T-Transition|F-Foreign +UGUnsp-Unsupported|UGShld-UnConfigured shielded|HSPShld-Hotspare shielded +CFShld-Configured shielded|Cpybck-CopyBack|CBShld-Copyback Shielded + + +Drive /c0/e252/s4 - Detailed Information : +======================================== + +Drive /c0/e252/s4 State : +======================= +Shield Counter = 0 +Media Error Count = 0 +Other Error Count = 0 +BBM Error Count = 0 +Drive Temperature = 23C (73.40 F) +Predictive Failure Count = 0 +S.M.A.R.T alert flagged by drive = No + + +Drive /c0/e252/s4 Device attributes : +=================================== +SN = Y3D0A0ICFSR2 +Manufacturer Id = TOSHIBA +Model Number = MK1401GRRB +NAND Vendor = NA +WWN = 500003952819F349 +Firmware Revision = 5205 +Firmware Release Number = N/A +Raw size = 136.732 GB [0x11177330 Sectors] +Coerced size = 136.218 GB [0x11070000 Sectors] +Non Coerced size = 136.232 GB [0x11077330 Sectors] +Device Speed = 6.0Gb/s +Link Speed = 6.0Gb/s +Write Cache = N/A +Logical Sector Size = 512B +Physical Sector Size = 512B +Connector Name = Port 4 - 7 x1 + + +Drive /c0/e252/s4 Policies/Settings : +=================================== +Drive position = DriveGroup:0, Span:0, Row:4 +Enclosure position = 1 +Connected Port Number = 7(path0) +Sequence Number = 2 +Commissioned Spare = No +Emergency Spare = No +Last Predictive Failure Event Sequence Number = 0 +Successful diagnostics completion on = N/A +SED Capable = No +SED Enabled = No +Secured = No +Cryptographic Erase Capable = No +Locked = No +Needs EKM Attention = No +PI Eligible = No +Certified = No +Wide Port Capable = No + +Port Information : +================ + +----------------------------------------- +Port Status Linkspeed SAS address +----------------------------------------- + 0 Active 6.0Gb/s 0x500003952819f34a + 1 Active 6.0Gb/s 0x0 +----------------------------------------- + + +Inquiry Data = +00 00 05 12 5b 00 10 02 54 4f 53 48 49 42 41 20 +4d 4b 31 34 30 31 47 52 52 42 20 20 20 20 20 20 +35 32 30 35 59 33 44 30 41 30 49 43 46 53 52 32 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + + + +Drive /c0/e252/s5 : +================= + +------------------------------------------------------------------------------ +EID:Slt DID State DG Size Intf Med SED PI SeSz Model Sp Type +------------------------------------------------------------------------------ +252:5 14 Onln 0 136.218 GB SAS HDD N N 512B MK1401GRRB U - +------------------------------------------------------------------------------ + +EID-Enclosure Device ID|Slt-Slot No.|DID-Device ID|DG-DriveGroup +DHS-Dedicated Hot Spare|UGood-Unconfigured Good|GHS-Global Hotspare +UBad-Unconfigured Bad|Onln-Online|Offln-Offline|Intf-Interface +Med-Media Type|SED-Self Encryptive Drive|PI-Protection Info +SeSz-Sector Size|Sp-Spun|U-Up|D-Down/PowerSave|T-Transition|F-Foreign +UGUnsp-Unsupported|UGShld-UnConfigured shielded|HSPShld-Hotspare shielded +CFShld-Configured shielded|Cpybck-CopyBack|CBShld-Copyback Shielded + + +Drive /c0/e252/s5 - Detailed Information : +======================================== + +Drive /c0/e252/s5 State : +======================= +Shield Counter = 0 +Media Error Count = 0 +Other Error Count = 0 +BBM Error Count = 0 +Drive Temperature = 22C (71.60 F) +Predictive Failure Count = 0 +S.M.A.R.T alert flagged by drive = No + + +Drive /c0/e252/s5 Device attributes : +=================================== +SN = Y3D0A0IBFSR2 +Manufacturer Id = TOSHIBA +Model Number = MK1401GRRB +NAND Vendor = NA +WWN = 500003952819F2B5 +Firmware Revision = 5205 +Firmware Release Number = N/A +Raw size = 136.732 GB [0x11177330 Sectors] +Coerced size = 136.218 GB [0x11070000 Sectors] +Non Coerced size = 136.232 GB [0x11077330 Sectors] +Device Speed = 6.0Gb/s +Link Speed = 6.0Gb/s +Write Cache = N/A +Logical Sector Size = 512B +Physical Sector Size = 512B +Connector Name = Port 4 - 7 x1 + + +Drive /c0/e252/s5 Policies/Settings : +=================================== +Drive position = DriveGroup:0, Span:0, Row:5 +Enclosure position = 1 +Connected Port Number = 6(path0) +Sequence Number = 2 +Commissioned Spare = No +Emergency Spare = No +Last Predictive Failure Event Sequence Number = 0 +Successful diagnostics completion on = N/A +SED Capable = No +SED Enabled = No +Secured = No +Cryptographic Erase Capable = No +Locked = No +Needs EKM Attention = No +PI Eligible = No +Certified = No +Wide Port Capable = No + +Port Information : +================ + +----------------------------------------- +Port Status Linkspeed SAS address +----------------------------------------- + 0 Active 6.0Gb/s 0x500003952819f2b6 + 1 Active 6.0Gb/s 0x0 +----------------------------------------- + + +Inquiry Data = +00 00 05 12 5b 00 10 02 54 4f 53 48 49 42 41 20 +4d 4b 31 34 30 31 47 52 52 42 20 20 20 20 20 20 +35 32 30 35 59 33 44 30 41 30 49 42 46 53 52 32 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + + + +Drive /c0/e252/s6 : +================= + +------------------------------------------------------------------------------ +EID:Slt DID State DG Size Intf Med SED PI SeSz Model Sp Type +------------------------------------------------------------------------------ +252:6 12 Onln 0 136.218 GB SAS HDD N N 512B MK1401GRRB U - +------------------------------------------------------------------------------ + +EID-Enclosure Device ID|Slt-Slot No.|DID-Device ID|DG-DriveGroup +DHS-Dedicated Hot Spare|UGood-Unconfigured Good|GHS-Global Hotspare +UBad-Unconfigured Bad|Onln-Online|Offln-Offline|Intf-Interface +Med-Media Type|SED-Self Encryptive Drive|PI-Protection Info +SeSz-Sector Size|Sp-Spun|U-Up|D-Down/PowerSave|T-Transition|F-Foreign +UGUnsp-Unsupported|UGShld-UnConfigured shielded|HSPShld-Hotspare shielded +CFShld-Configured shielded|Cpybck-CopyBack|CBShld-Copyback Shielded + + +Drive /c0/e252/s6 - Detailed Information : +======================================== + +Drive /c0/e252/s6 State : +======================= +Shield Counter = 0 +Media Error Count = 0 +Other Error Count = 0 +BBM Error Count = 0 +Drive Temperature = 27C (80.60 F) +Predictive Failure Count = 0 +S.M.A.R.T alert flagged by drive = No + + +Drive /c0/e252/s6 Device attributes : +=================================== +SN = Y3D0A0GYFSR2 +Manufacturer Id = TOSHIBA +Model Number = MK1401GRRB +NAND Vendor = NA +WWN = 500003952819EBDD +Firmware Revision = 5205 +Firmware Release Number = N/A +Raw size = 136.732 GB [0x11177330 Sectors] +Coerced size = 136.218 GB [0x11070000 Sectors] +Non Coerced size = 136.232 GB [0x11077330 Sectors] +Device Speed = 6.0Gb/s +Link Speed = 6.0Gb/s +Write Cache = N/A +Logical Sector Size = 512B +Physical Sector Size = 512B +Connector Name = Port 4 - 7 x1 + + +Drive /c0/e252/s6 Policies/Settings : +=================================== +Drive position = DriveGroup:0, Span:0, Row:6 +Enclosure position = 1 +Connected Port Number = 5(path0) +Sequence Number = 2 +Commissioned Spare = No +Emergency Spare = No +Last Predictive Failure Event Sequence Number = 0 +Successful diagnostics completion on = N/A +SED Capable = No +SED Enabled = No +Secured = No +Cryptographic Erase Capable = No +Locked = No +Needs EKM Attention = No +PI Eligible = No +Certified = No +Wide Port Capable = No + +Port Information : +================ + +----------------------------------------- +Port Status Linkspeed SAS address +----------------------------------------- + 0 Active 6.0Gb/s 0x500003952819ebde + 1 Active 6.0Gb/s 0x0 +----------------------------------------- + + +Inquiry Data = +00 00 05 12 5b 00 10 02 54 4f 53 48 49 42 41 20 +4d 4b 31 34 30 31 47 52 52 42 20 20 20 20 20 20 +35 32 30 35 59 33 44 30 41 30 47 59 46 53 52 32 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + + + +Drive /c0/e252/s7 : +================= + +------------------------------------------------------------------------------ +EID:Slt DID State DG Size Intf Med SED PI SeSz Model Sp Type +------------------------------------------------------------------------------ +252:7 13 Onln 0 136.218 GB SAS HDD N N 512B MK1401GRRB U - +------------------------------------------------------------------------------ + +EID-Enclosure Device ID|Slt-Slot No.|DID-Device ID|DG-DriveGroup +DHS-Dedicated Hot Spare|UGood-Unconfigured Good|GHS-Global Hotspare +UBad-Unconfigured Bad|Onln-Online|Offln-Offline|Intf-Interface +Med-Media Type|SED-Self Encryptive Drive|PI-Protection Info +SeSz-Sector Size|Sp-Spun|U-Up|D-Down/PowerSave|T-Transition|F-Foreign +UGUnsp-Unsupported|UGShld-UnConfigured shielded|HSPShld-Hotspare shielded +CFShld-Configured shielded|Cpybck-CopyBack|CBShld-Copyback Shielded + + +Drive /c0/e252/s7 - Detailed Information : +======================================== + +Drive /c0/e252/s7 State : +======================= +Shield Counter = 0 +Media Error Count = 0 +Other Error Count = 0 +BBM Error Count = 0 +Drive Temperature = 25C (77.00 F) +Predictive Failure Count = 0 +S.M.A.R.T alert flagged by drive = No + + +Drive /c0/e252/s7 Device attributes : +=================================== +SN = Y3D0A0F9FSR2 +Manufacturer Id = TOSHIBA +Model Number = MK1401GRRB +NAND Vendor = NA +WWN = 500003952819DBA5 +Firmware Revision = 5205 +Firmware Release Number = N/A +Raw size = 136.732 GB [0x11177330 Sectors] +Coerced size = 136.218 GB [0x11070000 Sectors] +Non Coerced size = 136.232 GB [0x11077330 Sectors] +Device Speed = 6.0Gb/s +Link Speed = 6.0Gb/s +Write Cache = N/A +Logical Sector Size = 512B +Physical Sector Size = 512B +Connector Name = Port 4 - 7 x1 + + +Drive /c0/e252/s7 Policies/Settings : +=================================== +Drive position = DriveGroup:0, Span:0, Row:7 +Enclosure position = 1 +Connected Port Number = 4(path0) +Sequence Number = 2 +Commissioned Spare = No +Emergency Spare = No +Last Predictive Failure Event Sequence Number = 0 +Successful diagnostics completion on = N/A +SED Capable = No +SED Enabled = No +Secured = No +Cryptographic Erase Capable = No +Locked = No +Needs EKM Attention = No +PI Eligible = No +Certified = No +Wide Port Capable = No + +Port Information : +================ + +----------------------------------------- +Port Status Linkspeed SAS address +----------------------------------------- + 0 Active 6.0Gb/s 0x500003952819dba6 + 1 Active 6.0Gb/s 0x0 +----------------------------------------- + + +Inquiry Data = +00 00 05 12 5b 00 10 02 54 4f 53 48 49 42 41 20 +4d 4b 31 34 30 31 47 52 52 42 20 20 20 20 20 20 +35 32 30 35 59 33 44 30 41 30 46 39 46 53 52 32 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + + + + diff --git a/enginecore/storcli_template/header b/enginecore/storcli_template/header new file mode 100644 index 00000000..6fc8b7e1 --- /dev/null +++ b/enginecore/storcli_template/header @@ -0,0 +1,4 @@ +CLI Version = ${cli_version} +Operating system = ${op_sys} +${controller_line}Status = ${status} +Description = ${description} diff --git a/enginecore/storcli_template/performance_mode b/enginecore/storcli_template/performance_mode new file mode 100644 index 00000000..cd2cc69a --- /dev/null +++ b/enginecore/storcli_template/performance_mode @@ -0,0 +1,12 @@ +${header} +Controller Properties : +===================== + +----------------------------------------- +Ctrl_Prop Value +----------------------------------------- +Perf Mode ${mode_num} - ${mode_description} +----------------------------------------- + + + diff --git a/enginecore/storcli_template/physical_disk_data b/enginecore/storcli_template/physical_disk_data new file mode 100644 index 00000000..2c1da8d8 --- /dev/null +++ b/enginecore/storcli_template/physical_disk_data @@ -0,0 +1,7 @@ +${header} + +${physical_drives} + + + + diff --git a/enginecore/storcli_template/physical_disk_entry b/enginecore/storcli_template/physical_disk_entry new file mode 100644 index 00000000..993c5e23 --- /dev/null +++ b/enginecore/storcli_template/physical_disk_entry @@ -0,0 +1,91 @@ + +Drive ${drive_path} : +================= + +${drive_table} + +EID-Enclosure Device ID|Slt-Slot No.|DID-Device ID|DG-DriveGroup +DHS-Dedicated Hot Spare|UGood-Unconfigured Good|GHS-Global Hotspare +UBad-Unconfigured Bad|Onln-Online|Offln-Offline|Intf-Interface +Med-Media Type|SED-Self Encryptive Drive|PI-Protection Info +SeSz-Sector Size|Sp-Spun|U-Up|D-Down/PowerSave|T-Transition|F-Foreign +UGUnsp-Unsupported|UGShld-UnConfigured shielded|HSPShld-Hotspare shielded +CFShld-Configured shielded|Cpybck-CopyBack|CBShld-Copyback Shielded + + +Drive ${drive_path} - Detailed Information : +======================================== + +Drive ${drive_path} State : +======================= +Shield Counter = 0 +Media Error Count = ${media_error_count} +Other Error Count = ${other_error_count} +BBM Error Count = 0 +Drive Temperature = ${drive_temp_c}C (${drive_temp_f} F) +Predictive Failure Count = ${predictive_failure_count} +S.M.A.R.T alert flagged by drive = No + + +Drive ${drive_path} Device attributes : +=================================== +SN = Y3D0A0FDFSR2 +Manufacturer Id = TOSHIBA +Model Number = ${drive_model} +NAND Vendor = NA +WWN = 500003952819DBF9 +Firmware Revision = 5205 +Firmware Release Number = N/A +Raw size = ${drive_size} [0x11177330 Sectors] +Coerced size = ${drive_size} [0x11070000 Sectors] +Non Coerced size = ${drive_size} [0x11077330 Sectors] +Device Speed = 6.0Gb/s +Link Speed = 6.0Gb/s +Write Cache = N/A +Logical Sector Size = 512B +Physical Sector Size = 512B +Connector Name = Port 0 - 3 x1 + + +Drive ${drive_path} Policies/Settings : +=================================== +Drive position = DriveGroup:0, Span:0, Row:0 +Enclosure position = 1 +Connected Port Number = 3(path0) +Sequence Number = 2 +Commissioned Spare = No +Emergency Spare = No +Last Predictive Failure Event Sequence Number = 0 +Successful diagnostics completion on = N/A +SED Capable = No +SED Enabled = No +Secured = No +Cryptographic Erase Capable = No +Locked = No +Needs EKM Attention = No +PI Eligible = No +Certified = No +Wide Port Capable = No + +Port Information : +================ + +----------------------------------------- +Port Status Linkspeed SAS address +----------------------------------------- + 0 Active 6.0Gb/s 0x500003952819dbfa + 1 Active 6.0Gb/s 0x0 +----------------------------------------- + + +Inquiry Data = +00 00 05 12 5b 00 10 02 54 4f 53 48 49 42 41 20 +4d 4b 31 34 30 31 47 52 52 42 20 20 20 20 20 20 +35 32 30 35 59 33 44 30 41 30 46 44 46 53 52 32 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + + diff --git a/enginecore/storcli_template/pr_rate b/enginecore/storcli_template/pr_rate new file mode 100644 index 00000000..d6719621 --- /dev/null +++ b/enginecore/storcli_template/pr_rate @@ -0,0 +1,9 @@ +${header} +Controller Properties : +===================== + +----------------------- +Ctrl_Prop Value +----------------------- +Patrol Read Rate ${pr_rate}% +----------------------- diff --git a/enginecore/storcli_template/rebuild_rate b/enginecore/storcli_template/rebuild_rate new file mode 100644 index 00000000..6699d1ac --- /dev/null +++ b/enginecore/storcli_template/rebuild_rate @@ -0,0 +1,12 @@ +${header} +Controller Properties : +===================== + +------------------ +Ctrl_Prop Value +------------------ +Rebuildrate ${rebuild_rate}% +------------------ + + + diff --git a/enginecore/storcli_template/virtual_drive_data b/enginecore/storcli_template/virtual_drive_data new file mode 100644 index 00000000..78c8b441 --- /dev/null +++ b/enginecore/storcli_template/virtual_drive_data @@ -0,0 +1,49 @@ +/c${controller}/v${virtual_drives_num} : +====== + +${virtual_drives} + +Cac=CacheCade|Rec=Recovery|OfLn=OffLine|Pdgd=Partially Degraded|Dgrd=Degraded +Optl=Optimal|RO=Read Only|RW=Read Write|HD=Hidden|TRANS=TransportReady|B=Blocked| +Consist=Consistent|R=Read Ahead Always|NR=No Read Ahead|WB=WriteBack| +AWB=Always WriteBack|WT=WriteThrough|C=Cached IO|D=Direct IO|sCC=Scheduled +Check Consistency + + +PDs for VD ${virtual_drives_num} : +============ + +${physical_drives} + +EID-Enclosure Device ID|Slt-Slot No.|DID-Device ID|DG-DriveGroup +DHS-Dedicated Hot Spare|UGood-Unconfigured Good|GHS-Global Hotspare +UBad-Unconfigured Bad|Onln-Online|Offln-Offline|Intf-Interface +Med-Media Type|SED-Self Encryptive Drive|PI-Protection Info +SeSz-Sector Size|Sp-Spun|U-Up|D-Down/PowerSave|T-Transition|F-Foreign +UGUnsp-Unsupported|UGShld-UnConfigured shielded|HSPShld-Hotspare shielded +CFShld-Configured shielded|Cpybck-CopyBack|CBShld-Copyback Shielded + + +VD${virtual_drives_num} Properties : +============== +Strip Size = 64 KB +Number of Blocks = 1714028544 +VD has Emulated PD = No +Span Depth = 1 +Number of Drives Per Span = 8 +Write Cache(initial setting) = WriteBack +Disk Cache Policy = Disabled +Encryption = None +Data Protection = Disabled +Active Operations = None +Exposed to OS = Yes +OS Drive Name = /dev/sda +Creation Date = 15-11-2018 +Creation Time = 04:00:22 AM +Emulation type = default +Is LD Ready for OS Requests = Yes +SCSI NAA Id = 60030057013c6550237fabd6107d475d +SCSI Unmap = No + + + diff --git a/enginecore/tests/server_load_m1.py b/enginecore/tests/server_load_m1.py index dd704281..906a4eef 100644 --- a/enginecore/tests/server_load_m1.py +++ b/enginecore/tests/server_load_m1.py @@ -91,7 +91,7 @@ def test_server_power(self): 'domainName': 'an-a01n01', 'powerConsumption': 480, 'powerSource': 120 - }, 'serverwithbmc', notify=True) + }, notify=True) server.shut_down() thread.join() @@ -110,7 +110,7 @@ def test_server_power(self): def test_static(self): - static = StaticDeviceStateManager({ 'key': 5, 'name': 'test', 'powerConsumption': 240,'powerSource': 120}, 'staticasset', notify=True) + static = StaticDeviceStateManager({ 'key': 5, 'name': 'test', 'powerConsumption': 240,'powerSource': 120}, notify=True) static_up = {'1-outlet': 4, '2-outlet':2, '3-pdu':4, '31-outlet':2, '33-outlet':2, '4-serverwithbmc':4, '5-staticasset':2, '41-psu':2, '42-psu':2} static_down = {'1-outlet': 2, '2-outlet':2, '3-pdu':2, '31-outlet':2, '33-outlet':0, '4-serverwithbmc':4, '5-staticasset':0, '41-psu':2, '42-psu':2} @@ -135,8 +135,8 @@ def test_static(self): def test_server_psu(self): try: - psu_1 = StateManager({ 'key': 41 }, 'psu', notify=True) - psu_2 = StateManager({ 'key': 42 }, 'psu', notify=True) + psu_1 = StateManager({ 'key': 41 }, notify=True) + psu_2 = StateManager({ 'key': 42 }, notify=True) only_psu1_down = {'1-outlet': 2, '2-outlet':4, '3-pdu':2, '31-outlet':0, '33-outlet':2, '4-serverwithbmc':4, '41-psu':0, '42-psu':4} diff --git a/enginecore/tests/snmp_pdu.py b/enginecore/tests/snmp_pdu.py index c01862a7..58667911 100644 --- a/enginecore/tests/snmp_pdu.py +++ b/enginecore/tests/snmp_pdu.py @@ -146,7 +146,7 @@ def test_pdu_load(self): print("-> Test pdu load") - sm_out_1 = StateManager({'key': 31}, 'outlet', notify=True) + sm_out_1 = StateManager({'key': 31}, notify=True) amp_oid = '1.3.6.1.4.1.318.1.1.12.2.3.1.1.2.1' wattage_oid = '1.3.6.1.4.1.318.1.1.12.1.16.0' diff --git a/enginecore/tests/snmp_ups.py b/enginecore/tests/snmp_ups.py index 2782d45d..60c05a2a 100644 --- a/enginecore/tests/snmp_ups.py +++ b/enginecore/tests/snmp_ups.py @@ -73,8 +73,8 @@ def test_ups_oids(self): all_up_state = {'1-outlet': 1, '3-ups':1, '35-outlet':1, '33-outlet':1, '31-outlet':1, '4-staticasset': 1, '5-staticasset': 1, '6-staticasset': 1} expected_state = all_up_state.copy() - sm_out_1 = StateManager({'key': 1}, 'outlet', notify=True) - sm_out_31 = StateManager({'key': 31}, 'outlet', notify=True) + sm_out_1 = StateManager({'key': 1}, notify=True) + sm_out_31 = StateManager({'key': 31}, notify=True) hp_battery_oid = '1.3.6.1.4.1.318.1.1.1.2.3.1.0' # Battery charge 100% * 10 adv_battery_oid = '1.3.6.1.4.1.318.1.1.1.2.2.1.0' # Battery charge 100% diff --git a/storage-emulation-tests/README.md b/storage-emulation-tests/README.md index 8cedd467..640dc1c6 100644 --- a/storage-emulation-tests/README.md +++ b/storage-emulation-tests/README.md @@ -14,29 +14,31 @@ To switch between the two communications techniques, adjust the source code in t ## Virsh XML setup (using QEMU-KVM) * Ensure that the libvirt QEMU schema is included in the XML namespace: - \ + `` + +* For communication via a socket: + ``` + + + + + + + + + ``` * For communication via a pipe: - \ - \ - \ - \ - \ - \ - \ - \ - -* For communication via a pipe: - \ - \ - \ - \ - \ - \ - \ - \ - \ - + ``` + + + + + + + + + ``` Note that it is OK to enable both configurations in the VM at the same time. ## Communication via pipes diff --git a/storage-emulation-tests/guest/storcli64 b/storage-emulation-tests/guest/storcli64 index 6fe52b49..f090aa18 100755 --- a/storage-emulation-tests/guest/storcli64 +++ b/storage-emulation-tests/guest/storcli64 @@ -17,12 +17,11 @@ import os # Step 0: set up connection to simengine via virtio-serial connection # Select ONE of the following two lines to use socket or pipe communication -simengine = open("/dev/virtio-ports/systems.cdot.simengine.storage.pipe", "r+b", 0) -#simengine = open("/dev/virtio-ports/systems.cdot.simengine.storage.net", "r+b", 0) +# simengine = open("/dev/virtio-ports/systems.cdot.simengine.storage.pipe", "r+b", 0) +simengine = open("/dev/virtio-ports/systems.cdot.simengine.storage.net", "r+b", 0) # Step 1: encapsulate command-line arguments in json and send to socket simengine.write(bytes(json.dumps({'argv': sys.argv}), 'UTF-8')) -simengine.write(bytes('\n', "UTF-8")) # Step 2: wait for one line from the other end, deserialize, and output # the received values