diff --git a/data-node/src/main/java/org/graylog/datanode/bindings/PreflightChecksBindings.java b/data-node/src/main/java/org/graylog/datanode/bindings/PreflightChecksBindings.java index 8f2fc88221..c78a7edb26 100644 --- a/data-node/src/main/java/org/graylog/datanode/bindings/PreflightChecksBindings.java +++ b/data-node/src/main/java/org/graylog/datanode/bindings/PreflightChecksBindings.java @@ -18,7 +18,7 @@ package org.graylog.datanode.bindings; import com.google.inject.AbstractModule; import com.google.inject.multibindings.MapBinder; -import org.graylog.datanode.bootstrap.OpensearchBinDirExistsCheck; +import org.graylog.datanode.bootstrap.OpensearchBinPreflightCheck; import org.graylog2.bootstrap.preflight.PreflightCheck; public class PreflightChecksBindings extends AbstractModule { @@ -26,7 +26,7 @@ public class PreflightChecksBindings extends AbstractModule { @Override protected void configure() { - addPreflightCheck(OpensearchBinDirExistsCheck.class); + addPreflightCheck(OpensearchBinPreflightCheck.class); } diff --git a/data-node/src/main/java/org/graylog/datanode/bootstrap/OpensearchBinDirExistsCheck.java b/data-node/src/main/java/org/graylog/datanode/bootstrap/OpensearchBinDirExistsCheck.java deleted file mode 100644 index f434cff0a4..0000000000 --- a/data-node/src/main/java/org/graylog/datanode/bootstrap/OpensearchBinDirExistsCheck.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2020 Graylog, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * 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 - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * . - */ -package org.graylog.datanode.bootstrap; - -import org.graylog.datanode.Configuration; -import org.graylog2.bootstrap.preflight.PreflightCheck; -import org.graylog2.bootstrap.preflight.PreflightCheckException; - -import javax.inject.Inject; -import java.nio.file.Files; -import java.nio.file.Path; - -public class OpensearchBinDirExistsCheck implements PreflightCheck { - - private final String opensearchLocation; - - @Inject - public OpensearchBinDirExistsCheck(Configuration configuration) { - this.opensearchLocation = configuration.getOpensearchLocation(); - } - - @Override - public void runCheck() throws PreflightCheckException { - if (!Files.isDirectory(Path.of(opensearchLocation))) { - throw new PreflightCheckException("Opensearch base directory " + opensearchLocation + " doesn't exist!"); - } - } -} diff --git a/data-node/src/main/java/org/graylog/datanode/bootstrap/OpensearchBinPreflightCheck.java b/data-node/src/main/java/org/graylog/datanode/bootstrap/OpensearchBinPreflightCheck.java new file mode 100644 index 0000000000..085b1c2dbb --- /dev/null +++ b/data-node/src/main/java/org/graylog/datanode/bootstrap/OpensearchBinPreflightCheck.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.datanode.bootstrap; + +import org.graylog.datanode.Configuration; +import org.graylog2.bootstrap.preflight.PreflightCheck; +import org.graylog2.bootstrap.preflight.PreflightCheckException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.stream.Collectors; + +public class OpensearchBinPreflightCheck implements PreflightCheck { + + private final Path opensearchDir; + + private static final Logger LOG = LoggerFactory.getLogger(OpensearchBinPreflightCheck.class); + + @Inject + public OpensearchBinPreflightCheck(Configuration configuration) { + this(Path.of(configuration.getOpensearchLocation())); + } + + protected OpensearchBinPreflightCheck(Path opensearchBaseDirectory) { + this.opensearchDir = opensearchBaseDirectory; + } + + @Override + public void runCheck() throws PreflightCheckException { + if (!Files.isDirectory(opensearchDir)) { + throw new PreflightCheckException("Opensearch base directory " + opensearchDir + " doesn't exist!"); + } + + final Path binPath = opensearchDir.resolve(Paths.get("bin", "opensearch")); + + if (!Files.exists(binPath)) { + throw new PreflightCheckException("Opensearch binary " + binPath + " doesn't exist!"); + } + + if (!Files.isExecutable(binPath)) { + final String permissions = getPermissions(binPath) + .map(p -> " Permissions of the binary are: " + p) + .orElse(""); + throw new PreflightCheckException("Opensearch binary " + binPath + " is not executable!" + permissions); + } + } + + private static Optional getPermissions(Path binPath) { + try { + return Optional.of(Files.getPosixFilePermissions(binPath)) + .map(perms -> perms.stream().map(Enum::toString).collect(Collectors.joining(","))); + } catch (IOException e) { + LOG.warn("Failed to obtain opensearch binary permissions: " + e.getMessage()); + return Optional.empty(); + } + } +} diff --git a/data-node/src/main/java/org/graylog/datanode/management/OpensearchProcessImpl.java b/data-node/src/main/java/org/graylog/datanode/management/OpensearchProcessImpl.java index 2c1494109b..67eed88a5d 100644 --- a/data-node/src/main/java/org/graylog/datanode/management/OpensearchProcessImpl.java +++ b/data-node/src/main/java/org/graylog/datanode/management/OpensearchProcessImpl.java @@ -179,11 +179,13 @@ class OpensearchProcessImpl implements OpensearchProcess, ProcessListener { @Override public void onProcessComplete(int exitValue) { + LOG.info("Opensearch process completed with exit code {}", exitValue); onEvent(ProcessEvent.PROCESS_TERMINATED); } @Override public void onProcessFailed(ExecuteException e) { + LOG.warn("Opensearch process failed", e); onEvent(ProcessEvent.PROCESS_TERMINATED); } } diff --git a/data-node/src/main/java/org/graylog/datanode/process/ProcessStateMachine.java b/data-node/src/main/java/org/graylog/datanode/process/ProcessStateMachine.java index 9877bd1e29..9ead799c57 100644 --- a/data-node/src/main/java/org/graylog/datanode/process/ProcessStateMachine.java +++ b/data-node/src/main/java/org/graylog/datanode/process/ProcessStateMachine.java @@ -38,7 +38,9 @@ public class ProcessStateMachine { // Freshly created process, it hasn't started yet and doesn't have any pid. config.configure(ProcessState.NEW) - .permit(ProcessEvent.PROCESS_STARTED, ProcessState.STARTING); + .permit(ProcessEvent.PROCESS_STARTED, ProcessState.STARTING) + .permit(ProcessEvent.PROCESS_TERMINATED, ProcessState.TERMINATED) + .permit(ProcessEvent.HEALTH_CHECK_FAILED, ProcessState.TERMINATED); // the process has started already, now we have to wait for a running OS and available REST api // the startupFailuresCounter keeps track of failed REST status calls and allow failures during the diff --git a/data-node/src/test/java/org/graylog/datanode/bootstrap/OpensearchBinPreflightCheckTest.java b/data-node/src/test/java/org/graylog/datanode/bootstrap/OpensearchBinPreflightCheckTest.java new file mode 100644 index 0000000000..3c2039293d --- /dev/null +++ b/data-node/src/test/java/org/graylog/datanode/bootstrap/OpensearchBinPreflightCheckTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.datanode.bootstrap; + +import org.assertj.core.api.Assertions; +import org.graylog2.bootstrap.preflight.PreflightCheckException; +import org.graylog2.shared.utilities.StringUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +class OpensearchBinPreflightCheckTest { + + @TempDir + private Path tempDir; + + @Test + void testNonexistentDirectory() { + final Path baseDirectory = tempDir.resolve("nonexistent"); + final OpensearchBinPreflightCheck check = new OpensearchBinPreflightCheck(baseDirectory); + Assertions.assertThatThrownBy(check::runCheck) + .isInstanceOf(PreflightCheckException.class) + .hasMessage("Opensearch base directory %s doesn't exist!", baseDirectory.toAbsolutePath()); + } + + + @Test + void testNonexistentBinary() throws IOException { + final Path baseDir = tempDir.resolve("opensearch"); + final Path binDir = baseDir.resolve("bin"); + Files.createDirectories(binDir); + + // nonexistent! + final Path executable = binDir.resolve("opensearch"); + + final OpensearchBinPreflightCheck check = new OpensearchBinPreflightCheck(baseDir); + + Assertions.assertThatThrownBy(check::runCheck) + .isInstanceOf(PreflightCheckException.class) + .hasMessage("Opensearch binary %s doesn't exist!", executable.toAbsolutePath()); + } + + @Test + void testBinaryWithoutExecPermission() throws IOException { + final Path baseDir = tempDir.resolve("opensearch"); + final Path binDir = baseDir.resolve("bin"); + Files.createDirectories(binDir); + final Path executable = binDir.resolve("opensearch"); + Files.createFile(executable); + + final OpensearchBinPreflightCheck check = new OpensearchBinPreflightCheck(baseDir); + Assertions.assertThatThrownBy(check::runCheck) + .isInstanceOf(PreflightCheckException.class) + .hasMessageStartingWith(StringUtils.f("Opensearch binary %s is not executable!", executable.toAbsolutePath())); + } + + + @Test + void testBinaryOk() throws IOException { + final Path baseDir = tempDir.resolve("opensearch"); + final Path binDir = baseDir.resolve("bin"); + Files.createDirectories(binDir); + final Path executable = binDir.resolve("opensearch"); + Files.createFile(executable); + Files.setPosixFilePermissions(executable, Collections.singleton(PosixFilePermission.OWNER_EXECUTE)); + + + final OpensearchBinPreflightCheck check = new OpensearchBinPreflightCheck(baseDir); + Assertions.assertThatCode(check::runCheck) + .doesNotThrowAnyException(); + } +} +