mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-27 02:01:40 +00:00
Refined network management (#1672)
This PR implements several refinements to the way that NetworkManager controls the network interface. - The monitor detects and logs changes to the network address - The monitor detects and logs changes to the connection and will reinitialize the connection if needed - Remove NetworkInterface.java class, which wasn't used anywhere - Use java.net.NetworkInterface to get IP addresses for any interface (device) - Adds a metric for the current IP address (address on the currently selected interface)
This commit is contained in:
@@ -242,7 +242,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<template #activator="{ on, attrs }">
|
||||
<td v-bind="attrs" v-on="on" @click="setSelectedVideoFormat(value)">
|
||||
<v-icon small class="mr-2">mdi-information</v-icon>
|
||||
</td>
|
||||
|
||||
@@ -9,24 +9,36 @@ interface MetricItem {
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const generalMetrics = computed<MetricItem[]>(() => [
|
||||
{
|
||||
header: "Version",
|
||||
value: useSettingsStore().general.version || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "Hardware Model",
|
||||
value: useSettingsStore().general.hardwareModel || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "Platform",
|
||||
value: useSettingsStore().general.hardwarePlatform || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "GPU Acceleration",
|
||||
value: useSettingsStore().general.gpuAcceleration || "Unknown"
|
||||
const generalMetrics = computed<MetricItem[]>(() => {
|
||||
const stats = [
|
||||
{
|
||||
header: "Version",
|
||||
value: useSettingsStore().general.version || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "Hardware Model",
|
||||
value: useSettingsStore().general.hardwareModel || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "Platform",
|
||||
value: useSettingsStore().general.hardwarePlatform || "Unknown"
|
||||
},
|
||||
|
||||
{
|
||||
header: "GPU Acceleration",
|
||||
value: useSettingsStore().general.gpuAcceleration || "Unknown"
|
||||
}
|
||||
];
|
||||
|
||||
if (!useSettingsStore().network.networkingDisabled) {
|
||||
stats.push({
|
||||
header: "IP Address",
|
||||
value: useSettingsStore().metrics.ipAddress || "Unknown"
|
||||
});
|
||||
}
|
||||
]);
|
||||
|
||||
return stats;
|
||||
});
|
||||
|
||||
const platformMetrics = computed<MetricItem[]>(() => {
|
||||
const stats = [
|
||||
|
||||
@@ -62,7 +62,8 @@ export const useSettingsStore = defineStore("settings", {
|
||||
cpuThr: undefined,
|
||||
cpuUptime: undefined,
|
||||
diskUtilPct: undefined,
|
||||
npuUsage: undefined
|
||||
npuUsage: undefined,
|
||||
ipAddress: undefined
|
||||
},
|
||||
currentFieldLayout: {
|
||||
field: {
|
||||
@@ -95,7 +96,8 @@ export const useSettingsStore = defineStore("settings", {
|
||||
cpuThr: data.cpuThr || undefined,
|
||||
cpuUptime: data.cpuUptime || undefined,
|
||||
diskUtilPct: data.diskUtilPct || undefined,
|
||||
npuUsage: data.npuUsage || undefined
|
||||
npuUsage: data.npuUsage || undefined,
|
||||
ipAddress: data.ipAddress || undefined
|
||||
};
|
||||
},
|
||||
updateGeneralSettingsFromWebsocket(data: WebsocketSettingsUpdate) {
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface MetricData {
|
||||
cpuUptime?: string;
|
||||
diskUtilPct?: string;
|
||||
npuUsage?: string;
|
||||
ipAddress?: string;
|
||||
}
|
||||
|
||||
export enum NetworkConnectionType {
|
||||
|
||||
@@ -167,11 +167,11 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
<v-row>
|
||||
<!-- Active modules -->
|
||||
<v-col
|
||||
v-for="(module, index) in activeVisionModules"
|
||||
:key="`enabled-${module.uniqueName}`"
|
||||
cols="12"
|
||||
sm="6"
|
||||
lg="4"
|
||||
v-for="(module, index) in activeVisionModules"
|
||||
:key="`enabled-${module.uniqueName}`"
|
||||
>
|
||||
<v-card dark color="primary">
|
||||
<v-card-title>{{ module.nickname }}</v-card-title>
|
||||
@@ -223,8 +223,8 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
<photon-camera-stream
|
||||
class="mt-3"
|
||||
id="output-camera-stream"
|
||||
class="mt-3"
|
||||
:camera-settings="module"
|
||||
stream-type="Processed"
|
||||
style="width: 100%; height: auto"
|
||||
@@ -233,16 +233,16 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
<v-card-text class="pt-0">
|
||||
<v-row>
|
||||
<v-col cols="12" md="4" class="pr-md-0 pb-0 pb-md-3">
|
||||
<v-btn color="secondary" @click="setCameraView(module.matchedCameraInfo, true)" style="width: 100%">
|
||||
<v-btn color="secondary" style="width: 100%" @click="setCameraView(module.matchedCameraInfo, true)">
|
||||
<span>Details</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" md="5" class="pr-0">
|
||||
<v-btn
|
||||
class="black--text"
|
||||
@click="deactivateCamera(module.uniqueName)"
|
||||
color="accent"
|
||||
style="width: 100%"
|
||||
@click="deactivateCamera(module.uniqueName)"
|
||||
>
|
||||
Deactivate
|
||||
</v-btn>
|
||||
@@ -250,9 +250,9 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
<v-col cols="6" md="3">
|
||||
<v-btn
|
||||
class="black--text pa-0"
|
||||
@click="deleteThisCamera(module.uniqueName)"
|
||||
color="red"
|
||||
style="width: 100%"
|
||||
@click="deleteThisCamera(module.uniqueName)"
|
||||
>
|
||||
<v-icon>mdi-trash-can-outline</v-icon>
|
||||
</v-btn>
|
||||
@@ -263,7 +263,7 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
</v-col>
|
||||
|
||||
<!-- Disabled modules -->
|
||||
<v-col cols="12" sm="6" lg="4" v-for="module in disabledVisionModules" :key="`disabled-${module.uniqueName}`">
|
||||
<v-col v-for="module in disabledVisionModules" :key="`disabled-${module.uniqueName}`" cols="12" sm="6" lg="4">
|
||||
<v-card dark color="primary">
|
||||
<v-card-title>{{ module.nickname }}</v-card-title>
|
||||
<v-card-subtitle>Status: <span class="inactive-status">Deactivated</span></v-card-subtitle>
|
||||
@@ -299,16 +299,16 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
<v-card-text class="pt-0">
|
||||
<v-row>
|
||||
<v-col cols="12" md="4" class="pr-md-0 pb-0 pb-md-3">
|
||||
<v-btn color="secondary" @click="setCameraView(module.matchedCameraInfo)" style="width: 100%">
|
||||
<v-btn color="secondary" style="width: 100%" @click="setCameraView(module.matchedCameraInfo)">
|
||||
<span>Details</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" md="5" class="pr-0">
|
||||
<v-btn
|
||||
class="black--text"
|
||||
@click="activateModule(module.uniqueName)"
|
||||
color="accent"
|
||||
style="width: 100%"
|
||||
@click="activateModule(module.uniqueName)"
|
||||
>
|
||||
Activate
|
||||
</v-btn>
|
||||
@@ -316,9 +316,9 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
<v-col cols="6" md="3">
|
||||
<v-btn
|
||||
class="black--text pa-0"
|
||||
@click="deleteThisCamera(module.uniqueName)"
|
||||
color="red"
|
||||
style="width: 100%"
|
||||
@click="deleteThisCamera(module.uniqueName)"
|
||||
>
|
||||
<v-icon>mdi-trash-can-outline</v-icon>
|
||||
</v-btn>
|
||||
@@ -329,7 +329,7 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
</v-col>
|
||||
|
||||
<!-- Unassigned cameras -->
|
||||
<v-col cols="12" sm="6" lg="4" v-for="(camera, index) in unmatchedCameras" :key="index">
|
||||
<v-col v-for="(camera, index) in unmatchedCameras" :key="index" cols="12" sm="6" lg="4">
|
||||
<v-card dark color="primary">
|
||||
<v-card-title>
|
||||
<span v-if="camera.PVUsbCameraInfo">USB Camera:</span>
|
||||
@@ -345,12 +345,12 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
<v-card-text class="pt-0">
|
||||
<v-row>
|
||||
<v-col cols="6" class="pr-0">
|
||||
<v-btn color="secondary" @click="setCameraView(camera)" style="width: 100%">
|
||||
<v-btn color="secondary" style="width: 100%" @click="setCameraView(camera)">
|
||||
<span>Details</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-btn class="black--text" @click="activateCamera(camera)" color="accent" style="width: 100%">
|
||||
<v-btn class="black--text" color="accent" style="width: 100%" @click="activateCamera(camera)">
|
||||
Activate
|
||||
</v-btn>
|
||||
</v-col>
|
||||
@@ -377,7 +377,7 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
|
||||
<!-- Camera details modal -->
|
||||
<v-dialog v-model="viewingDetails">
|
||||
<v-card dark flat color="primary" v-if="viewingCamera !== null">
|
||||
<v-card v-if="viewingCamera !== null" dark flat color="primary">
|
||||
<v-card-title class="d-flex justify-space-between">
|
||||
<span>{{ cameraInfoFor(viewingCamera)?.name ?? cameraInfoFor(viewingCamera)?.baseName }}</span>
|
||||
<v-btn text @click="setCameraView(null)">
|
||||
@@ -398,10 +398,10 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
|
||||
</v-banner>
|
||||
<div v-if="showCurrentView">
|
||||
<h3>Saved camera</h3>
|
||||
<PvCameraInfoCard :camera="viewingCamera" :showTitle="false" />
|
||||
<PvCameraInfoCard :camera="viewingCamera" :show-title="false" />
|
||||
<br />
|
||||
<h3>Current camera</h3>
|
||||
<PvCameraInfoCard :camera="getMatchedDevice(viewingCamera)" :showTitle="false" />
|
||||
<PvCameraInfoCard :camera="getMatchedDevice(viewingCamera)" :show-title="false" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<PvCameraInfoCard :camera="viewingCamera" />
|
||||
|
||||
@@ -62,8 +62,8 @@ const arducamWarningShown = computed<boolean>(() => {
|
||||
<template>
|
||||
<v-container class="pa-3" fluid>
|
||||
<v-banner
|
||||
v-model="arducamWarningShown"
|
||||
v-if="arducamWarningShown"
|
||||
v-model="arducamWarningShown"
|
||||
rounded
|
||||
color="red"
|
||||
dark
|
||||
@@ -86,7 +86,7 @@ const arducamWarningShown = computed<boolean>(() => {
|
||||
<PipelineConfigCard />
|
||||
|
||||
<!-- TODO - not sure this belongs here -->
|
||||
<v-dialog :persistent="false" v-model="warningShown" v-if="warningShown" max-width="800" dark>
|
||||
<v-dialog v-if="warningShown" v-model="warningShown" :persistent="false" max-width="800" dark>
|
||||
<v-card dark flat color="primary">
|
||||
<v-card-title>Setup some cameras to get started!</v-card-title>
|
||||
<v-card-text>
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.photonvision.common.hardware.metrics;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.HashMap;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.configuration.HardwareConfig;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
@@ -31,6 +32,7 @@ import org.photonvision.common.hardware.metrics.cmds.PiCmds;
|
||||
import org.photonvision.common.hardware.metrics.cmds.RK3588Cmds;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.networking.NetworkUtils;
|
||||
import org.photonvision.common.util.ShellExec;
|
||||
|
||||
public class MetricsManager {
|
||||
@@ -119,6 +121,14 @@ public class MetricsManager {
|
||||
return safeExecute(cmds.ramUsageCommand);
|
||||
}
|
||||
|
||||
public String getIpAddress() {
|
||||
String dev = ConfigManager.getInstance().getConfig().getNetworkConfig().networkManagerIface;
|
||||
logger.debug("Requesting IP addresses for \"" + dev + "\"");
|
||||
String addr = NetworkUtils.getIPAddresses(dev);
|
||||
logger.debug("Got value \"" + addr + "\"");
|
||||
return addr;
|
||||
}
|
||||
|
||||
public void publishMetrics() {
|
||||
logger.debug("Publishing Metrics...");
|
||||
final var metrics = new HashMap<String, String>();
|
||||
@@ -133,6 +143,7 @@ public class MetricsManager {
|
||||
metrics.put("gpuMemUtil", this.getMallocedMemory());
|
||||
metrics.put("diskUtilPct", this.getUsedDiskPct());
|
||||
metrics.put("npuUsage", this.getNpuUsage());
|
||||
metrics.put("ipAddress", this.getIpAddress());
|
||||
|
||||
DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics));
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.networking;
|
||||
|
||||
import java.net.InterfaceAddress;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class NetworkInterface {
|
||||
private static final Logger logger = new Logger(NetworkInterface.class, LogGroup.General);
|
||||
|
||||
public final String name;
|
||||
public final String displayName;
|
||||
public final String ipAddress;
|
||||
public final String netmask;
|
||||
public final String broadcast;
|
||||
|
||||
public NetworkInterface(java.net.NetworkInterface inetface, InterfaceAddress ifaceAddress) {
|
||||
name = inetface.getName();
|
||||
displayName = inetface.getDisplayName();
|
||||
|
||||
var inetAddress = ifaceAddress.getAddress();
|
||||
ipAddress = inetAddress.getHostAddress();
|
||||
netmask = getIPv4LocalNetMask(ifaceAddress);
|
||||
|
||||
// TODO: (low) hack to "get" gateway, this is gross and bad, pls fix
|
||||
var splitIPAddr = ipAddress.split("\\.");
|
||||
splitIPAddr[3] = "1";
|
||||
splitIPAddr[3] = "255";
|
||||
broadcast = String.join(".", splitIPAddr);
|
||||
}
|
||||
|
||||
private static String getIPv4LocalNetMask(InterfaceAddress interfaceAddress) {
|
||||
var netPrefix = interfaceAddress.getNetworkPrefixLength();
|
||||
try {
|
||||
// Since this is for IPv4, it's 32 bits, so set the sign value of
|
||||
// the int to "negative"...
|
||||
int shiftby = (1 << 31);
|
||||
// For the number of bits of the prefix -1 (we already set the sign bit)
|
||||
for (int i = netPrefix - 1; i > 0; i--) {
|
||||
// Shift the sign right... Java makes the sign bit sticky on a shift...
|
||||
// So no need to "set it back up"...
|
||||
shiftby = (shiftby >> 1);
|
||||
}
|
||||
// Transform the resulting value in xxx.xxx.xxx.xxx format, like if
|
||||
/// it was a standard address...
|
||||
// Return the address thus created...
|
||||
return ((shiftby >> 24) & 255)
|
||||
+ "."
|
||||
+ ((shiftby >> 16) & 255)
|
||||
+ "."
|
||||
+ ((shiftby >> 8) & 255)
|
||||
+ "."
|
||||
+ (shiftby & 255);
|
||||
// return InetAddress.getByName(maskString);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to get netmask!", e);
|
||||
}
|
||||
// Something went wrong here...
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,12 @@
|
||||
|
||||
package org.photonvision.common.networking;
|
||||
|
||||
import java.net.NetworkInterface;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.configuration.NetworkConfig;
|
||||
import org.photonvision.common.dataflow.DataChangeDestination;
|
||||
@@ -38,6 +39,7 @@ import org.photonvision.common.util.TimedTaskManager;
|
||||
|
||||
public class NetworkManager {
|
||||
private static final Logger logger = new Logger(NetworkManager.class, LogGroup.General);
|
||||
private HashMap<String, String> activeConnections = new HashMap<String, String>();
|
||||
|
||||
private NetworkManager() {}
|
||||
|
||||
@@ -61,17 +63,21 @@ public class NetworkManager {
|
||||
|
||||
if (!Platform.isLinux()) {
|
||||
logger.info("Not managing network on non-Linux platforms.");
|
||||
this.networkingIsDisabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PlatformUtils.isRoot()) {
|
||||
logger.error("Cannot manage network without root!");
|
||||
this.networkingIsDisabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Start tasks to monitor the network interface(s)
|
||||
var ethernetDevices = NetworkUtils.getAllWiredInterfaces();
|
||||
for (NMDeviceInfo deviceInfo : ethernetDevices) {
|
||||
activeConnections.put(
|
||||
deviceInfo.devName, NetworkUtils.getActiveConnection(deviceInfo.devName));
|
||||
monitorDevice(deviceInfo.devName, 5000);
|
||||
}
|
||||
|
||||
@@ -81,7 +87,9 @@ public class NetworkManager {
|
||||
try {
|
||||
// if the configured interface isn't in the list of available ones, select one that is
|
||||
var iFace = physicalDevices.stream().findFirst().orElseThrow();
|
||||
logger.warn("The configured interface doesn't match any available interface. Applying configuration to " + iFace.devName);
|
||||
logger.warn(
|
||||
"The configured interface doesn't match any available interface. Applying configuration to "
|
||||
+ iFace.devName);
|
||||
// update NetworkConfig with found interface
|
||||
config.networkManagerIface = iFace.devName;
|
||||
ConfigManager.getInstance().requestSave();
|
||||
@@ -96,7 +104,13 @@ public class NetworkManager {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Setting " + config.connectionType + " with team " + config.ntServerAddress + " on " + config.networkManagerIface);
|
||||
logger.info(
|
||||
"Setting "
|
||||
+ config.connectionType
|
||||
+ " with team "
|
||||
+ config.ntServerAddress
|
||||
+ " on "
|
||||
+ config.networkManagerIface);
|
||||
|
||||
// always set hostname (unless it's blank)
|
||||
if (!config.hostname.isBlank()) {
|
||||
@@ -129,15 +143,14 @@ public class NetworkManager {
|
||||
var shell = new ShellExec(true, false);
|
||||
shell.executeBashCommand("cat /etc/hostname | tr -d \" \\t\\n\\r\"");
|
||||
var oldHostname = shell.getOutput().replace("\n", "");
|
||||
logger.debug("Old host name: >" + oldHostname +"<");
|
||||
logger.debug("New host name: >" + hostname +"<");
|
||||
logger.debug("Old host name: \"" + oldHostname + "\"");
|
||||
logger.debug("New host name: \"" + hostname + "\"");
|
||||
|
||||
if (!oldHostname.equals(hostname)) {
|
||||
var setHostnameRetCode =
|
||||
shell.executeBashCommand(
|
||||
"echo $NEW_HOSTNAME > /etc/hostname".replace("$NEW_HOSTNAME", hostname));
|
||||
setHostnameRetCode =
|
||||
shell.executeBashCommand("hostnamectl set-hostname " + hostname);
|
||||
setHostnameRetCode = shell.executeBashCommand("hostnamectl set-hostname " + hostname);
|
||||
|
||||
// Add to /etc/hosts
|
||||
var addHostRetCode =
|
||||
@@ -168,31 +181,23 @@ public class NetworkManager {
|
||||
private void setConnectionDHCP(NetworkConfig config) {
|
||||
String connName = "dhcp-" + config.networkManagerIface;
|
||||
|
||||
String addDHCPcommand = """
|
||||
nmcli connection add
|
||||
con-name "${connection}"
|
||||
ifname "${interface}"
|
||||
type ethernet
|
||||
autoconnect no
|
||||
ipv4.method auto
|
||||
ipv6.method disabled
|
||||
""";
|
||||
addDHCPcommand = addDHCPcommand.replaceAll("[\\n]", " ");
|
||||
|
||||
var shell = new ShellExec();
|
||||
try {
|
||||
if (NetworkUtils.connDoesNotExist(connName)) {
|
||||
// create connection
|
||||
logger.info("Creating the DHCP connection " + connName );
|
||||
logger.info("Creating DHCP connection " + connName);
|
||||
shell.executeBashCommand(
|
||||
addDHCPcommand
|
||||
.replace("${connection}", connName)
|
||||
.replace("${interface}", config.networkManagerIface)
|
||||
);
|
||||
NetworkingCommands.addConnectionCommand
|
||||
.replace("${connection}", connName)
|
||||
.replace("${interface}", config.networkManagerIface));
|
||||
}
|
||||
logger.info("Updating the DHCP connection " + connName);
|
||||
shell.executeBashCommand(
|
||||
NetworkingCommands.modDHCPCommand.replace("${connection}", connName));
|
||||
// activate it
|
||||
logger.info("Activating the DHCP connection " + connName );
|
||||
shell.executeBashCommand("nmcli connection up \"${connection}\"".replace("${connection}", connName), false);
|
||||
logger.info("Activating DHCP connection " + connName);
|
||||
shell.executeBashCommand(
|
||||
"nmcli connection up \"${connection}\"".replace("${connection}", connName), false);
|
||||
activeConnections.put(config.networkManagerIface, connName);
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception while setting DHCP!", e);
|
||||
}
|
||||
@@ -200,20 +205,6 @@ public class NetworkManager {
|
||||
|
||||
private void setConnectionStatic(NetworkConfig config) {
|
||||
String connName = "static-" + config.networkManagerIface;
|
||||
String addStaticCommand = """
|
||||
nmcli connection add
|
||||
con-name "${connection}"
|
||||
ifname "${interface}"
|
||||
type ethernet
|
||||
autoconnect no
|
||||
ipv4.addresses ${ipaddr}/8
|
||||
ipv4.gateway ${gateway}
|
||||
ipv4.method "manual"
|
||||
ipv6.method "disabled"
|
||||
""";
|
||||
addStaticCommand = addStaticCommand.replaceAll("[\\n]", " ");
|
||||
|
||||
String modStaticCommand = "nmcli connection mod \"${connection}\" ipv4.addresses ${ipaddr}/8 ipv4.gateway ${gateway}";
|
||||
|
||||
if (config.staticIp.isBlank()) {
|
||||
logger.warn("Got empty static IP?");
|
||||
@@ -222,34 +213,31 @@ public class NetworkManager {
|
||||
|
||||
// guess at the gateway from the staticIp
|
||||
String[] parts = config.staticIp.split("\\.");
|
||||
parts[parts.length-1] = "1";
|
||||
parts[parts.length - 1] = "1";
|
||||
String gateway = String.join(".", parts);
|
||||
|
||||
var shell = new ShellExec();
|
||||
try {
|
||||
if (NetworkUtils.connDoesNotExist(connName)) {
|
||||
// create connection
|
||||
logger.info("Creating the Static connection " + connName );
|
||||
logger.info("Creating Static connection " + connName);
|
||||
shell.executeBashCommand(
|
||||
addStaticCommand
|
||||
.replace("${connection}", connName)
|
||||
.replace("${interface}", config.networkManagerIface)
|
||||
.replace("${ipaddr}", config.staticIp)
|
||||
.replace("${gateway}", gateway)
|
||||
);
|
||||
} else {
|
||||
// modify it in case the static IP address is different
|
||||
logger.info("Modifying the Static connection " + connName );
|
||||
shell.executeBashCommand(
|
||||
modStaticCommand
|
||||
.replace("${connection}", connName)
|
||||
.replace("${ipaddr}", config.staticIp)
|
||||
.replace("${gateway}", gateway)
|
||||
);
|
||||
NetworkingCommands.addConnectionCommand
|
||||
.replace("${connection}", connName)
|
||||
.replace("${interface}", config.networkManagerIface));
|
||||
}
|
||||
// modify it in case the static IP address is different
|
||||
logger.info("Updating the Static connection " + connName);
|
||||
shell.executeBashCommand(
|
||||
NetworkingCommands.modStaticCommand
|
||||
.replace("${connection}", connName)
|
||||
.replace("${ipaddr}", config.staticIp)
|
||||
.replace("${gateway}", gateway));
|
||||
// activate it
|
||||
logger.info("Activating the Static connection " + connName );
|
||||
shell.executeBashCommand("nmcli connection up \"${connection}\"".replace("${connection}", connName), false);
|
||||
logger.info("Activating the Static connection " + connName);
|
||||
shell.executeBashCommand(
|
||||
"nmcli connection up \"${connection}\"".replace("${connection}", connName), false);
|
||||
activeConnections.put(config.networkManagerIface, connName);
|
||||
} catch (Exception e) {
|
||||
logger.error("Error while setting static IP!", e);
|
||||
}
|
||||
@@ -267,31 +255,60 @@ public class NetworkManager {
|
||||
logger.error("Can't find " + path + ", so can't monitor " + devName);
|
||||
return;
|
||||
}
|
||||
logger.debug("Watching network interface at path: " + path);
|
||||
var last = new Object() {boolean carrier = true; boolean exceptionLogged = false;};
|
||||
Runnable task = () -> {
|
||||
try {
|
||||
boolean carrier = Files.readString(path).trim().equals("1");
|
||||
if (carrier != last.carrier) {
|
||||
if (carrier) {
|
||||
// carrier came back
|
||||
logger.info("Interface " + devName + " has re-connected, reinitializing");
|
||||
reinitialize();
|
||||
} else {
|
||||
logger.warn("Interface " + devName + " is disconnected, check Ethernet!");
|
||||
var last =
|
||||
new Object() {
|
||||
boolean carrier = true;
|
||||
boolean exceptionLogged = false;
|
||||
String addresses = "";
|
||||
};
|
||||
Runnable task =
|
||||
() -> {
|
||||
try {
|
||||
boolean carrier = Files.readString(path).trim().equals("1");
|
||||
if (carrier != last.carrier) {
|
||||
if (carrier) {
|
||||
// carrier came back
|
||||
logger.info("Interface " + devName + " has re-connected, reinitializing");
|
||||
reinitialize();
|
||||
} else {
|
||||
logger.warn("Interface " + devName + " is disconnected, check Ethernet!");
|
||||
}
|
||||
}
|
||||
NetworkInterface iFace;
|
||||
iFace = NetworkInterface.getByName(devName);
|
||||
if (iFace.isUp()) {
|
||||
String tmpAddresses = "";
|
||||
tmpAddresses = iFace.getInterfaceAddresses().toString();
|
||||
if (!last.addresses.equals(tmpAddresses)) {
|
||||
// addresses have changed, log the difference
|
||||
last.addresses = tmpAddresses;
|
||||
logger.info("Interface " + devName + " has address(es): " + last.addresses);
|
||||
}
|
||||
var conn = NetworkUtils.getActiveConnection(devName);
|
||||
if (!conn.equals(activeConnections.get(devName))) {
|
||||
logger.warn(
|
||||
"Unexpected connection "
|
||||
+ conn
|
||||
+ " active on "
|
||||
+ devName
|
||||
+ ". Expected "
|
||||
+ activeConnections.get(devName));
|
||||
logger.info("Reinitializing");
|
||||
reinitialize();
|
||||
}
|
||||
}
|
||||
last.carrier = carrier;
|
||||
last.exceptionLogged = false;
|
||||
} catch (Exception e) {
|
||||
if (!last.exceptionLogged) {
|
||||
// Log the exception only once, but keep trying
|
||||
logger.error("Could not check network status for " + devName, e);
|
||||
last.exceptionLogged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
last.carrier = carrier;
|
||||
last.exceptionLogged = false;
|
||||
} catch (Exception e) {
|
||||
if (!last.exceptionLogged) {
|
||||
// Log the exception only once, but keep trying
|
||||
logger.error("Could not check network status for " + devName, e);
|
||||
last.exceptionLogged = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
TimedTaskManager.getInstance().addTask(taskName, task, millisInterval);
|
||||
logger.debug("Watching network interface at path: " + path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.photonvision.common.networking;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.NetworkInterface;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -156,15 +157,50 @@ public class NetworkUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getActiveConnection(String devName) {
|
||||
var shell = new ShellExec(true, true);
|
||||
try {
|
||||
shell.executeBashCommand(
|
||||
"nmcli -g GENERAL.CONNECTION dev show \"" + devName + "\"", true, false);
|
||||
return shell.getOutput().strip();
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception from nmcli!");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static boolean connDoesNotExist(String connName) {
|
||||
var shell = new ShellExec(true, true);
|
||||
try {
|
||||
// set nmcli back to DHCP, and re-run dhclient -- this ought to grab a new IP address
|
||||
shell.executeBashCommand("nmcli -f GENERAL.STATE connection show \"" + connName + "\"");
|
||||
shell.executeBashCommand(
|
||||
"nmcli -g GENERAL.STATE connection show \"" + connName + "\"", true, false);
|
||||
return (shell.getExitCode() == 10);
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception from nmcli!");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String getIPAddresses(String iFaceName) {
|
||||
if (iFaceName == null || iFaceName.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
List<String> addresses = new ArrayList<String>();
|
||||
try {
|
||||
var iFace = NetworkInterface.getByName(iFaceName);
|
||||
for (var addr : iFace.getInterfaceAddresses()) {
|
||||
var addrStr = addr.getAddress().toString();
|
||||
if (addrStr.startsWith("/")) {
|
||||
addrStr = addrStr.substring(1);
|
||||
}
|
||||
addrStr = addrStr + "/" + addr.getNetworkPrefixLength();
|
||||
addresses.add(addrStr);
|
||||
}
|
||||
// addresses = iFace.inetAddresses().map(a ->
|
||||
// a.getAddress().toString()).collect(Collectors.joining(","));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return String.join(", ", addresses);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.common.networking;
|
||||
|
||||
// using a separate class because Spotless fails on text blocks
|
||||
// spotless:off
|
||||
public class NetworkingCommands {
|
||||
public static final String addConnectionCommand = """
|
||||
nmcli connection add
|
||||
con-name "${connection}"
|
||||
ifname "${interface}"
|
||||
type ethernet
|
||||
""".replaceAll("[\\n]", " ");
|
||||
|
||||
public static final String modStaticCommand = """
|
||||
nmcli connection modify ${connection}
|
||||
autoconnect yes
|
||||
ipv4.method manual
|
||||
ipv6.method disabled
|
||||
ipv4.addresses ${ipaddr}/8
|
||||
ipv4.gateway ${gateway}
|
||||
""".replaceAll("[\\n]", " ");
|
||||
|
||||
public static final String modDHCPCommand = """
|
||||
nmcli connection modify "${connection}"
|
||||
autoconnect yes
|
||||
ipv4.method auto
|
||||
ipv6.method disabled
|
||||
""".replaceAll("[\\n]", " ");
|
||||
}
|
||||
//spotless:on
|
||||
@@ -40,8 +40,15 @@ public class ShellExec {
|
||||
this.readError = readError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a bash command. We can handle complex bash commands including multiple executions (; |
|
||||
* and ||), quotes, expansions ($), escapes (\), e.g.: "cd /abc/def; mv ghi 'older ghi '$(whoami)"
|
||||
*
|
||||
* @param command Bash command to execute
|
||||
* @return process exit code
|
||||
*/
|
||||
public int executeBashCommand(String command) throws IOException {
|
||||
return executeBashCommand(command, true);
|
||||
return executeBashCommand(command, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,10 +56,25 @@ public class ShellExec {
|
||||
* and ||), quotes, expansions ($), escapes (\), e.g.: "cd /abc/def; mv ghi 'older ghi '$(whoami)"
|
||||
*
|
||||
* @param command Bash command to execute
|
||||
* @return true if bash got started, but your command may have failed.
|
||||
* @param wait true if the command should wait for the proccess to complete
|
||||
* @return process exit code
|
||||
*/
|
||||
public int executeBashCommand(String command, boolean wait) throws IOException {
|
||||
logger.debug("Executing \"" + command + "\"");
|
||||
return executeBashCommand(command, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a bash command. We can handle complex bash commands including multiple executions (; |
|
||||
* and ||), quotes, expansions ($), escapes (\), e.g.: "cd /abc/def; mv ghi 'older ghi '$(whoami)"
|
||||
* This runs the commands with the default logging.
|
||||
*
|
||||
* @param command Bash command to execute
|
||||
* @param wait true if the command should wait for the proccess to complete
|
||||
* @param debug true if the command and return value should be logged
|
||||
* @return process exit code
|
||||
*/
|
||||
public int executeBashCommand(String command, boolean wait, boolean debug) throws IOException {
|
||||
if (debug) logger.debug("Executing \"" + command + "\"");
|
||||
|
||||
boolean success = false;
|
||||
Runtime r = Runtime.getRuntime();
|
||||
@@ -68,7 +90,7 @@ public class ShellExec {
|
||||
// Consume streams, older jvm's had a memory leak if streams were not read,
|
||||
// some other jvm+OS combinations may block unless streams are consumed.
|
||||
int retcode = doProcess(wait, process);
|
||||
logger.debug("Got exit code " + retcode);
|
||||
if (debug) logger.debug("Got exit code " + retcode);
|
||||
return retcode;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user