changes to support mobility from rest and configuring mobility from a wlan context menu

This commit is contained in:
Blake J. Harnden 2018-09-12 14:34:37 -07:00
parent dd9ad5644d
commit f2f83f247d
12 changed files with 379 additions and 15 deletions

View file

@ -71,6 +71,7 @@ public class Controller implements Initializable {
private NodeWlanDialog nodeWlanDialog = new NodeWlanDialog(this); private NodeWlanDialog nodeWlanDialog = new NodeWlanDialog(this);
private ConfigDialog configDialog = new ConfigDialog(this); private ConfigDialog configDialog = new ConfigDialog(this);
private HooksDialog hooksDialog = new HooksDialog(this); private HooksDialog hooksDialog = new HooksDialog(this);
private MobilityDialog mobilityDialog = new MobilityDialog(this);
public Controller() { public Controller() {
// load configuration // load configuration
@ -108,6 +109,7 @@ public class Controller implements Initializable {
nodeWlanDialog.setOwner(window); nodeWlanDialog.setOwner(window);
nodeEmaneDialog.setOwner(window); nodeEmaneDialog.setOwner(window);
configDialog.setOwner(window); configDialog.setOwner(window);
mobilityDialog.setOwner(window);
} }
@FXML @FXML

View file

@ -39,9 +39,10 @@ public class CoreClient {
logger.info("joining core session({}) state({}): {}", sessionId, sessionState, session); logger.info("joining core session({}) state({}): {}", sessionId, sessionState, session);
for (CoreNode node : session.getNodes()) { for (CoreNode node : session.getNodes()) {
if (node.getModel() == null) { if (node.getModel() == null) {
logger.info("skipping joined session node: {}", node.getName());
continue; continue;
} }
NodeType nodeType = NodeType.getNodeType(node.getNodeTypeKey()); NodeType nodeType = NodeType.getNodeType(node.getNodeTypeKey());
node.setIcon(nodeType.getIcon()); node.setIcon(nodeType.getIcon());
networkGraph.addNode(node); networkGraph.addNode(node);
@ -220,4 +221,16 @@ public class CoreClient {
public String getTerminalCommand(CoreNode node) throws IOException { public String getTerminalCommand(CoreNode node) throws IOException {
return coreApi.getTerminalCommand(sessionId, node); return coreApi.getTerminalCommand(sessionId, node);
} }
public boolean setMobilityConfig(CoreNode node, MobilityConfig config) throws IOException {
return coreApi.setMobilityConfig(sessionId, node, config);
}
public MobilityConfig getMobilityConfig(CoreNode node) throws IOException {
return coreApi.getMobilityConfig(sessionId, node);
}
public boolean mobilityAction(CoreNode node, String action) throws IOException {
return coreApi.mobilityAction(sessionId, node, action);
}
} }

View file

@ -0,0 +1,20 @@
package com.core.data;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class MobilityConfig {
private String file;
@JsonProperty("refresh_ms")
private Integer refresh;
private String loop;
private String autostart;
private String map;
@JsonProperty("script_start")
private String startScript;
@JsonProperty("script_pause")
private String pauseScript;
@JsonProperty("script_stop")
private String stopScript;
}

View file

@ -1,6 +1,8 @@
package com.core.data; package com.core.data;
import lombok.Data; import lombok.Data;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
@ -8,6 +10,7 @@ import java.util.Map;
@Data @Data
public class NodeType { public class NodeType {
private static final Logger logger = LogManager.getLogger();
public static final int DEFAULT = 0; public static final int DEFAULT = 0;
public static final int SWITCH = 4; public static final int SWITCH = 4;
public static final int HUB = 5; public static final int HUB = 5;
@ -36,7 +39,7 @@ public class NodeType {
addNodeType(new NodeType(DEFAULT, "mdr", "MDR", "/icons/router-100.png")); addNodeType(new NodeType(DEFAULT, "mdr", "MDR", "/icons/router-100.png"));
addNodeType(new NodeType(SWITCH, "Switch", "/icons/switch-100.png")); addNodeType(new NodeType(SWITCH, "Switch", "/icons/switch-100.png"));
addNodeType(new NodeType(HUB, "Hub", "/icons/hub-100.png")); addNodeType(new NodeType(HUB, "Hub", "/icons/hub-100.png"));
addNodeType(new NodeType(WLAN, "WLAN", "/icons/wlan-100.png")); addNodeType(new NodeType(WLAN, "wlan", "WLAN", "/icons/wlan-100.png"));
addNodeType(new NodeType(EMANE, "EMANE", "/icons/emane-100.png")); addNodeType(new NodeType(EMANE, "EMANE", "/icons/emane-100.png"));
DISPLAY_MAP.put(HUB, "Hub"); DISPLAY_MAP.put(HUB, "Hub");

View file

@ -82,6 +82,8 @@ public class CorePopupGraphMousePlugin<V, E> extends EditingPopupGraphMousePlugi
case NodeType.WLAN: case NodeType.WLAN:
menuItems.add(createMenuItem("WLAN Settings", menuItems.add(createMenuItem("WLAN Settings",
event -> controller.getNodeWlanDialog().showDialog(node))); event -> controller.getNodeWlanDialog().showDialog(node)));
menuItems.add(createMenuItem("Mobility",
event -> controller.getMobilityDialog().showDialog(node)));
menuItems.add(createMenuItem("Link MDRs", menuItems.add(createMenuItem("Link MDRs",
event -> networkGraph.linkMdrs(node))); event -> networkGraph.linkMdrs(node)));
break; break;

View file

@ -271,6 +271,19 @@ public class NetworkGraph {
nodeMap.put(node.getId(), node); nodeMap.put(node.getId(), node);
} }
public void setNodeLocation(CoreNode nodeData) {
// update actual graph node
CoreNode node = nodeMap.get(nodeData.getId());
node.getPosition().setX(nodeData.getPosition().getX());
node.getPosition().setY(nodeData.getPosition().getY());
// set graph node location
Double x = Math.abs(node.getPosition().getX());
Double y = Math.abs(node.getPosition().getY());
graphLayout.setLocation(node, x, y);
graphViewer.repaint();
}
public void removeNode(CoreNode node) { public void removeNode(CoreNode node) {
try { try {
controller.getCoreClient().deleteNode(node); controller.getCoreClient().deleteNode(node);

View file

@ -170,6 +170,22 @@ public class CoreApi {
return WebUtils.putJson(url, jsonData); return WebUtils.putJson(url, jsonData);
} }
public boolean setMobilityConfig(Integer session, CoreNode node, MobilityConfig config) throws IOException {
String url = getUrl(String.format("sessions/%s/nodes/%s/mobility", session, node.getId()));
String data = JsonUtils.toString(config);
return WebUtils.postJson(url, data);
}
public MobilityConfig getMobilityConfig(Integer session, CoreNode node) throws IOException {
String url = getUrl(String.format("sessions/%s/nodes/%s/mobility", session, node.getId()));
return WebUtils.getJson(url, MobilityConfig.class);
}
public boolean mobilityAction(Integer session, CoreNode node, String action) throws IOException {
String url = getUrl(String.format("sessions/%s/nodes/%s/mobility/%s", session, node.getId(), action));
return WebUtils.putJson(url, null);
}
public String getTerminalCommand(Integer session, CoreNode node) throws IOException { public String getTerminalCommand(Integer session, CoreNode node) throws IOException {
String url = getUrl(String.format("sessions/%s/nodes/%s/terminal", session, node.getId())); String url = getUrl(String.format("sessions/%s/nodes/%s/terminal", session, node.getId()));
return WebUtils.getJson(url, String.class); return WebUtils.getJson(url, String.class);

View file

@ -0,0 +1,117 @@
package com.core.ui;
import com.core.Controller;
import com.core.data.CoreNode;
import com.core.data.MobilityConfig;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.controls.JFXToggleButton;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.layout.GridPane;
import javafx.stage.FileChooser;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.IOException;
public class MobilityDialog extends StageDialog {
private static final Logger logger = LogManager.getLogger();
@FXML
private JFXTextField fileTextField;
@FXML
private JFXTextField refreshTextField;
@FXML
private JFXToggleButton loopToggleButton;
@FXML
private JFXTextField nodeMappingTextField;
@FXML
private JFXTextField autoStartTextField;
@FXML
private JFXTextField startTextField;
@FXML
private JFXTextField pauseTextField;
@FXML
private JFXTextField stopTextField;
private CoreNode node;
public MobilityDialog(Controller controller) {
super(controller, "/fxml/mobility_dialog.fxml");
setTitle("Mobility Script");
JFXButton saveButton = createButton("Save");
saveButton.setOnAction(event -> {
MobilityConfig mobilityConfig = new MobilityConfig();
mobilityConfig.setFile(fileTextField.getText());
mobilityConfig.setAutostart(autoStartTextField.getText());
String loop = loopToggleButton.isSelected() ? "1" : "";
mobilityConfig.setLoop(loop);
mobilityConfig.setRefresh(Integer.parseInt(refreshTextField.getText()));
mobilityConfig.setMap(nodeMappingTextField.getText());
mobilityConfig.setStartScript(startTextField.getText());
mobilityConfig.setPauseScript(pauseTextField.getText());
mobilityConfig.setStopScript(stopTextField.getText());
try {
controller.getCoreClient().setMobilityConfig(node, mobilityConfig);
} catch (IOException ex) {
logger.error("error setting mobility configuration", ex);
Toast.error("error setting mobility configuration");
}
close();
});
addCancelButton();
}
@FXML
private void onSelectAction(ActionEvent event) {
JFXButton button = (JFXButton) event.getSource();
GridPane gridPane = (GridPane) button.getParent();
JFXTextField textField = (JFXTextField) gridPane.getChildren().get(0);
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Select File");
fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Mobility",
"*.mobility"));
File file = fileChooser.showOpenDialog(getController().getWindow());
if (file != null) {
logger.info("opening session xml: {}", file.getPath());
textField.setText(file.getPath());
}
}
public void showDialog(CoreNode node) {
this.node = node;
try {
MobilityConfig mobilityConfig = getController().getCoreClient().getMobilityConfig(this.node);
fileTextField.setText(mobilityConfig.getFile());
autoStartTextField.setText(mobilityConfig.getAutostart());
boolean loop = "1".equals(mobilityConfig.getLoop());
loopToggleButton.setSelected(loop);
refreshTextField.setText(mobilityConfig.getRefresh().toString());
nodeMappingTextField.setText(mobilityConfig.getMap());
startTextField.setText(mobilityConfig.getStartScript());
pauseTextField.setText(mobilityConfig.getPauseScript());
stopTextField.setText(mobilityConfig.getStopScript());
} catch (IOException ex) {
logger.error("error getting mobility config", ex);
Toast.error("error getting mobility config");
}
show();
}
}

View file

@ -2,6 +2,7 @@ package com.core.websocket;
import com.core.Controller; import com.core.Controller;
import com.core.data.CoreEvent; import com.core.data.CoreEvent;
import com.core.data.CoreNode;
import com.core.data.SessionState; import com.core.data.SessionState;
import com.core.utils.JsonUtils; import com.core.utils.JsonUtils;
import io.socket.client.IO; import io.socket.client.IO;
@ -26,6 +27,17 @@ public class CoreWebSocket {
socket.on(Socket.EVENT_CONNECT, args -> { socket.on(Socket.EVENT_CONNECT, args -> {
logger.info("connected to web socket"); logger.info("connected to web socket");
}); });
socket.on("node", args -> {
for (Object arg : args) {
try {
CoreNode node = JsonUtils.read(arg.toString(), CoreNode.class);
logger.info("core node update: {}", node);
controller.getNetworkGraph().setNodeLocation(node);
} catch (IOException ex) {
logger.error("error getting core node", ex);
}
}
});
socket.on("event", args -> { socket.on("event", args -> {
for (Object arg : args) { for (Object arg : args) {
try { try {

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.JFXButton?>
<?import com.jfoenix.controls.JFXTextField?>
<?import com.jfoenix.controls.JFXToggleButton?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<GridPane hgap="10.0" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" vgap="10.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" percentWidth="40.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
</rowConstraints>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
<children>
<Label text="File" />
<Label text="Refresh Rate (ms)" GridPane.rowIndex="1" />
<Label text="Auto-Start (0.0 for runtime)" GridPane.rowIndex="3" />
<Label text="Node Mapping (optional, e.g. 0:1,1:2,etc)" GridPane.rowIndex="4" />
<Label text="Start Script" GridPane.rowIndex="5" />
<Label text="Pause Script" GridPane.rowIndex="6" />
<Label text="Stop Script" GridPane.rowIndex="7" />
<JFXTextField fx:id="refreshTextField" GridPane.columnIndex="1" GridPane.rowIndex="1" />
<JFXTextField fx:id="autoStartTextField" GridPane.columnIndex="1" GridPane.rowIndex="3" />
<JFXTextField fx:id="nodeMappingTextField" GridPane.columnIndex="1" GridPane.rowIndex="4" />
<JFXToggleButton fx:id="loopToggleButton" contentDisplay="GRAPHIC_ONLY" maxWidth="1.7976931348623157E308" text="Loop?" GridPane.columnIndex="1" GridPane.columnSpan="2147483647" GridPane.halignment="CENTER" GridPane.rowIndex="2" />
<Label text="Loop?" GridPane.rowIndex="2" />
<GridPane hgap="5.0" GridPane.columnIndex="1">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" percentWidth="80.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<JFXTextField fx:id="fileTextField" />
<JFXButton maxWidth="1.7976931348623157E308" onAction="#onSelectAction" styleClass="core-button" text="Select" GridPane.columnIndex="1" />
</children>
</GridPane>
<GridPane hgap="5.0" GridPane.columnIndex="1" GridPane.rowIndex="6">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" percentWidth="80.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<JFXTextField fx:id="pauseTextField" />
<JFXButton maxWidth="1.7976931348623157E308" onAction="#onSelectAction" styleClass="core-button" text="Select" GridPane.columnIndex="1" />
</children>
</GridPane>
<GridPane hgap="5.0" GridPane.columnIndex="1" GridPane.rowIndex="7">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" percentWidth="80.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<JFXTextField fx:id="stopTextField" />
<JFXButton maxWidth="1.7976931348623157E308" onAction="#onSelectAction" styleClass="core-button" text="Select" GridPane.columnIndex="1" />
</children>
</GridPane>
<GridPane hgap="5.0" GridPane.columnIndex="1" GridPane.rowIndex="5">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" percentWidth="80.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<JFXTextField fx:id="startTextField" />
<JFXButton maxWidth="1.7976931348623157E308" onAction="#onSelectAction" styleClass="core-button" text="Select" GridPane.columnIndex="1" />
</children>
</GridPane>
</children>
</GridPane>

View file

@ -11,6 +11,7 @@ from flask import send_file
from flask_socketio import SocketIO from flask_socketio import SocketIO
from flask_socketio import emit from flask_socketio import emit
import mobility_routes
from core import logger from core import logger
from core.emulator.coreemu import CoreEmu from core.emulator.coreemu import CoreEmu
from core.emulator.emudata import InterfaceData from core.emulator.emudata import InterfaceData
@ -25,12 +26,14 @@ from core.mobility import BasicRangeModel
from core.service import ServiceManager from core.service import ServiceManager
CORE_LOCK = Lock() CORE_LOCK = Lock()
coreemu = CoreEmu()
app = Flask(__name__) app = Flask(__name__)
app.config["SECRET_KEY"] = "core"
socketio = SocketIO(app) socketio = SocketIO(app)
app.config["SECRET_KEY"] = "core"
coreemu = CoreEmu() mobility_routes.coreemu = coreemu
app.register_blueprint(mobility_routes.mobility_api, url_prefix="/sessions/<int:session_id>")
def synchronized(function): def synchronized(function):
@ -120,19 +123,22 @@ def broadcast_event(event):
}) })
def broadcast_node(node):
socketio.emit("node", {
"id": node.id,
"name": node.name,
"model": node.model,
"position": {
"x": node.x_position,
"y": node.y_position,
},
"services": node.services.split("|"),
})
@socketio.on("connect") @socketio.on("connect")
def websocket_connect(): def websocket_connect():
emit("info", {"message": "You are connected!"}) emit("info", {"message": "You are connected!"})
socketio.emit("node", {
"id": 1,
"x": 100,
"y": 101
})
socketio.emit("node", {
"id": 1,
"x": 100,
"y": 150
})
@socketio.on("disconnect") @socketio.on("disconnect")
@ -142,7 +148,7 @@ def websocket_disconnect():
@app.route("/") @app.route("/")
def home(): def home():
return render_template('index.html') return render_template("index.html")
@app.route("/ips", methods=["POST"]) @app.route("/ips", methods=["POST"])
@ -233,6 +239,7 @@ def create_session():
# add handlers # add handlers
session.event_handlers.append(broadcast_event) session.event_handlers.append(broadcast_event)
session.node_handlers.append(broadcast_node)
response_data = jsonify( response_data = jsonify(
id=session.session_id, id=session.session_id,

63
webapp/mobility_routes.py Normal file
View file

@ -0,0 +1,63 @@
from flask import Blueprint
from flask import jsonify
from flask import request
from core.mobility import Ns2ScriptedMobility
mobility_api = Blueprint("mobility_api", __name__)
coreemu = None
@mobility_api.route("/nodes/<node_id>/mobility", methods=["POST"])
def set_mobility_config(session_id, node_id):
session = coreemu.sessions.get(session_id)
if not session:
return jsonify(error="session does not exist"), 404
if node_id.isdigit():
node_id = int(node_id)
data = request.get_json() or {}
session.mobility.set_model_config(node_id, Ns2ScriptedMobility.name, data)
return jsonify()
@mobility_api.route("/nodes/<node_id>/mobility")
def get_mobility_config(session_id, node_id):
session = coreemu.sessions.get(session_id)
if not session:
return jsonify(error="session does not exist"), 404
if node_id.isdigit():
node_id = int(node_id)
config = session.mobility.get_model_config(node_id, Ns2ScriptedMobility.name)
return jsonify(config)
@mobility_api.route("/nodes/<node_id>/mobility/<action>", methods=["PUT"])
def mobility_action(session_id, node_id, action):
session = coreemu.sessions.get(session_id)
if not session:
return jsonify(error="session does not exist"), 404
if node_id.isdigit():
node_id = int(node_id)
node = session.objects.get(node_id)
if not node:
return jsonify(error="node does not exist"), 404
if action == "start":
node.mobility.start()
elif action == "pause":
node.mobility.pause()
elif action == "stop":
node.mobility.stop(move_initial=True)
else:
return jsonify(error="invalid mobility action: %s" % action), 404
return jsonify()