Advanced networking settings (#899)

Exposes NetworkManager interface name and more robustly handles device/interface names internally.

---------

Co-authored-by: Sriman Achanta <68172138+srimanachanta@users.noreply.github.com>
This commit is contained in:
Matt
2023-09-01 12:58:35 -07:00
committed by GitHub
parent 08892b9e68
commit 306677e56f
14 changed files with 313 additions and 90 deletions

View File

@@ -83,12 +83,12 @@ public class ConfigManager {
// Cannot import into SQL if we aren't in SQL mode rn
return;
}
logger.info("Translating settings zip!");
var maybeCams = Path.of(folderPath.toAbsolutePath().toString(), "cameras").toFile();
var maybeCamsBak = Path.of(folderPath.toAbsolutePath().toString(), "cameras_backup").toFile();
if (maybeCams.exists() && maybeCams.isDirectory()) {
logger.info("Translating settings zip!");
var legacy = new LegacyConfigProvider(folderPath);
legacy.load();
var loadedConfig = legacy.getConfig();
@@ -264,6 +264,12 @@ public class ConfigManager {
this.getConfig().getCameraConfigurations().clear();
}
public void clearConfig() {
logger.info("Clearing configuration!");
m_provider.clearConfig();
m_provider.saveToDisk();
}
public void saveToDisk() {
m_provider.saveToDisk();
}

View File

@@ -20,7 +20,7 @@ package org.photonvision.common.configuration;
import java.nio.file.Path;
public abstract class ConfigProvider {
private PhotonConfiguration config;
protected PhotonConfiguration config;
abstract void load();
@@ -30,6 +30,10 @@ public abstract class ConfigProvider {
return config;
}
public void clearConfig() {
config = new PhotonConfiguration();
}
public abstract boolean saveUploadedHardwareConfig(Path uploadPath);
public abstract boolean saveUploadedHardwareSettings(Path uploadPath);

View File

@@ -19,15 +19,14 @@ package org.photonvision.common.configuration;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.networking.NetworkMode;
import org.photonvision.common.networking.NetworkUtils;
import org.photonvision.common.util.file.JacksonUtils;
public class NetworkConfig {
@@ -37,21 +36,31 @@ public class NetworkConfig {
public String staticIp = "";
public String hostname = "photonvision";
public boolean runNTServer = false;
public boolean shouldManage;
@JsonIgnore public static final String NM_IFACE_STRING = "${interface}";
@JsonIgnore public static final String NM_IP_STRING = "${ipaddr}";
public String networkManagerIface = "Wired connection 1";
public String physicalInterface = "eth0";
public String networkManagerIface;
public String setStaticCommand =
"nmcli con mod ${interface} ipv4.addresses ${ipaddr}/8 ipv4.method \"manual\" ipv6.method \"disabled\"";
public String setDHCPcommand =
"nmcli con mod ${interface} ipv4.method \"auto\" ipv6.method \"disabled\"";
private boolean shouldManage;
public NetworkConfig() {
setShouldManage(false);
if (Platform.isLinux()) {
// Default to the name of the first Ethernet connection. Otherwise, "Wired connection 1" is a
// reasonable guess
this.networkManagerIface =
NetworkUtils.getAllWiredInterfaces().stream()
.map(it -> it.connName)
.findFirst()
.orElse("Wired connection 1");
}
// We can (usually) manage networking on Linux devices, and if we can we should try to. Command
// line inhibitions happen at a level above this class
setShouldManage(deviceCanManageNetwork());
}
@JsonCreator
@@ -64,7 +73,6 @@ public class NetworkConfig {
@JsonProperty("runNTServer") boolean runNTServer,
@JsonProperty("shouldManage") boolean shouldManage,
@JsonProperty("networkManagerIface") String networkManagerIface,
@JsonProperty("physicalInterface") String physicalInterface,
@JsonProperty("setStaticCommand") String setStaticCommand,
@JsonProperty("setDHCPcommand") String setDHCPcommand) {
this.ntServerAddress = ntServerAddress;
@@ -73,7 +81,6 @@ public class NetworkConfig {
this.hostname = hostname;
this.runNTServer = runNTServer;
this.networkManagerIface = networkManagerIface;
this.physicalInterface = physicalInterface;
this.setStaticCommand = setStaticCommand;
this.setDHCPcommand = setDHCPcommand;
setShouldManage(shouldManage);
@@ -81,7 +88,9 @@ public class NetworkConfig {
public Map<String, Object> toHashMap() {
try {
return new ObjectMapper().convertValue(this, JacksonUtils.UIMap.class);
var ret = new ObjectMapper().convertValue(this, JacksonUtils.UIMap.class);
ret.put("canManage", this.deviceCanManageNetwork());
return ret;
} catch (Exception e) {
e.printStackTrace();
return new HashMap<>();
@@ -89,18 +98,28 @@ public class NetworkConfig {
}
@JsonIgnore
public String getEscapedIfaceName() {
public String getPhysicalInterfaceName() {
return NetworkUtils.getNMinfoForConnName(this.networkManagerIface).devName;
}
@JsonIgnore
public String getEscapedInterfaceName() {
return "\"" + networkManagerIface + "\"";
}
@JsonGetter("shouldManage")
@JsonIgnore
public boolean shouldManage() {
return this.shouldManage || Platform.isLinux();
return this.shouldManage;
}
@JsonSetter("shouldManage")
@JsonIgnore
public void setShouldManage(boolean shouldManage) {
this.shouldManage = shouldManage || Platform.isLinux();
this.shouldManage = shouldManage && this.deviceCanManageNetwork();
}
@JsonIgnore
private boolean deviceCanManageNetwork() {
return Platform.isLinux();
}
@Override
@@ -117,8 +136,6 @@ public class NetworkConfig {
+ runNTServer
+ ", networkManagerIface="
+ networkManagerIface
+ ", physicalInterface="
+ physicalInterface
+ ", setStaticCommand="
+ setStaticCommand
+ ", setDHCPcommand="

View File

@@ -24,6 +24,7 @@ import java.util.Map;
import java.util.stream.Collectors;
import org.photonvision.PhotonVersion;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.networking.NetworkUtils;
import org.photonvision.common.util.SerializationUtils;
import org.photonvision.raspi.LibCameraJNI;
import org.photonvision.vision.processes.VisionModule;
@@ -55,6 +56,10 @@ public class PhotonConfiguration {
this.cameraConfigurations = cameraConfigurations;
}
public PhotonConfiguration() {
this(new HardwareConfig(), new HardwareSettings(), new NetworkConfig());
}
public HardwareConfig getHardwareConfig() {
return hardwareConfig;
}
@@ -93,7 +98,12 @@ public class PhotonConfiguration {
Map<String, Object> map = new HashMap<>();
var settingsSubmap = new HashMap<String, Object>();
settingsSubmap.put("networkSettings", networkConfig.toHashMap());
// Hack active interfaces into networkSettings
var netConfigMap = networkConfig.toHashMap();
netConfigMap.put("networkInterfaceNames", NetworkUtils.getAllWiredInterfaces());
settingsSubmap.put("networkSettings", netConfigMap);
map.put(
"cameraSettings",
VisionModuleManager.getInstance().getModules().stream()
@@ -140,17 +150,4 @@ public class PhotonConfiguration {
public List<HashMap<String, Object>> calibrations;
public boolean isFovConfigurable = true;
}
@Override
public String toString() {
return "PhotonConfiguration [hardwareConfig="
+ hardwareConfig
+ ", hardwareSettings="
+ hardwareSettings
+ ", networkConfig="
+ networkConfig
+ ", cameraConfigurations="
+ cameraConfigurations
+ "]";
}
}

View File

@@ -62,7 +62,6 @@ public class SqlConfigProvider extends ConfigProvider {
private static final String dbName = "photon.sqlite";
private final String dbPath;
private PhotonConfiguration config;
private final Object m_mutex = new Object();
private final File rootFolder;

View File

@@ -38,9 +38,10 @@ public class NetworkManager {
}
private boolean isManaged = false;
public boolean networkingIsDisabled = false; // Passed in via CLI
public void initialize(boolean shouldManage) {
isManaged = shouldManage;
isManaged = shouldManage && !networkingIsDisabled;
if (!isManaged) {
return;
}
@@ -99,8 +100,8 @@ public class NetworkManager {
// set nmcli back to DHCP, and re-run dhclient -- this ought to grab a new IP address
shell.executeBashCommand(
config.setDHCPcommand.replace(
NetworkConfig.NM_IFACE_STRING, config.getEscapedIfaceName()));
shell.executeBashCommand("dhclient " + config.physicalInterface, false);
NetworkConfig.NM_IFACE_STRING, config.getEscapedInterfaceName()));
shell.executeBashCommand("dhclient " + config.getPhysicalInterfaceName(), false);
} catch (Exception e) {
logger.error("Exception while setting DHCP!");
}
@@ -111,7 +112,7 @@ public class NetworkManager {
shell.executeBashCommand(
config
.setStaticCommand
.replace(NetworkConfig.NM_IFACE_STRING, config.getEscapedIfaceName())
.replace(NetworkConfig.NM_IFACE_STRING, config.getEscapedInterfaceName())
.replace(NetworkConfig.NM_IP_STRING, config.staticIp));
if (Platform.isRaspberryPi()) {
@@ -119,17 +120,17 @@ public class NetworkManager {
// integral in my testing (Matt)
shell.executeBashCommand(
"sh -c 'nmcli con down "
+ config.getEscapedIfaceName()
+ config.getEscapedInterfaceName()
+ "; nmcli con up "
+ config.getEscapedIfaceName()
+ config.getEscapedInterfaceName()
+ "'");
} else {
// for now just bring down /up -- more testing needed on beelink et al
shell.executeBashCommand(
"sh -c 'nmcli con down "
+ config.getEscapedIfaceName()
+ config.getEscapedInterfaceName()
+ "; nmcli con up "
+ config.getEscapedIfaceName()
+ config.getEscapedInterfaceName()
+ "'");
}
} catch (Exception e) {

View File

@@ -0,0 +1,131 @@
/*
* 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.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.ShellExec;
public class NetworkUtils {
private static final Logger logger = new Logger(NetworkUtils.class, LogGroup.General);
public static enum NMType {
NMTYPE_ETHERNET("ethernet"),
NMTYPE_WIFI("wifi"),
NMTYPE_UNKNOWN("");
private NMType(String id) {
identifier = id;
}
private final String identifier;
public static NMType typeForString(String s) {
for (var t : NMType.values()) {
if (t.identifier.equals(s)) {
return t;
}
}
return NMTYPE_UNKNOWN;
}
}
public static class NMDeviceInfo {
public NMDeviceInfo(String c, String d, String type) {
connName = c;
devName = d;
nmType = NMType.typeForString(type);
}
public final String connName; // Human readable name used by "nmcli con"
public final String devName; // underlying device, used by dhclient
public final NMType nmType;
@Override
public String toString() {
return "NMDeviceInfo [connName="
+ connName
+ ", devName="
+ devName
+ ", nmType="
+ nmType
+ "]";
}
}
public static ArrayList<NMDeviceInfo> getAllInterfaces() {
var ret = new ArrayList<NMDeviceInfo>();
if (!Platform.isLinux()) {
// Can't determine interface name on Linux, give up
return ret;
}
try {
var shell = new ShellExec(true, true);
shell.executeBashCommand(
"nmcli -t -f GENERAL.CONNECTION,GENERAL.DEVICE,GENERAL.TYPE device show");
String out = shell.getOutput();
if (out == null) {
return new ArrayList<>();
}
Pattern pattern =
Pattern.compile("GENERAL.CONNECTION:(.*)\nGENERAL.DEVICE:(.*)\nGENERAL.TYPE:(.*)");
Matcher matcher = pattern.matcher(out);
while (matcher.find()) {
ret.add(new NMDeviceInfo(matcher.group(1), matcher.group(2), matcher.group(3)));
}
} catch (IOException e) {
logger.error("Could not get active NM ifaces!", e);
}
logger.debug("Found network interfaces:\n" + ret.toString());
return ret;
}
public static List<NMDeviceInfo> getAllActiveInterfaces() {
// Seems like if a interface exists but isn't actually connected, the connection name will be an
// empty string. Check here and only return connections with non-empty names
return getAllInterfaces().stream()
.filter(it -> !it.connName.trim().isEmpty())
.collect(Collectors.toList());
}
public static List<NMDeviceInfo> getAllWiredInterfaces() {
return getAllActiveInterfaces().stream()
.filter(it -> it.nmType == NMType.NMTYPE_ETHERNET)
.collect(Collectors.toList());
}
public static NMDeviceInfo getNMinfoForConnName(String connName) {
for (NMDeviceInfo info : getAllActiveInterfaces()) {
if (info.connName.equals(connName)) {
return info;
}
}
return null;
}
}