| // Copyright (C) 2017 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.acceptance.ssh; |
| |
| import static com.google.common.collect.ImmutableList.toImmutableList; |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Streams; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.acceptance.AbstractDaemonTest; |
| import com.google.gerrit.acceptance.NoHttpd; |
| import com.google.gerrit.acceptance.Sandboxed; |
| import com.google.gerrit.acceptance.UseSsh; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Spliterator; |
| import java.util.stream.Collectors; |
| import java.util.stream.StreamSupport; |
| import org.junit.Test; |
| |
| @NoHttpd |
| @UseSsh |
| public class SshCommandsIT extends AbstractDaemonTest { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| // TODO: It would be better to dynamically generate these lists |
| private static final ImmutableList<String> COMMON_ROOT_COMMANDS = |
| ImmutableList.of( |
| "apropos", |
| "close-connection", |
| "flush-caches", |
| "gc", |
| "logging", |
| "ls-groups", |
| "ls-members", |
| "ls-projects", |
| "ls-user-refs", |
| "plugin", |
| "reload-config", |
| "show-caches", |
| "show-connections", |
| "show-queue", |
| "version"); |
| |
| private static final ImmutableList<String> MASTER_ONLY_ROOT_COMMANDS = |
| ImmutableList.of( |
| "ban-commit", |
| "create-account", |
| "create-branch", |
| "create-group", |
| "create-project", |
| "index", |
| "query", |
| "receive-pack", |
| "rename-group", |
| "review", |
| "sequence", |
| "set-account", |
| "set-head", |
| "set-members", |
| "set-project", |
| "set-project-parent", |
| "set-reviewers", |
| "set-topic", |
| "stream-events", |
| "test-submit"); |
| |
| private static final ImmutableList<String> EMPTY = ImmutableList.of(); |
| private static final ImmutableMap<String, List<String>> MASTER_COMMANDS = |
| ImmutableMap.<String, List<String>>builder() |
| .put("kill", EMPTY) |
| .put("ps", EMPTY) |
| // TODO(dpursehouse): Add "scp" and "suexec" |
| .put( |
| "gerrit", |
| Streams.concat(COMMON_ROOT_COMMANDS.stream(), MASTER_ONLY_ROOT_COMMANDS.stream()) |
| .sorted() |
| .collect(toImmutableList())) |
| .put( |
| "gerrit index", |
| ImmutableList.of( |
| "changes", "changes-in-project")) // "activate" and "start" are not included |
| .put("gerrit logging", ImmutableList.of("ls", "set")) |
| .put( |
| "gerrit plugin", |
| ImmutableList.of("add", "enable", "install", "ls", "reload", "remove", "rm")) |
| .put("gerrit test-submit", ImmutableList.of("rule", "type")) |
| .put("gerrit sequence", ImmutableList.of("set", "show")) |
| .build(); |
| |
| private static final ImmutableMap<String, List<String>> SLAVE_COMMANDS = |
| ImmutableMap.of( |
| "kill", |
| EMPTY, |
| "gerrit", |
| COMMON_ROOT_COMMANDS, |
| "gerrit plugin", |
| ImmutableList.of("add", "enable", "install", "ls", "reload", "remove", "rm")); |
| |
| @Test |
| @Sandboxed |
| public void sshCommandCanBeExecuted() throws Exception { |
| testCommandExecution(MASTER_COMMANDS); |
| |
| restartAsSlave(); |
| testCommandExecution(SLAVE_COMMANDS); |
| } |
| |
| private void testCommandExecution(Map<String, List<String>> commands) throws Exception { |
| for (String root : commands.keySet()) { |
| List<String> cmds = commands.get(root); |
| if (cmds.isEmpty()) { |
| testCommandExecution(root); |
| } else { |
| for (String cmd : cmds) { |
| testCommandExecution(String.format("%s %s", root, cmd)); |
| } |
| } |
| } |
| } |
| |
| private void testCommandExecution(String cmd) throws Exception { |
| // We can't assert that adminSshSession.hasError() is false, because using the --help |
| // option causes the usage info to be written to stderr. Instead, we assert on the |
| // content of the stderr, which will always start with "gerrit command" when the --help |
| // option is used. |
| logger.atFine().log(cmd); |
| adminSshSession.exec(String.format("%s --help", cmd)); |
| String response = adminSshSession.getError(); |
| assertWithMessage(String.format("command %s failed: %s", cmd, response)) |
| .that(response) |
| .startsWith(cmd); |
| } |
| |
| @Test |
| public void nonExistingCommandFails() throws Exception { |
| adminSshSession.exec("gerrit non-existing-command --help"); |
| assertThat(adminSshSession.getError()) |
| .startsWith("fatal: gerrit: non-existing-command: not found"); |
| } |
| |
| @Test |
| @Sandboxed |
| public void listCommands() throws Exception { |
| adminSshSession.exec("gerrit --help"); |
| List<String> commands = parseCommandsFromGerritHelpText(adminSshSession.getError()); |
| assertThat(commands).containsExactlyElementsIn(MASTER_COMMANDS.get("gerrit")).inOrder(); |
| |
| restartAsSlave(); |
| adminSshSession.exec("gerrit --help"); |
| commands = parseCommandsFromGerritHelpText(adminSshSession.getError()); |
| assertThat(commands).containsExactlyElementsIn(SLAVE_COMMANDS.get("gerrit")).inOrder(); |
| } |
| |
| @Test |
| @Sandboxed |
| public void showConnections() throws Exception { |
| Spliterator<String> connectionsOutput = |
| getOutputLines(adminSshSession.exec("gerrit show-connections")); |
| |
| assertThat(findConnectionsInOutput(connectionsOutput, "user")).hasSize(1); |
| } |
| |
| @Test |
| @Sandboxed |
| public void cloeConnections() throws Exception { |
| List<String> connectionsOutput = |
| findConnectionsInOutput( |
| getOutputLines(adminSshSession.exec("gerrit show-connections")), "user"); |
| String connectionId = |
| Splitter.on(" ") |
| .trimResults() |
| .omitEmptyStrings() |
| .split(connectionsOutput.get(0)) |
| .iterator() |
| .next(); |
| |
| String closeConnectionOutput = adminSshSession.exec("gerrit close-connection " + connectionId); |
| assertThat(closeConnectionOutput).contains(connectionId); |
| |
| assertThat( |
| findConnectionsInOutput( |
| getOutputLines(adminSshSession.exec("gerrit show-connections")), "user")) |
| .isEmpty(); |
| } |
| |
| private List<String> parseCommandsFromGerritHelpText(String helpText) { |
| List<String> commands = new ArrayList<>(); |
| |
| String[] lines = helpText.split("\\n"); |
| |
| // Skip all lines including the line starting with "Available commands" |
| int row = 0; |
| do { |
| row++; |
| } while (row < lines.length && !lines[row - 1].startsWith("Available commands")); |
| |
| // Skip all empty lines |
| while (lines[row].trim().isEmpty()) { |
| row++; |
| } |
| |
| // Parse commands from all lines that are indented (start with a space) |
| while (row < lines.length && lines[row].startsWith(" ")) { |
| String line = lines[row].trim(); |
| // Abort on empty line |
| if (line.isEmpty()) { |
| break; |
| } |
| |
| // Cut off command description if there is one |
| int endOfCommand = line.indexOf(' '); |
| commands.add(endOfCommand > 0 ? line.substring(0, line.indexOf(' ')) : line); |
| row++; |
| } |
| |
| return commands; |
| } |
| |
| private List<String> findConnectionsInOutput(Spliterator<String> connectionsOutput, String user) { |
| List<String> connections = |
| StreamSupport.stream(connectionsOutput, false) |
| .filter(s -> s.contains("localhost") && s.contains(user)) |
| .collect(Collectors.toList()); |
| return connections; |
| } |
| |
| private Spliterator<String> getOutputLines(String output) throws Exception { |
| return Splitter.on("\n").trimResults().omitEmptyStrings().split(output).spliterator(); |
| } |
| } |