Skip to content

Commit

Permalink
feat(linux.net): DHCP server selection method (#5578)
Browse files Browse the repository at this point in the history
* Implemented systemd unit exists method

Signed-off-by: pierantoniomerlino <[email protected]>

* Added folder and tests

Signed-off-by: pierantoniomerlino <[email protected]>

* Replaced method for checking unit presence; test updated

Signed-off-by: pierantoniomerlino <[email protected]>

* Applied cleanup

Signed-off-by: pierantoniomerlino <[email protected]>

* Reverted variable name

Signed-off-by: pierantoniomerlino <[email protected]>

* Fixed sonar complains

Signed-off-by: pierantoniomerlino <[email protected]>

* Improved tool search

Signed-off-by: pierantoniomerlino <[email protected]>

* Changed logger level

Signed-off-by: pierantoniomerlino <[email protected]>

* Added log when dnsmasq start fails

Signed-off-by: pierantoniomerlino <[email protected]>

* Fixed minor issues

Signed-off-by: pierantoniomerlino <[email protected]>

* Simplified command process

Signed-off-by: pierantoniomerlino <[email protected]>

* Removed useless inner class

Signed-off-by: pierantoniomerlino <[email protected]>

* Improved logging

Signed-off-by: pierantoniomerlino <[email protected]>

---------

Signed-off-by: pierantoniomerlino <[email protected]>
  • Loading branch information
pierantoniomerlino authored and MMaiero committed Dec 19, 2024
1 parent f43c66e commit 380b838
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 62 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*******************************************************************************
* Copyright (c) 2011, 2023 Eurotech and/or its affiliates and others
*
* Copyright (c) 2011, 2024 Eurotech and/or its affiliates and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
*
* SPDX-License-Identifier: EPL-2.0
*
*
* Contributors:
* Eurotech
*******************************************************************************/
Expand Down Expand Up @@ -58,7 +58,8 @@ public DhcpServerManager(CommandExecutorService service) {

public static DhcpServerTool getTool() {
if (dhcpServerTool == DhcpServerTool.NONE) {
if (LinuxNetworkUtil.toolExists(DhcpServerTool.DNSMASQ.getValue())) {
if (LinuxNetworkUtil.toolExists(DhcpServerTool.DNSMASQ.getValue())
&& LinuxNetworkUtil.systemdSystemUnitExists(DhcpServerTool.DNSMASQ.getValue() + ".service")) {
dhcpServerTool = DhcpServerTool.DNSMASQ;
} else if (LinuxNetworkUtil.toolExists(DhcpServerTool.UDHCPD.getValue())) {
dhcpServerTool = DhcpServerTool.UDHCPD;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*******************************************************************************/
package org.eclipse.kura.linux.net.dhcp.server;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -42,10 +43,10 @@ public class DnsmasqTool implements DhcpLinuxTool {
private String globalConfigFilename = "/etc/dnsmasq.d/dnsmasq-globals.conf";
private static final String GLOBAL_CONFIGURATION = "port=0\nbind-dynamic\n";

static final Command IS_ACTIVE_COMMAND = new Command(new String[] { "systemctl", "is-active", "--quiet",
DhcpServerTool.DNSMASQ.getValue() });
static final Command RESTART_COMMAND = new Command(new String[] { "systemctl", "restart",
DhcpServerTool.DNSMASQ.getValue() });
static final String[] IS_ACTIVE_COMMANDLINE = new String[] { "systemctl", "is-active", "--quiet",
DhcpServerTool.DNSMASQ.getValue() };
static final String[] RESTART_COMMANDLINE = new String[] { "systemctl", "restart",
DhcpServerTool.DNSMASQ.getValue() };

private CommandExecutorService executorService;
private Map<String, byte[]> configsLastHash = Collections.synchronizedMap(new HashMap<>());
Expand All @@ -57,7 +58,7 @@ public DnsmasqTool(CommandExecutorService service) {

@Override
public boolean isRunning(String interfaceName) throws KuraProcessExecutionErrorException {
CommandStatus status = this.executorService.execute(IS_ACTIVE_COMMAND);
CommandStatus status = this.executorService.execute(new Command(IS_ACTIVE_COMMANDLINE));

boolean isRunning;
try {
Expand Down Expand Up @@ -85,11 +86,19 @@ public CommandStatus startInterface(String interfaceName) throws KuraProcessExec
"Failed to start DHCP server for interface: " + interfaceName);
}

CommandStatus restartStatus = this.executorService.execute(RESTART_COMMAND);
Command restartCommand = new Command(RESTART_COMMANDLINE);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
restartCommand.setErrorStream(err);
restartCommand.setOutputStream(out);
CommandStatus restartStatus = this.executorService.execute(restartCommand);

if (!restartStatus.getExitStatus().isSuccessful()) {
logger.error("dnsmasq Systemd unit startup failed. Check if the tool is properly installed on the system.");
logger.error("dnsmasq stderr {}", new String(err.toByteArray(), StandardCharsets.UTF_8));
logger.error("dnsmasq stdout {}", new String(out.toByteArray(), StandardCharsets.UTF_8));
removeInterfaceConfig(interfaceName);
restartStatus = this.executorService.execute(RESTART_COMMAND);
restartStatus = this.executorService.execute(restartCommand);
}

return restartStatus;
Expand All @@ -100,7 +109,7 @@ public boolean disableInterface(String interfaceName) throws KuraProcessExecutio
boolean isInterfaceDisabled = true;

if (removeInterfaceConfig(interfaceName)) {
CommandStatus status = this.executorService.execute(RESTART_COMMAND);
CommandStatus status = this.executorService.execute(new Command(RESTART_COMMANDLINE));
isInterfaceDisabled = status.getExitStatus().isSuccessful();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*******************************************************************************
* Copyright (c) 2011, 2022 Eurotech and/or its affiliates and others
*
* Copyright (c) 2011, 2024 Eurotech and/or its affiliates and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
*
* SPDX-License-Identifier: EPL-2.0
*
*
* Contributors:
* Eurotech
*******************************************************************************/
Expand Down Expand Up @@ -44,12 +44,9 @@

public class LinuxNetworkUtil {

public static final String ACCESS_POINT_INTERFACE_SUFFIX = "_ap";

private static final String ETHTOOL_COMMAND = "ethtool";

private static final Logger logger = LoggerFactory.getLogger(LinuxNetworkUtil.class);

public static final String ACCESS_POINT_INTERFACE_SUFFIX = "_ap";
private static Map<String, LinuxIfconfig> ifconfigs = new HashMap<>();
private static final String[] IGNORE_IFACES = { "can", "sit", "mon.wlan" };
private static final ArrayList<String> TOOLS = new ArrayList<>();
Expand All @@ -65,13 +62,12 @@ public class LinuxNetworkUtil {
private static final String IFCONFIG = "ifconfig";
private static final String IWCONFIG = "iwconfig";
private static final String IP = "ip";

private static final String LINE_MSG = "line: {}";

private static final String ERR_EXECUTING_CMD_MSG = "error executing command --- {} --- exit value={}";

private static final String FAKE_MAC_ADDRESS = "12:34:56:78:ab:cd";

private static final String ETHTOOL_COMMAND = "ethtool";
private static final String[] DEFAULT_PATH = new String[] { "/sbin/", "/usr/sbin/", "/bin/", "/usr/bin/" };
private static final ProcessBuilder PROCESS_BUILDER = new ProcessBuilder();
private final CommandExecutorService executorService;
private final WifiOptions wifiOptions;

Expand Down Expand Up @@ -264,22 +260,59 @@ private boolean isWifiLinkUpInternal(String ifaceName) throws KuraException {
}

public static boolean toolExists(String tool) {
boolean ret = false;
final String[] searchFolders = new String[] { "/sbin/", "/usr/sbin/", "/bin/", "/usr/bin/" };
return getToolPath(tool).isPresent();
}

if (TOOLS.contains(tool)) {
ret = true;
public static Optional<String> getToolPath(String tool) {
Optional<String> optionalToolPath = getCachedTool(tool);
if (optionalToolPath.isPresent()) {
return optionalToolPath;
} else {
for (String folder : searchFolders) {
File fTool = new File(folder + tool);
if (fTool.exists()) {
TOOLS.add(tool);
ret = true;
break;
for (String folder : DEFAULT_PATH) {
String toolPath = folder + tool;
File toolFile = new File(toolPath);
if (toolFile.exists()) {
TOOLS.add(toolPath);
return Optional.of(toolPath);
}
}
}
return ret;
return Optional.empty();
}

private static Optional<String> getCachedTool(String tool) {
return TOOLS.stream().filter(item -> item.endsWith(tool)).findFirst();
}

/**
* Checks if the given Systemd system unit is installed.
* The result is based on the exit code of "systemctl status <unitName>"
* as presented in https://www.man7.org/linux/man-pages/man1/systemctl.1.html#EXIT_STATUS
*
* @param unitName
* the name of the Systemd system unit
* @return true if the unit is installed
*/
public static boolean systemdSystemUnitExists(String unitName) {
Optional<String> optionalSystemctlPath = getToolPath("systemctl");
if (!optionalSystemctlPath.isPresent()) {
logger.debug("Systemctl command not found in default paths");
return false;
}
PROCESS_BUILDER.command(optionalSystemctlPath.get(), "status", unitName);
Process process;
try {
process = PROCESS_BUILDER.start();
int exitCode = process.waitFor();
return exitCode < 4;
} catch (IOException e) {
logger.error("Cannot check {} unit existence", unitName, e);
return false;
} catch (InterruptedException e1) {
Thread.currentThread().interrupt();
logger.error("Cannot check {} unit existence", unitName, e1);
return false;
}
}

/**
Expand Down Expand Up @@ -1124,7 +1157,7 @@ public void createApNetworkInterface(String ifaceName, String dedicatedApInterfa
return;
}

CommandStatus status = this.executeCommand(formIwDevIfaceInterfaceAddAp(ifaceName, dedicatedApInterface));
CommandStatus status = executeCommand(formIwDevIfaceInterfaceAddAp(ifaceName, dedicatedApInterface));

if (!status.getExitStatus().isSuccessful()) {
throw new KuraException(KuraErrorCode.OS_COMMAND_ERROR,
Expand All @@ -1137,7 +1170,7 @@ public void setNetworkInterfaceMacAddress(String ifaceName) throws KuraException
return;
}

CommandStatus status = this.executeCommand(formIpLinkSetAddress(ifaceName, FAKE_MAC_ADDRESS));
CommandStatus status = executeCommand(formIpLinkSetAddress(ifaceName, FAKE_MAC_ADDRESS));

if (!status.getExitStatus().isSuccessful()) {
throw new KuraException(KuraErrorCode.OS_COMMAND_ERROR,
Expand All @@ -1150,7 +1183,7 @@ public void setNetworkInterfaceLinkUp(String ifaceName) throws KuraException {
return;
}

CommandStatus status = this.executeCommand(formIpLinkSetStatus(ifaceName, "up"));
CommandStatus status = executeCommand(formIpLinkSetStatus(ifaceName, "up"));

if (!status.getExitStatus().isSuccessful()) {
throw new KuraException(KuraErrorCode.OS_COMMAND_ERROR, "Failed to set link up for interface " + ifaceName);
Expand All @@ -1162,7 +1195,7 @@ public void setNetworkInterfaceLinkDown(String ifaceName) throws KuraException {
return;
}

CommandStatus status = this.executeCommand(formIpLinkSetStatus(ifaceName, "down"));
CommandStatus status = executeCommand(formIpLinkSetStatus(ifaceName, "down"));

if (!status.getExitStatus().isSuccessful()) {
throw new KuraException(KuraErrorCode.OS_COMMAND_ERROR, "Failed to set link up for interface " + ifaceName);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*******************************************************************************
* Copyright (c) 2017, 2023 Eurotech and/or its affiliates and others
*
* Copyright (c) 2017, 2024 Eurotech and/or its affiliates and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
*
* SPDX-License-Identifier: EPL-2.0
*
*
* Contributors:
* Eurotech
******************************************************************************/
Expand All @@ -27,9 +27,12 @@
import org.eclipse.kura.linux.net.dhcp.server.DnsmasqLeaseReader;
import org.eclipse.kura.linux.net.dhcp.server.UdhcpdConfigConverter;
import org.eclipse.kura.linux.net.dhcp.server.UdhcpdLeaseReader;
import org.eclipse.kura.linux.net.util.LinuxNetworkUtil;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

public class DhcpServerManagerTest {
Expand All @@ -50,6 +53,8 @@ public class DhcpServerManagerTest {
private Optional<DhcpServerConfigConverter> returnedConfigConverter;
private Optional<DhcpServerLeaseReader> returnedLeaseReader;
private Exception occurredException;
private MockedStatic<LinuxNetworkUtil> mockedLinuxNetworkUtil;
private DhcpServerTool tool;

/*
* Scenarios
Expand Down Expand Up @@ -268,6 +273,63 @@ public void shouldReturnLeaseReaderForNone() throws NoSuchFieldException {
thenReturnedLeaseReaderIsEmpty();
}

@Test
public void shouldGetDnsmasqToolIfBinaryAndUnitArePresent() throws NoSuchFieldException {
givenLinuxNetworkUtil("dnsmasq", Optional.of("dnsmasq.service"));
givenDhcpServerManager(DhcpServerTool.NONE);

whenGetTool();

thenToolIs(DhcpServerTool.DNSMASQ);
}

@Test
public void shouldNotGetDnsmasqToolIfOnlyBinaryIsPresent() throws NoSuchFieldException {
givenLinuxNetworkUtil("dnsmasq", Optional.empty());
givenDhcpServerManager(DhcpServerTool.NONE);

whenGetTool();

thenToolIs(DhcpServerTool.NONE);
}

@Test
public void shouldGetDhcpdTool() throws NoSuchFieldException {
givenLinuxNetworkUtil("dhcpd", Optional.empty());
givenDhcpServerManager(DhcpServerTool.NONE);

whenGetTool();

thenToolIs(DhcpServerTool.DHCPD);
}

@Test
public void shouldGetUdhcpdTool() throws NoSuchFieldException {
givenLinuxNetworkUtil("udhcpd", Optional.empty());
givenDhcpServerManager(DhcpServerTool.NONE);

whenGetTool();

thenToolIs(DhcpServerTool.UDHCPD);
}

@Test
public void shouldNotGetAnyTool() throws NoSuchFieldException {
givenLinuxNetworkUtil("", Optional.empty());
givenDhcpServerManager(DhcpServerTool.NONE);

whenGetTool();

thenToolIs(DhcpServerTool.NONE);
}

@After
public void deregisterStaticMocks() {
if (this.mockedLinuxNetworkUtil != null) {
this.mockedLinuxNetworkUtil.close();
}
}

/*
* Steps
*/
Expand All @@ -281,7 +343,16 @@ private void givenDhcpServerManager(DhcpServerTool dhcpServerTool) throws NoSuch

TestUtil.setFieldValue(new DhcpServerManager(null), "dhcpServerTool", dhcpServerTool);

this.dhcpServerManager = new DhcpServerManager(executorMock);
this.dhcpServerManager = new DhcpServerManager(this.executorMock);
}

private void givenLinuxNetworkUtil(String toolName, Optional<String> unitName) {
this.mockedLinuxNetworkUtil = Mockito.mockStatic(LinuxNetworkUtil.class);
this.mockedLinuxNetworkUtil.when(() -> LinuxNetworkUtil.toolExists(toolName)).thenReturn(true);
if (unitName.isPresent()) {
this.mockedLinuxNetworkUtil.when(() -> LinuxNetworkUtil.systemdSystemUnitExists(unitName.get()))
.thenReturn(true);
}
}

/*
Expand Down Expand Up @@ -328,6 +399,14 @@ private void whenGetLeaseReader() {
}
}

private void whenGetTool() {
try {
this.tool = DhcpServerManager.getTool();
} catch (Exception e) {
this.occurredException = e;
}
}

/*
* Then
*/
Expand Down Expand Up @@ -364,4 +443,7 @@ private void thenReturnedLeaseReaderIs(Class<?> dhcpServerLeaseReader) {
assertEquals(dhcpServerLeaseReader, this.returnedLeaseReader.get().getClass());
}

private void thenToolIs(DhcpServerTool expectedTool) {
assertEquals(expectedTool, this.tool);
}
}
Loading

0 comments on commit 380b838

Please sign in to comment.