Skip to content

Commit

Permalink
Identify Stargate nodes in the UI (#1179)
Browse files Browse the repository at this point in the history
* Add parsing to gossipinfo content to identify the stargate nodes and return property via the NodeStatus object
* Contextually set the visualization and tooltip on the nodes for Stargate nodes
* Remove some stray console logging and bump a legitimate problem up to an error.
* Change property to be more future proof in case there are ever other types of nodes to detect
* Visually display stargate nodes differently and do not include stargate nodes in selection options for creating repairs/schedules
* Cleanup node status modal when node is a stargate node
* Refactor out some common functionality to a shared utils class
* Add file header, cleanup imports and comments
  • Loading branch information
jdonenine authored Mar 29, 2022
1 parent db87066 commit c54491e
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public final class NodesStatus {
private static final List<Pattern> ENDPOINT_SEVERITY_PATTERNS = Lists.newArrayList();
private static final List<Pattern> ENDPOINT_HOSTID_PATTERNS = Lists.newArrayList();
private static final List<Pattern> ENDPOINT_TOKENS_PATTERNS = Lists.newArrayList();
private static final List<Pattern> ENDPOINT_TYPE_PATTERNS = Lists.newArrayList();

private static final Pattern ENDPOINT_NAME_PATTERN_IP4
= Pattern.compile("^([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})", Pattern.MULTILINE | Pattern.DOTALL);
Expand All @@ -65,6 +66,7 @@ public final class NodesStatus {
private static final Pattern ENDPOINT_SEVERITY_21_PATTERN = Pattern.compile("(SEVERITY)(:)([0-9.]+)");
private static final Pattern ENDPOINT_HOSTID_21_PATTERN = Pattern.compile("(HOST_ID)(:)([0-9a-z-]+)");
private static final Pattern ENDPOINT_LOAD_SCYLLA_44_PATTERN = Pattern.compile("(LOAD)(:)([0-9eE.\\+]+)");
private static final Pattern ENDPOINT_TYPE_STARGATE_PATTERN = Pattern.compile("(X10):([0-9]*):(stargate)");

private static final String NOT_AVAILABLE = "Not available";

Expand Down Expand Up @@ -146,6 +148,7 @@ private GossipInfo parseEndpointStatesString(
Optional<String> tokens = parseEndpointState(ENDPOINT_TOKENS_PATTERNS, endpointString, 2, String.class);
Optional<Double> load = parseEndpointState(ENDPOINT_LOAD_PATTERNS, endpointString, 3, Double.class);
totalLoad += load.orElse(0.0);
Optional<String> stargate = parseEndpointState(ENDPOINT_TYPE_PATTERNS, endpointString, 3, String.class);

EndpointState endpointState = new EndpointState(
endpoint.orElse(NOT_AVAILABLE),
Expand All @@ -156,7 +159,8 @@ private GossipInfo parseEndpointStatesString(
severity.orElse(0.0),
releaseVersion.orElse(NOT_AVAILABLE),
tokens.orElse(NOT_AVAILABLE),
load.orElse(0.0));
load.orElse(0.0),
stargate.isPresent() ? NodeType.STARGATE : NodeType.CASSANDRA);

if (!status.orElse(NOT_AVAILABLE).toLowerCase().contains("left")
&& !status.orElse(NOT_AVAILABLE).toLowerCase().contains("removed")) {
Expand Down Expand Up @@ -207,6 +211,7 @@ private static void initPatterns() {
ENDPOINT_SEVERITY_PATTERNS.addAll(Arrays.asList(ENDPOINT_SEVERITY_22_PATTERN, ENDPOINT_SEVERITY_21_PATTERN));
ENDPOINT_HOSTID_PATTERNS.addAll(Arrays.asList(ENDPOINT_HOSTID_22_PATTERN, ENDPOINT_HOSTID_21_PATTERN));
ENDPOINT_TOKENS_PATTERNS.add(ENDPOINT_TOKENS_22_PATTERN);
ENDPOINT_TYPE_PATTERNS.add(ENDPOINT_TYPE_STARGATE_PATTERN);
}

public static final class GossipInfo {
Expand Down Expand Up @@ -236,6 +241,11 @@ public GossipInfo(
}
}

public enum NodeType {
CASSANDRA,
STARGATE;
}

public static final class EndpointState {

@JsonProperty
Expand Down Expand Up @@ -265,6 +275,9 @@ public static final class EndpointState {
@JsonProperty
public final Double load;

@JsonProperty
public final NodeType type;

public EndpointState(
String endpoint,
String hostId,
Expand All @@ -274,7 +287,8 @@ public EndpointState(
Double severity,
String releaseVersion,
String tokens,
Double load) {
Double load,
NodeType type) {

this.endpoint = endpoint;
this.hostId = hostId;
Expand All @@ -285,6 +299,7 @@ public EndpointState(
this.releaseVersion = releaseVersion;
this.tokens = tokens;
this.load = load;
this.type = type;
}

public String getDc() {
Expand Down Expand Up @@ -322,7 +337,10 @@ public String toString() {
+ hostId
+ " / "
+ "Tokens : "
+ tokens;
+ tokens
+ " / "
+ "Type : "
+ type;
}
}
}
4 changes: 1 addition & 3 deletions src/ui/app/jsx/cluster-list.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,15 @@ const Cluster = CreateReactClass({
method: 'GET',
component: this,
complete: function(data) {
console.log(this.component.props.name + " complete.");
this.component.setState({clusterStatuses: setTimeout(this.component._refreshClusterStatus, 30000),
clusterStatus: $.parseJSON(data.responseText)});

if(this.component.state.clusterStatus.nodes_status){
this.component.setState({nodes_status: this.component.state.clusterStatus.nodes_status});
}
console.log(this.component.props.name + " : Next attempt in 30s.")
},
error: function(data) {
console.log(this.component.props.name + " failed.");
console.error(this.component.props.name + " failed.");
this.component.setState({clusterStatuses: setTimeout(this.component._refreshClusterStatus, 30000)});

}
Expand Down
7 changes: 2 additions & 5 deletions src/ui/app/jsx/event-subscription-form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import CreateReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import Select from 'react-select';
import {getUrlPrefix} from "jsx/mixin";
import {getNodeOptions} from "../node-utils";


const subscriptionForm = CreateReactClass({
Expand Down Expand Up @@ -197,11 +198,7 @@ const subscriptionForm = CreateReactClass({
},

_getNodeOptions: function() {
this.setState({
nodeOptions: this.state.clusterStatus.nodes_status.endpointStates[0].endpointNames.map(
obj => { return {value: obj, label: obj}; }
)
});
this.setState(getNodeOptions(this.state.clusterStatus.nodes_status.endpointStates, true));
},

_handleSelectOnChange: function(valueContext, actionContext) {
Expand Down
84 changes: 60 additions & 24 deletions src/ui/app/jsx/node-status.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ const NodeStatus = CreateReactClass({
},

render: function() {
const isStargate = this.props.endpointStatus.type === "STARGATE";

let displayNodeStyle = {
display: "none"
}
Expand Down Expand Up @@ -213,8 +215,14 @@ const NodeStatus = CreateReactClass({

let buttonStyle = "btn btn-xs btn-success";
let largeButtonStyle = "btn btn-lg btn-static btn-success";

if(!this.props.endpointStatus.status.endsWith('UP')){

// If the node is a Stargate node we won't have real status for it
// so we'll display it slightly differently
if (isStargate) {
buttonStyle = "btn btn-xs btn-info";
largeButtonStyle = "btn btn-lg btn-static btn-info";
// If it's not a stargate node, then we can appropriately use its status
} else if(!this.props.endpointStatus.status.endsWith('UP')){
buttonStyle = "btn btn-xs btn-danger";
largeButtonStyle = "btn btn-lg btn-static btn-danger";
}
Expand All @@ -234,9 +242,24 @@ const NodeStatus = CreateReactClass({
overflow: "auto",
height: "200px"
}

const modalTitle = (
<Modal.Title>
Endpoint {this.props.endpointStatus.endpoint}
{isStargate &&
<span>&nbsp;(Stargate)</span>
}
</Modal.Title>
);

const tooltip = (
<Tooltip id="tooltip"><strong>{this.props.endpointStatus.endpoint}</strong> ({humanFileSize(this.props.endpointStatus.load, 1024)})</Tooltip>
<Tooltip id="tooltip">
<strong>{this.props.endpointStatus.endpoint}</strong>
{isStargate &&
<span>&nbsp;(Stargate)</span>
}
&nbsp;({humanFileSize(this.props.endpointStatus.load, 1024)})
</Tooltip>
);

const tokenList = this.state.tokens.map(token => <div key={token}>{token}</div>);
Expand Down Expand Up @@ -271,7 +294,7 @@ const NodeStatus = CreateReactClass({
<OverlayTrigger placement="top" overlay={tooltip}><button type="button" style={btStyle} className={buttonStyle} onClick={this.open}>&nbsp;</button></OverlayTrigger>
<Modal show={this.state.showModal} onHide={this.close} bsSize="large" aria-labelledby="contained-modal-title-lg" dialogClassName="large-modal">
<Modal.Header closeButton>
<Modal.Title>Endpoint {this.props.endpointStatus.endpoint}</Modal.Title>
{modalTitle}
</Modal.Header>
<Modal.Body>
<div className="row">
Expand All @@ -284,26 +307,39 @@ const NodeStatus = CreateReactClass({
<p>{this.props.endpointStatus.dc} / {this.props.endpointStatus.rack}</p>
</div>
<div className="col-lg-3">
<h4>Release version</h4>
<p>{this.props.endpointStatus.releaseVersion}</p>
</div>
<div className="col-lg-3">
<h4>Tokens</h4>
<p><OverlayTrigger trigger="click" placement="bottom" overlay={tokens}><button type="button" className="btn btn-md btn-info" style={takeSnapshotStyle}>{this.state.tokens.length}</button></OverlayTrigger></p>
<h4>Node Type</h4>
<p>{isStargate ? "Stargate" : "Cassandra"}</p>
</div>
<div className="col-lg-3">
<h4>Status</h4>
<p><button type="button" className={largeButtonStyle}>{this.props.endpointStatus.status}</button></p>
</div>
<div className="col-lg-3">
<h4>Severity</h4>
<p>{this.props.endpointStatus.severity}</p>
</div>
<div className="col-lg-3">
<h4>Data size on disk</h4>
<p>{humanFileSize(this.props.endpointStatus.load, 1024)}</p>
<h4>Release version</h4>
<p>{this.props.endpointStatus.releaseVersion}</p>
</div>
{!isStargate &&
<div className="col-lg-3">
<h4>Tokens</h4>
<p><OverlayTrigger trigger="click" placement="bottom" overlay={tokens}><button type="button" className="btn btn-md btn-info" style={takeSnapshotStyle}>{this.state.tokens.length}</button></OverlayTrigger></p>
</div>
}
{!isStargate &&
<div className="col-lg-3">
<h4>Status</h4>
<p><button type="button" className={largeButtonStyle}>{this.props.endpointStatus.status}</button></p>
</div>
}
{!isStargate &&
<div className="col-lg-3">
<h4>Severity</h4>
<p>{this.props.endpointStatus.severity}</p>
</div>
}
{!isStargate &&
<div className="col-lg-3">
<h4>Data size on disk</h4>
<p>{humanFileSize(this.props.endpointStatus.load, 1024)}</p>
</div>
}
</div>
{!isStargate &&
<div className="row">
<div className="col-lg-12">
<Tabs defaultActiveKey={1} id="node-tab">
Expand Down Expand Up @@ -342,10 +378,10 @@ const NodeStatus = CreateReactClass({
</p>
</div>
}
<OverlayTrigger trigger="focus" placement="bottom" overlay={takeSnapshotClick}><button type="button" className="btn btn-md btn-success" style={takeSnapshotStyle}>Take a snapshot</button></OverlayTrigger>
<OverlayTrigger trigger="focus" placement="bottom" overlay={takeSnapshotClick}><button type="button" className="btn btn-md btn-success" style={takeSnapshotStyle}>Take a snapshot</button></OverlayTrigger>
<button type="button" className="btn btn-md btn-success" style={progressStyle} disabled>Taking a snapshot...</button>
</div>
<div className="col-lg-12">&nbsp;</div>
<div className="col-lg-12">&nbsp;</div>
{snapshots}
</div>
</div>
Expand All @@ -354,10 +390,10 @@ const NodeStatus = CreateReactClass({
<Tab eventKey={6} title="Streams">
<Streams endpoint={this.props.endpointStatus.endpoint} clusterName={this.props.clusterName}/>
</Tab>
</Tabs>
</Tabs>
</div>
</div>

}
</Modal.Body>
<Modal.Footer>
<Button onClick={this.close}>Close</Button>
Expand Down
7 changes: 2 additions & 5 deletions src/ui/app/jsx/repair-form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import Moment from 'moment';
import moment from "moment";
import Modal from 'react-bootstrap/lib/Modal';
import Button from 'react-bootstrap/lib/Button';
import {getNodeOptions} from "../node-utils";

Moment.locale(navigator.language);

Expand Down Expand Up @@ -165,11 +166,7 @@ const repairForm = CreateReactClass({
},

_getNodeOptions: function() {
this.setState({
nodeOptions: this.state.clusterStatus.nodes_status.endpointStates[0].endpointNames.sort().map(
obj => { return {value: obj, label: obj}; }
)
});
this.setState(getNodeOptions(this.state.clusterStatus.nodes_status.endpointStates, true));
},

_getKeyspaceOptions: function() {
Expand Down
73 changes: 73 additions & 0 deletions src/ui/app/node-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* Utility method for digging through the endpointStates API response structure
* to identify the possible nodes
*
* @param endpointStates - Array
* @returns An array of node/endpoint objects
*/

export const getNodesFromEndpointStates = function(endpointStates) {
const nodes = [];
if (!endpointStates || !endpointStates.length) {
return nodes;
}

for(let endpointState of endpointStates) {
if (!endpointState || !endpointState.endpoints) {
continue;
}
for (let datacenterId in endpointState.endpoints) {
const datacenter = endpointState.endpoints[datacenterId];
if (!datacenter) {
continue;
}
for (let rackId in datacenter) {
const rack = datacenter[rackId];
if (!rack) {
continue;
}
for (let endpoint of rack) {
if (endpoint) {
nodes.push(endpoint);
}
}
}
}
}
return nodes;
}

/**
* Utility function for generating a set of options for a select representing a drop down of nodes.
* If excludeStargateNodes is true stargate nodes will not be included in the set of options generated.
*
* @param endpointStates - Array
* @param excludeStargateNodes - Boolean
* @returns An object suitable for passing to Select component
*/
export const getNodeOptions = function(endpointStates, excludeStargateNodes) {
let nodes = getNodesFromEndpointStates(endpointStates);
if (!nodes) {
return {
nodeOptions: []
}
}
const includedNodes = excludeStargateNodes ? nodes.filter(node => node.type !== "STARGATE") : nodes;
return {
nodeOptions: includedNodes.map(node => node.endpoint).sort().map(
obj => { return {value: obj, label: obj}; }
)
};
}

0 comments on commit c54491e

Please sign in to comment.