Fix static IP and network settings on Pi

This commit is contained in:
Matt
2020-09-15 11:34:27 -07:00
committed by GitHub
parent 71fc8a7017
commit 44bfc3ea6c
10 changed files with 160 additions and 318 deletions

View File

@@ -12,6 +12,7 @@
color="#ffd843"
:label="name"
:value="index"
:disabled="disabled"
/>
</v-radio-group>
</div>
@@ -21,7 +22,7 @@
export default {
name: 'Radio',
// eslint-disable-next-line vue/require-prop-types
props: ['value', 'list'],
props: ['value', 'list', 'disabled'],
data() {
return {}
},

View File

@@ -108,7 +108,6 @@ export default new Vuex.Store({
// Below options are only configurable if supported is true
connectionType: 0, // 0 = DHCP, 1 = Static
staticIp: "",
netmask: "",
hostname: "photonvision",
runNTServer: false,
},
@@ -182,6 +181,17 @@ export default new Vuex.Store({
}
},
mutateNetworkSettings: (state, payload) => {
for (let key in payload) {
if (!payload.hasOwnProperty(key)) continue;
const value = payload[key];
const settings = state.settings.networkSettings;
if (settings.hasOwnProperty(key)) {
Vue.set(settings, key, value);
}
}
},
mutatePipelineResults(state, payload) {
// Key: index, value: result
for (let key in payload) {

View File

@@ -4,34 +4,33 @@
ref="form"
v-model="valid"
>
<CVSwitch
v-model="runNTServer"
name="Run NetworkTables Server"
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
/>
<CVnumberinput
v-model="settings.teamNumber"
v-model="teamNumber"
:disabled="settings.runNTServer"
name="Team Number"
:rules="[v => (v > 0) || 'Team number must be greater than zero', v => (v < 10000) || 'Team number must have fewer than five digits']"
class="mb-4"
/>
<CVSwitch
v-model="settings.runNTServer"
name="Run NetworkTables Server"
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
<CVradio
v-model="connectionType"
:list="['DHCP','Static']"
:disabled="!$store.state.settings.networkSettings.supported"
/>
<template v-if="$store.state.settings.networkSettings.supported">
<CVradio
v-model="settings.connectionType"
:list="['DHCP','Static']"
<template v-if="!isDHCP">
<CVinput
v-model="staticIp"
:input-cols="inputCols"
:rules="[v => isIPv4(v) || 'Invalid IPv4 address']"
name="IP"
/>
<template v-if="!isDHCP">
<CVinput
v-model="settings.staticIp"
:input-cols="inputCols"
:rules="[v => isIPv4(v) || 'Invalid IPv4 address']"
name="IP"
/>
</template>
</template>
<CVinput
v-model="settings.hostname"
v-model="hostname"
:input-cols="inputCols"
:rules="[v => isHostname(v) || 'Invalid hostname']"
name="Hostname"
@@ -49,94 +48,135 @@
</template>
<script>
import CVnumberinput from '../../components/common/cv-number-input'
import CVradio from '../../components/common/cv-radio'
import CVinput from '../../components/common/cv-input'
import CVSwitch from "@/components/common/cv-switch";
import CVnumberinput from '../../components/common/cv-number-input'
import CVradio from '../../components/common/cv-radio'
import CVinput from '../../components/common/cv-input'
import CVSwitch from "@/components/common/cv-switch";
// https://stackoverflow.com/a/17871737
const ipv4Regex = /^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/;
// https://stackoverflow.com/a/18494710
const hostnameRegex = /^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)+(\.([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*))*$/;
// https://stackoverflow.com/a/17871737
const ipv4Regex = /^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/;
// https://stackoverflow.com/a/18494710
const hostnameRegex = /^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)+(\.([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*))*$/;
export default {
name: 'Networking',
components: {
CVSwitch,
CVnumberinput,
CVradio,
CVinput
export default {
name: 'Networking',
components: {
CVSwitch,
CVnumberinput,
CVradio,
CVinput
},
data() {
return {
file: undefined,
snackbar: {
color: "success",
text: ""
},
snack: false,
isLoading: false,
valid: true, // Are all settings valid
}
},
computed: {
inputCols() {
return this.$vuetify.breakpoint.smAndUp ? 10 : 7;
},
data() {
return {
file: undefined,
snackbar: {
color: "success",
text: ""
},
snack: false,
isLoading: false,
valid: true, // Are all settings valid
isDHCP() {
return this.settings.connectionType === 0;
},
settings() {
return this.$store.state.settings.networkSettings;
},
teamNumber: {
get() {
return this.settings.teamNumber
},
set(value) {
this.$store.commit('mutateNetworkSettings', {['teamNumber']: value});
}
},
runNTServer: {
get() {
return this.settings.runNTServer
},
set(value) {
this.$store.commit('mutateNetworkSettings', {['runNTServer']: value});
}
},
computed: {
inputCols() {
return this.$vuetify.breakpoint.smAndUp ? 10 : 7;
connectionType: {
get() {
return this.settings.connectionType
},
isDHCP() {
return this.settings.connectionType === 0;
},
settings() {
return this.$store.state.settings.networkSettings;
set(value) {
this.$store.commit('mutateNetworkSettings', {['connectionType']: value});
}
},
methods: {
isIPv4(v) {
return ipv4Regex.test(v);
staticIp: {
get() {
return this.settings.staticIp
},
isHostname(v) {
return hostnameRegex.test(v);
set(value) {
this.$store.commit('mutateNetworkSettings', {['staticIp']: value});
}
},
hostname: {
get() {
return this.settings.hostname
},
// https://www.freesoft.org/CIE/Course/Subnet/6.htm
// https://stackoverflow.com/a/13957228
isSubnetMask(v) {
// Has to be valid IPv4 so we'll start here
if (!this.isIPv4(v)) return false;
set(value) {
this.$store.commit('mutateNetworkSettings', {['hostname']: value});
}
},
},
methods: {
isIPv4(v) {
return ipv4Regex.test(v);
},
isHostname(v) {
return hostnameRegex.test(v);
},
// https://www.freesoft.org/CIE/Course/Subnet/6.htm
// https://stackoverflow.com/a/13957228
isSubnetMask(v) {
// Has to be valid IPv4 so we'll start here
if (!this.isIPv4(v)) return false;
let octets = v.split(".").map(it => Number(it));
let restAreOnes = false;
for (let i = 3; i >= 0; i--) {
let octets = v.split(".").map(it => Number(it));
let restAreOnes = false;
for (let i = 3; i >= 0; i--) {
for (let j = 0; j < 8; j++) {
let bitValue = (octets[i] >>> j & 1) == 1;
if (restAreOnes && !bitValue)
return false;
restAreOnes = bitValue;
let bitValue = (octets[i] >>> j & 1) == 1;
if (restAreOnes && !bitValue)
return false;
restAreOnes = bitValue;
}
}
return true;
},
sendGeneralSettings() {
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
function (response) {
if (response.status === 200) {
this.snackbar = {
color: "success",
text: "Settings updated successfully"
};
this.snack = true;
}
},
function (error) {
}
return true;
},
sendGeneralSettings() {
this.axios.post("http://" + this.$address + "/api/settings/general", this.settings).then(
function (response) {
if (response.status === 200) {
this.snackbar = {
color: "error",
text: (error.response || {data: "Couldn't save settings"}).data
color: "success",
text: "Settings updated successfully"
};
this.snack = true;
}
)
},
},
function (error) {
this.snackbar = {
color: "error",
text: (error.response || {data: "Couldn't save settings"}).data
};
this.snack = true;
}
)
},
}
},
}
</script>
<style lang="" scoped>

View File

@@ -162,7 +162,7 @@ public class Main {
// Force load the hardware manager
HardwareManager.getInstance();
NetworkManager.getInstance().initialize(false);
NetworkManager.getInstance().reinitialize();
NetworkTablesManager.getInstance()
.setConfig(ConfigManager.getInstance().getConfig().getNetworkConfig());

View File

@@ -26,7 +26,6 @@ public class NetworkConfig {
public int teamNumber = 0;
public NetworkMode connectionType = NetworkMode.DHCP;
public String staticIp = "";
public String netmask = "";
public String hostname = "photonvision";
public boolean runNTServer = false;
@@ -38,18 +37,16 @@ public class NetworkConfig {
int teamNumber,
NetworkMode connectionType,
String staticIp,
String netmask,
String hostname,
boolean runNTServer,
boolean shouldManage) {
this.teamNumber = teamNumber;
this.connectionType = connectionType;
this.staticIp = staticIp;
this.netmask = netmask;
this.hostname = hostname;
this.runNTServer = runNTServer;
this.shouldManage = shouldManage || Platform.isRaspberryPi();
this.shouldManage = shouldManage;
}
public static NetworkConfig fromHashMap(Map<String, Object> map) {
@@ -60,7 +57,6 @@ public class NetworkConfig {
ret.shouldManage = (Boolean) map.get("supported");
ret.connectionType = NetworkMode.values()[(Integer) map.get("connectionType")];
ret.staticIp = (String) map.get("staticIp");
ret.netmask = (String) map.get("netmask");
ret.hostname = (String) map.get("hostname");
ret.runNTServer = (Boolean) map.get("runNTServer");
return ret;
@@ -69,12 +65,15 @@ public class NetworkConfig {
public HashMap<String, Object> toHashMap() {
HashMap<String, Object> tmp = new HashMap<>();
tmp.put("teamNumber", teamNumber);
tmp.put("supported", shouldManage);
tmp.put("supported", shouldManage());
tmp.put("connectionType", connectionType.ordinal());
tmp.put("staticIp", staticIp);
tmp.put("netmask", netmask);
tmp.put("hostname", hostname);
tmp.put("runNTServer", runNTServer);
return tmp;
}
public boolean shouldManage() {
return this.shouldManage || Platform.isRaspberryPi();
}
}

View File

@@ -46,8 +46,6 @@ public class NetworkTablesManager {
private static final Logger logger = new Logger(NetworkTablesManager.class, LogGroup.General);
public boolean isServer = false;
private static class NTLogger implements Consumer<LogMessage> {
private boolean hasReportedConnectionFailure = false;
@@ -77,7 +75,6 @@ public class NetworkTablesManager {
}
private void setClientMode(int teamNumber) {
isServer = false;
logger.info("Starting NT Client");
ntInstance.stopServer();
@@ -91,7 +88,6 @@ public class NetworkTablesManager {
}
private void setServerMode() {
isServer = true;
logger.info("Starting NT Server");
ntInstance.stopClient();
ntInstance.startServer();

View File

@@ -1,121 +0,0 @@
/*
* Copyright (C) 2020 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.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public class LinuxNetworking extends SysNetworking {
private static final String PATH = "/etc/dhcpcd.conf";
private Logger logger = new Logger(LinuxNetworking.class, LogGroup.General);
@Override
public boolean setDHCP() {
File dhcpConf = new File(PATH);
logger.debug("Removing static IP from " + PATH);
if (dhcpConf.exists()) {
try {
List<String> lines = FileUtils.readLines(dhcpConf, StandardCharsets.UTF_8);
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
if (line.startsWith("interface " + networkInterface.name)) {
lines.remove(i);
for (int j = i; j < lines.size(); j++) {
String subInterface = lines.get(j);
if (subInterface.contains("static ip_address")
|| subInterface.contains("static routers")) {
lines.remove(j);
j--;
}
if (subInterface.contains("interface")) {
break;
}
}
FileUtils.writeLines(dhcpConf, lines);
return true;
}
}
} catch (IOException e) {
logger.error("Failed to set DHCP!", e);
return false;
}
} else {
logger.error("dhcpcd5 is not installed, unable to set IP.");
return false;
}
return true;
}
@Override
public boolean setHostname(String newHostname) {
try {
var setHostnameRetCode = shell.execute("hostnamectl", "set-hostname", newHostname);
return setHostnameRetCode == 0;
} catch (Exception e) {
logger.error("Failed to set hostname!", e);
return false;
}
}
@Override
public boolean setStatic(String ipAddress, String netmask) {
setDHCP(); // clean up old static interface
File dhcpConf = new File(PATH);
try {
List<String> lines = FileUtils.readLines(dhcpConf, StandardCharsets.UTF_8);
lines.add("interface " + networkInterface.name);
InetAddress iNetMask = InetAddress.getByName(netmask);
int prefix = convertNetmaskToCIDR(iNetMask);
lines.add("static ip_address=" + ipAddress + "/" + prefix);
FileUtils.writeLines(dhcpConf, lines);
return true;
} catch (IOException e) {
logger.error("Failed to set Static IP!", e);
}
return false;
}
@Override
public List<java.net.NetworkInterface> getNetworkInterfaces() throws SocketException {
List<java.net.NetworkInterface> netInterfaces;
try {
netInterfaces = Collections.list(java.net.NetworkInterface.getNetworkInterfaces());
} catch (SocketException e) {
return null;
}
List<java.net.NetworkInterface> goodInterfaces = new ArrayList<>();
for (var netInterface : netInterfaces) {
if (netInterface.getDisplayName().contains("lo")) continue;
if (!netInterface.isUp()) continue;
goodInterfaces.add(netInterface);
}
return goodInterfaces;
}
}

View File

@@ -76,12 +76,20 @@ public class NetworkManager {
}
if (config.connectionType == NetworkMode.DHCP) {
return; // TODO do we need to reconnect or something?
var shell = new ShellExec();
try {
if (config.staticIp != "") {
shell.executeBashCommand("ip addr del " + config.staticIp + "/8 dev eth0");
}
shell.executeBashCommand("dhclient eth0");
} catch (Exception e) {
logger.error("Exception while setting DHCP!");
}
} else if (config.connectionType == NetworkMode.STATIC) {
var shell = new ShellExec();
if (config.staticIp.length() > 0) {
try {
shell.executeBashCommand("ip addr add " + config.staticIp + "/24" + " dev eth0");
shell.executeBashCommand("ip addr add " + config.staticIp + "/8" + " dev eth0");
} catch (Exception e) {
logger.error("Error while setting static IP!", e);
}
@@ -95,6 +103,6 @@ public class NetworkManager {
}
public void reinitialize() {
initialize(ConfigManager.getInstance().getConfig().getNetworkConfig().shouldManage);
initialize(ConfigManager.getInstance().getConfig().getNetworkConfig().shouldManage());
}
}

View File

@@ -1,84 +0,0 @@
/*
* Copyright (C) 2020 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.net.InetAddress;
import java.net.SocketException;
import java.util.List;
import org.photonvision.common.util.ShellExec;
public abstract class SysNetworking {
NetworkInterface networkInterface;
ShellExec shell = new ShellExec(true, true);
private String hostname = getHostname();
public String getHostname() {
if (hostname == null) {
try {
var retCode = shell.execute("hostname", null, true);
if (retCode == 0) {
while (!shell.isOutputCompleted()) {}
return shell.getOutput();
} else {
return null;
}
} catch (IOException e) {
return null;
}
} else return hostname;
}
// code belongs to
// https://stackoverflow.com/questions/19531411/calculate-cidr-from-a-given-netmask-java
public static int convertNetmaskToCIDR(InetAddress netmask) {
byte[] netmaskBytes = netmask.getAddress();
int cidr = 0;
boolean zero = false;
for (byte b : netmaskBytes) {
int mask = 0x80;
for (int i = 0; i < 8; i++) {
int result = b & mask;
if (result == 0) {
zero = true;
} else if (zero) {
throw new IllegalArgumentException("Invalid netmask.");
} else {
cidr++;
}
mask >>>= 1;
}
}
return cidr;
}
public void setNetworkInterface(NetworkInterface networkInterface) {
this.networkInterface = networkInterface;
}
public abstract boolean setDHCP();
public abstract boolean setHostname(String hostname);
public abstract boolean setStatic(String ipAddress, String netmask);
public abstract List<java.net.NetworkInterface> getNetworkInterfaces() throws SocketException;
}

View File

@@ -73,15 +73,8 @@ public class RequestHandler {
public static void onGeneralSettings(Context context) throws JsonProcessingException {
Map<String, Object> map =
(Map<String, Object>) kObjectMapper.readValue(context.body(), Map.class);
var networking =
(Map<String, Object>)
map.get("networkSettings"); // teamNumber (int), supported (bool), connectionType (int),
// staticIp (str), netmask (str), gateway (str), hostname (str)
var lighting =
(Map<String, Object>) map.get("lighting"); // supported (true/false), brightness (int)
// TODO do stuff with lighting
var networkConfig = NetworkConfig.fromHashMap(networking);
var networkConfig = NetworkConfig.fromHashMap(map);
ConfigManager.getInstance().setNetworkSettings(networkConfig);
ConfigManager.getInstance().requestSave();
NetworkManager.getInstance().reinitialize();