summaryrefslogtreecommitdiffstats
path: root/node-admin
diff options
context:
space:
mode:
authorMartin Polden <martin.polden@gmail.com>2017-01-23 12:56:18 +0100
committerMartin Polden <martin.polden@gmail.com>2017-01-23 15:31:34 +0100
commitce461f93e3de16379df0a1b2e1d6f62ca8a8d25d (patch)
treec9ff6773f97184c4c4eb2db4c0f0745f5b514dd1 /node-admin
parentc40e51eca9838bbc75393333d4a93f5b942b1d65 (diff)
Add Docker ACL maintainer
Diffstat (limited to 'node-admin')
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/ContainerAclSpec.java31
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperations.java2
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImpl.java62
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/AclMaintainer.java111
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java23
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java3
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java17
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/GetAclResponse.java45
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/ComponentsProviderImpl.java5
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/IpTables.java34
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImplTest.java37
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ComponentsProviderWithMocks.java2
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerTester.java2
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java28
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/AclMaintainerTest.java110
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java2
16 files changed, 483 insertions, 31 deletions
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/ContainerAclSpec.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/ContainerAclSpec.java
new file mode 100644
index 00000000000..545dcd66666
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/ContainerAclSpec.java
@@ -0,0 +1,31 @@
+package com.yahoo.vespa.hosted.node.admin;
+
+/**
+ * An ACL specification for a container.
+ *
+ * @author mpolden
+ */
+public class ContainerAclSpec {
+
+ private final String hostname;
+ private final String ipAddress;
+ private final String trustedBy;
+
+ public ContainerAclSpec(String hostname, String ipAddress, String trustedBy) {
+ this.hostname = hostname;
+ this.ipAddress = ipAddress;
+ this.trustedBy = trustedBy;
+ }
+
+ public String hostname() {
+ return hostname;
+ }
+
+ public String ipAddress() {
+ return ipAddress;
+ }
+
+ public String trustedBy() {
+ return trustedBy;
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperations.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperations.java
index 5de85d3b506..52b5bede912 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperations.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperations.java
@@ -27,6 +27,8 @@ public interface DockerOperations {
void executeCommandInContainer(ContainerName containerName, String[] command);
+ void executeCommandInNetworkNamespace(ContainerName containerName, String[] command);
+
void resumeNode(ContainerName containerName);
void restartServicesOnNode(ContainerName containerName);
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImpl.java
index 28d0a5bafa9..73d66d685a9 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImpl.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImpl.java
@@ -18,6 +18,7 @@ import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec;
import com.yahoo.vespa.hosted.node.admin.util.Environment;
import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger;
+import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Inet6Address;
import java.net.InetAddress;
@@ -29,6 +30,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -76,12 +78,18 @@ public class DockerOperationsImpl implements DockerOperations {
private final Docker docker;
private final Environment environment;
+ private final Consumer<List<String>> commandExecutor;
private GaugeWrapper numberOfRunningContainersGauge;
public DockerOperationsImpl(Docker docker, Environment environment, MetricReceiverWrapper metricReceiver) {
+ this(docker, environment, metricReceiver, DockerOperationsImpl::runCommand);
+ }
+
+ DockerOperationsImpl(Docker docker, Environment environment, MetricReceiverWrapper metricReceiver, Consumer<List<String>> commandExecutor) {
this.docker = docker;
this.environment = environment;
setMetrics(metricReceiver);
+ this.commandExecutor = commandExecutor;
}
@Override
@@ -262,7 +270,7 @@ public class DockerOperationsImpl implements DockerOperations {
for (int retry = 0; retry < 30; ++retry) {
try {
- runCommand(command);
+ commandExecutor.accept(command);
logger.info("Done setting up network");
return;
} catch (Exception e) {
@@ -278,18 +286,6 @@ public class DockerOperationsImpl implements DockerOperations {
}
}
- private void runCommand(final List<String> command) throws Exception {
- ProcessBuilder builder = new ProcessBuilder(command);
- builder.redirectErrorStream(true);
- Process process = builder.start();
-
- String output = CharStreams.toString(new InputStreamReader(process.getInputStream()));
- int resultCode = process.waitFor();
- if (resultCode != 0) {
- throw new Exception("Command " + Joiner.on(' ').join(command) + " failed: " + output);
- }
- }
-
@Override
public void scheduleDownloadOfImage(final ContainerNodeSpec nodeSpec, Runnable callback) {
PrefixLogger logger = PrefixLogger.getNodeAgentLogger(DockerOperationsImpl.class, nodeSpec.containerName);
@@ -331,6 +327,31 @@ public class DockerOperationsImpl implements DockerOperations {
}
@Override
+ public void executeCommandInNetworkNamespace(ContainerName containerName, String[] command) {
+ final PrefixLogger logger = PrefixLogger.getNodeAgentLogger(DockerOperationsImpl.class, containerName);
+ final Docker.ContainerInfo containerInfo = docker.inspectContainer(containerName)
+ .orElseThrow(() -> new RuntimeException("Container " + containerName + " does not exist"));
+ final Integer containerPid = containerInfo.getPid()
+ .orElseThrow(() -> new RuntimeException("Container " + containerName + " isn't running (pid not found)"));
+
+ final List<String> wrappedCommand = new LinkedList<>();
+ wrappedCommand.add("sudo");
+ wrappedCommand.add("-n"); // Run non-interactively and fail if a password is required
+ wrappedCommand.add("nsenter");
+ wrappedCommand.add(String.format("--net=/host/proc/%d/ns/net", containerPid));
+ wrappedCommand.add("--");
+ wrappedCommand.addAll(Arrays.asList(command));
+
+ try {
+ commandExecutor.accept(wrappedCommand);
+ } catch (Exception e) {
+ logger.error(String.format("Failed to execute %s in network namespace for %s (PID = %d)",
+ Arrays.toString(command), containerName.asString(), containerPid));
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
public void resumeNode(ContainerName containerName) {
executeCommandInContainer(containerName, RESUME_NODE_COMMAND);
}
@@ -370,4 +391,19 @@ public class DockerOperationsImpl implements DockerOperations {
// Some containers could already be running, count them and initialize to that value
numberOfRunningContainersGauge.sample(getAllManagedContainers().size());
}
+
+ private static void runCommand(final List<String> command) {
+ ProcessBuilder builder = new ProcessBuilder(command);
+ builder.redirectErrorStream(true);
+ try {
+ Process process = builder.start();
+ String output = CharStreams.toString(new InputStreamReader(process.getInputStream()));
+ int resultCode = process.waitFor();
+ if (resultCode != 0) {
+ throw new RuntimeException("Command " + Joiner.on(' ').join(command) + " failed: " + output);
+ }
+ } catch (IOException|InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/AclMaintainer.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/AclMaintainer.java
new file mode 100644
index 00000000000..4deefff44c5
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/AclMaintainer.java
@@ -0,0 +1,111 @@
+package com.yahoo.vespa.hosted.node.admin.maintenance;
+
+import com.google.common.net.InetAddresses;
+import com.yahoo.net.HostName;
+import com.yahoo.vespa.hosted.dockerapi.Container;
+import com.yahoo.vespa.hosted.dockerapi.ContainerName;
+import com.yahoo.vespa.hosted.node.admin.ContainerAclSpec;
+import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations;
+import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository;
+import com.yahoo.vespa.hosted.node.admin.util.IpTables;
+import com.yahoo.vespa.hosted.node.admin.util.IpTables.Policy;
+import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger;
+
+import java.net.Inet6Address;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+/**
+ * The responsibility of this class is to configure ACLs for all running containers. The ACLs are fetched from the Node
+ * repository. Based on those ACLs, iptables commands are created and then executed in each of the containers network
+ * namespace.
+ *
+ * If a rule cannot be configured (e.g. iptables process execution fails), a rollback is attempted by setting the
+ * default policy to ACCEPT which will allow any traffic. The configuration will be retried the next time the
+ * maintainer runs.
+ *
+ * The ACL maintainer does not handle IPv4 addresses and is thus only intended to configure ACLs for IPv6-only
+ * containers (e.g. any container, except node-admin).
+ *
+ * @author mpolden
+ */
+public class AclMaintainer implements Runnable {
+
+ private static final PrefixLogger log = PrefixLogger.getNodeAdminLogger(AclMaintainer.class);
+
+ private final DockerOperations dockerOperations;
+ private final NodeRepository nodeRepository;
+ private final Supplier<String> nodeAdminHostnameSupplier;
+
+ public AclMaintainer(DockerOperations dockerOperations, NodeRepository nodeRepository) {
+ this(dockerOperations, nodeRepository, HostName::getLocalhost);
+ }
+
+ AclMaintainer(DockerOperations dockerOperations, NodeRepository nodeRepository,
+ Supplier<String> nodeAdminHostnameSupplier) {
+ this.dockerOperations = dockerOperations;
+ this.nodeRepository = nodeRepository;
+ this.nodeAdminHostnameSupplier = nodeAdminHostnameSupplier;
+ }
+
+ private void applyAcl(ContainerName containerName, List<ContainerAclSpec> aclSpecs) {
+ try {
+ dockerOperations.executeCommandInNetworkNamespace(containerName, IpTables.flushChain());
+ aclSpecs.stream()
+ .map(ContainerAclSpec::ipAddress)
+ .filter(ipAddress -> {
+ final boolean isIpv6 = isIpv6(ipAddress);
+ if (!isIpv6) {
+ log.warning("Skipping unexpected IPv4 address in ACL configuration: " + ipAddress);
+ }
+ return isIpv6;
+ })
+ .forEach(ipAddress -> dockerOperations.executeCommandInNetworkNamespace(containerName,
+ IpTables.allowFromAddress(ipAddress)));
+ dockerOperations.executeCommandInNetworkNamespace(containerName, IpTables.chainPolicy(Policy.DROP));
+ } catch (Exception e) {
+ log.error("Exception occurred while configuring ACLs, attempting rollback", e);
+ try {
+ dockerOperations.executeCommandInNetworkNamespace(containerName, IpTables.chainPolicy(Policy.ACCEPT));
+ } catch (Exception ne) {
+ log.error("Rollback failed, giving up", ne);
+ }
+ }
+ }
+
+ private void configureAcls() {
+ final List<ContainerAclSpec> aclSpecs = nodeRepository.getContainerAclSpecs(nodeAdminHostnameSupplier.get());
+ final Map<String, List<ContainerAclSpec>> aclSpecsGroupedByHostname = aclSpecs.stream()
+ .collect(Collectors.groupingBy(ContainerAclSpec::trustedBy));
+
+ for (Map.Entry<String, List<ContainerAclSpec>> entry : aclSpecsGroupedByHostname.entrySet()) {
+ final String hostname = entry.getKey();
+ final Optional<Container> container = dockerOperations.getContainer(hostname);
+ if (!container.isPresent()) {
+ log.warning("Got ACL for hostname " + hostname + ", but no container with that hostname exist");
+ continue;
+ }
+ if (!container.get().isRunning) {
+ log.info("Skipping ACL configuration for stopped container " + container.get().hostname);
+ continue;
+ }
+ applyAcl(container.get().name, entry.getValue());
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ configureAcls();
+ } catch (Throwable t) {
+ log.error("Failed to configure ACLs", t);
+ }
+ }
+
+ private static boolean isIpv6(String ipAddress) {
+ return InetAddresses.forString(ipAddress) instanceof Inet6Address;
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java
index 3356d36f9b7..391368642ee 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java
@@ -3,14 +3,15 @@ package com.yahoo.vespa.hosted.node.admin.nodeadmin;
import com.yahoo.collections.Pair;
import com.yahoo.net.HostName;
+import com.yahoo.vespa.hosted.dockerapi.Container;
import com.yahoo.vespa.hosted.dockerapi.ContainerName;
import com.yahoo.vespa.hosted.dockerapi.metrics.CounterWrapper;
import com.yahoo.vespa.hosted.dockerapi.metrics.Dimensions;
import com.yahoo.vespa.hosted.dockerapi.metrics.GaugeWrapper;
import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper;
import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec;
-import com.yahoo.vespa.hosted.dockerapi.Container;
import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations;
+import com.yahoo.vespa.hosted.node.admin.maintenance.AclMaintainer;
import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer;
import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgent;
import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger;
@@ -31,8 +32,6 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-
/**
* Administers a host (for now only docker hosts) and its nodes (docker containers nodes).
*
@@ -40,11 +39,12 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
*/
public class NodeAdminImpl implements NodeAdmin {
private static final PrefixLogger logger = PrefixLogger.getNodeAdminLogger(NodeAdmin.class);
- private final ScheduledExecutorService metricsFetcherScheduler = Executors.newScheduledThreadPool(1);
+ private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private final DockerOperations dockerOperations;
private final Function<String, NodeAgent> nodeAgentFactory;
private final Optional<StorageMaintainer> storageMaintainer;
+ private final Optional<AclMaintainer> aclMaintainer;
private AtomicBoolean frozen = new AtomicBoolean(false);
private final Map<String, NodeAgent> nodeAgents = new HashMap<>();
@@ -56,11 +56,12 @@ public class NodeAdminImpl implements NodeAdmin {
private CounterWrapper numberOfUnhandledExceptionsInNodeAgent;
public NodeAdminImpl(final DockerOperations dockerOperations, final Function<String, NodeAgent> nodeAgentFactory,
- final Optional<StorageMaintainer> storageMaintainer, int nodeAgentScanIntervalMillis,
- final MetricReceiverWrapper metricReceiver) {
+ final Optional<StorageMaintainer> storageMaintainer, final int nodeAgentScanIntervalMillis,
+ final MetricReceiverWrapper metricReceiver, final Optional<AclMaintainer> aclMaintainer) {
this.dockerOperations = dockerOperations;
this.nodeAgentFactory = nodeAgentFactory;
this.storageMaintainer = storageMaintainer;
+ this.aclMaintainer = aclMaintainer;
this.nodeAgentScanIntervalMillis = nodeAgentScanIntervalMillis;
Dimensions dimensions = new Dimensions.Builder()
@@ -71,13 +72,15 @@ public class NodeAdminImpl implements NodeAdmin {
this.numberOfContainersInLoadImageState = metricReceiver.declareGauge(dimensions, "nodes.image.loading");
this.numberOfUnhandledExceptionsInNodeAgent = metricReceiver.declareCounter(dimensions, "nodes.unhandled_exceptions");
- metricsFetcherScheduler.scheduleWithFixedDelay(() -> {
+ scheduler.scheduleWithFixedDelay(() -> {
try {
nodeAgents.values().forEach(NodeAgent::updateContainerNodeMetrics);
} catch (Throwable e) {
logger.warning("Metric fetcher scheduler failed", e);
}
- }, 0, 30000, MILLISECONDS);
+ }, 0, 30000, TimeUnit.MILLISECONDS);
+
+ this.aclMaintainer.ifPresent(maintainer -> scheduler.scheduleAtFixedRate(maintainer, 30, 60, TimeUnit.SECONDS));
}
public void refreshContainersToRun(final List<ContainerNodeSpec> containersToRun) {
@@ -174,9 +177,9 @@ public class NodeAdminImpl implements NodeAdmin {
@Override
public void shutdown() {
- metricsFetcherScheduler.shutdown();
+ scheduler.shutdown();
try {
- if (! metricsFetcherScheduler.awaitTermination(30, TimeUnit.SECONDS)) {
+ if (! scheduler.awaitTermination(30, TimeUnit.SECONDS)) {
throw new RuntimeException("Did not manage to shutdown node-agent metrics update metricsFetcherScheduler.");
}
} catch (InterruptedException e) {
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java
index c6613610150..94a7d7d35b8 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java
@@ -1,6 +1,7 @@
// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.node.admin.noderepository;
+import com.yahoo.vespa.hosted.node.admin.ContainerAclSpec;
import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec;
import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes;
@@ -16,6 +17,8 @@ public interface NodeRepository {
Optional<ContainerNodeSpec> getContainerNodeSpec(String hostName);
+ List<ContainerAclSpec> getContainerAclSpecs(String hostName);
+
void updateNodeAttributes(String hostName, NodeAttributes nodeAttributes);
void markAsReady(String hostName);
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java
index d6272c40674..db14c3b62d8 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java
@@ -1,10 +1,12 @@
// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.node.admin.noderepository;
+import com.yahoo.vespa.hosted.node.admin.ContainerAclSpec;
import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec;
import com.yahoo.vespa.hosted.dockerapi.ContainerName;
import com.yahoo.vespa.hosted.dockerapi.DockerImage;
import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes;
+import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.GetAclResponse;
import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.GetNodesResponse;
import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.NodeReadyResponse;
import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.UpdateNodeAttributesRequestBody;
@@ -15,10 +17,12 @@ import com.yahoo.vespa.hosted.provision.Node;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* @author stiankri, dybis
@@ -79,6 +83,19 @@ public class NodeRepositoryImpl implements NodeRepository {
}
}
+ @Override
+ public List<ContainerAclSpec> getContainerAclSpecs(String hostName) {
+ try {
+ final String path = String.format("/nodes/v2/acl/%s?children=true", hostName);
+ final GetAclResponse response = requestExecutor.get(path, port, GetAclResponse.class);
+ return response.trustedNodes.stream()
+ .map(node -> new ContainerAclSpec(node.hostname, node.ipAddress, node.trustedBy))
+ .collect(Collectors.toList());
+ } catch (ConfigServerHttpRequestExecutor.NotFoundException e) {
+ return Collections.emptyList();
+ }
+ }
+
private static ContainerNodeSpec createContainerNodeSpec(GetNodesResponse.Node node)
throws IllegalArgumentException, NullPointerException {
Objects.requireNonNull(node.nodeState, "Unknown node state");
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/GetAclResponse.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/GetAclResponse.java
new file mode 100644
index 00000000000..8798323cf4d
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/GetAclResponse.java
@@ -0,0 +1,45 @@
+package com.yahoo.vespa.hosted.node.admin.noderepository.bindings;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This class represents a response from the /nodes/v2/acl/ API.
+ *
+ * @author mpolden
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class GetAclResponse {
+
+ @JsonProperty("trustedNodes")
+ public final List<Node> trustedNodes;
+
+ @JsonCreator
+ public GetAclResponse(@JsonProperty("trustedNodes") List<Node> trustedNodes) {
+ this.trustedNodes = trustedNodes == null ? Collections.emptyList() : trustedNodes;
+ }
+
+ public static class Node {
+
+ @JsonProperty("hostname")
+ public final String hostname;
+
+ @JsonProperty("ipAddress")
+ public final String ipAddress;
+
+ @JsonProperty("trustedBy")
+ public final String trustedBy;
+
+ @JsonCreator
+ public Node(@JsonProperty("hostname") String hostname, @JsonProperty("ipAddress") String ipAddress,
+ @JsonProperty("trustedBy") String trustedBy) {
+ this.hostname = hostname;
+ this.ipAddress = ipAddress;
+ this.trustedBy = trustedBy;
+ }
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/ComponentsProviderImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/ComponentsProviderImpl.java
index 5dffc1852d2..58095c14a9b 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/ComponentsProviderImpl.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/ComponentsProviderImpl.java
@@ -8,6 +8,7 @@ import com.yahoo.vespa.hosted.dockerapi.ContainerName;
import com.yahoo.vespa.hosted.dockerapi.Docker;
import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper;
import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations;
+import com.yahoo.vespa.hosted.node.admin.maintenance.AclMaintainer;
import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer;
import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdmin;
import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminImpl;
@@ -60,8 +61,10 @@ public class ComponentsProviderImpl implements ComponentsProvider {
final Function<String, NodeAgent> nodeAgentFactory =
(hostName) -> new NodeAgentImpl(hostName, nodeRepository, orchestrator, dockerOperations,
storageMaintainer, metricReceiver, environment);
+ final AclMaintainer aclMaintainer = new AclMaintainer(dockerOperations, nodeRepository);
+
final NodeAdmin nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, storageMaintainer,
- NODE_AGENT_SCAN_INTERVAL_MILLIS, metricReceiver);
+ NODE_AGENT_SCAN_INTERVAL_MILLIS, metricReceiver, Optional.of(aclMaintainer));
nodeAdminStateUpdater = new NodeAdminStateUpdater(nodeRepository, nodeAdmin, INITIAL_SCHEDULER_DELAY_MILLIS,
NODE_ADMIN_STATE_INTERVAL_MILLIS, orchestrator, baseHostName);
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/IpTables.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/IpTables.java
new file mode 100644
index 00000000000..eb154c098bc
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/IpTables.java
@@ -0,0 +1,34 @@
+package com.yahoo.vespa.hosted.node.admin.util;
+
+/**
+ * Utility class for creating iptables commands
+ *
+ * @author mpolden
+ */
+public class IpTables {
+
+ private static final String COMMAND = "ip6tables";
+
+ public static String[] allowFromAddress(String ipAddress) {
+ return new String[]{COMMAND, "-A", "INPUT", "-s", ipAddress, "-j", "ACCEPT"};
+ }
+
+ public static String[] chainPolicy(Policy policy) {
+ return new String[]{COMMAND, "-P", "INPUT", policy.target};
+ }
+
+ public static String[] flushChain() {
+ return new String[]{COMMAND, "-F", "INPUT"};
+ }
+
+ public enum Policy {
+ DROP("DROP"),
+ ACCEPT("ACCEPT");
+
+ private final String target;
+
+ Policy(String target) {
+ this.target = target;
+ }
+ }
+}
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImplTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImplTest.java
index b3b804ec7ad..2b68c869008 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImplTest.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImplTest.java
@@ -11,11 +11,17 @@ import org.hamcrest.CoreMatchers;
import org.junit.Test;
import org.mockito.InOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.Optional;
import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
-import static org.mockito.Matchers.*;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyVararg;
+import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@@ -123,4 +129,31 @@ public class DockerOperationsImplTest {
public void vespaVersionIsNotParsedFromUnexpectedContent() {
assertThat(DockerOperationsImpl.parseVespaVersion("No such command 'vespanodectl'"), CoreMatchers.is(Optional.empty()));
}
-} \ No newline at end of file
+
+ @Test
+ public void runsCommandInNetworkNamespace() {
+ ContainerName container = makeContainer("container-42", 42);
+ List<String> capturedArgs = new ArrayList<>();
+ DockerOperationsImpl dockerOperations = new DockerOperationsImpl(docker, environment,
+ new MetricReceiverWrapper(MetricReceiver.nullImplementation), capturedArgs::addAll);
+
+ dockerOperations.executeCommandInNetworkNamespace(container, new String[]{"iptables", "-nvL"});
+
+ assertEquals(Arrays.asList(
+ "sudo",
+ "-n",
+ "nsenter",
+ "--net=/host/proc/42/ns/net",
+ "--",
+ "iptables",
+ "-nvL"), capturedArgs);
+ }
+
+ private ContainerName makeContainer(String hostname, int pid) {
+ ContainerName containerName = new ContainerName(hostname);
+ Docker.ContainerInfo containerInfo = mock(Docker.ContainerInfo.class);
+ when(containerInfo.getPid()).thenReturn(Optional.of(pid));
+ when(docker.inspectContainer(eq(containerName))).thenReturn(Optional.of(containerInfo));
+ return containerName;
+ }
+}
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ComponentsProviderWithMocks.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ComponentsProviderWithMocks.java
index dff653ab94a..664d375ec09 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ComponentsProviderWithMocks.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ComponentsProviderWithMocks.java
@@ -34,7 +34,7 @@ public class ComponentsProviderWithMocks implements ComponentsProvider {
private final Function<String, NodeAgent> nodeAgentFactory =
(hostName) -> new NodeAgentImpl(hostName, nodeRepositoryMock, orchestratorMock,
dockerOperations, Optional.empty(), mr, environment);
- private NodeAdmin nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, Optional.empty(), 100, mr);
+ private NodeAdmin nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, Optional.empty(), 100, mr, Optional.empty());
@Override
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerTester.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerTester.java
index 01551b28c02..0912ccff814 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerTester.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerTester.java
@@ -59,7 +59,7 @@ public class DockerTester implements AutoCloseable {
final DockerOperations dockerOperations = new DockerOperationsImpl(dockerMock, environment, mr);
Function<String, NodeAgent> nodeAgentFactory = (hostName) -> new NodeAgentImpl(hostName, nodeRepositoryMock,
orchestratorMock, dockerOperations, Optional.of(storageMaintainer), mr, environment);
- nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, Optional.of(storageMaintainer), 100, mr);
+ nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, Optional.of(storageMaintainer), 100, mr, Optional.empty());
updater = new NodeAdminStateUpdater(nodeRepositoryMock, nodeAdmin, 1, 1, orchestratorMock, "basehostname");
}
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java
index 77f2ffe518a..75faf3bd484 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java
@@ -1,6 +1,7 @@
// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.node.admin.integrationTests;
+import com.yahoo.vespa.hosted.node.admin.ContainerAclSpec;
import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec;
import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes;
import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository;
@@ -8,7 +9,10 @@ import com.yahoo.vespa.hosted.provision.Node;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -18,11 +22,13 @@ import java.util.stream.Collectors;
* @author dybis
*/
public class NodeRepoMock implements NodeRepository {
- private List<ContainerNodeSpec> containerNodeSpecs = new ArrayList<>();
- private final CallOrderVerifier callOrderVerifier;
private static final Object monitor = new Object();
+ private List<ContainerNodeSpec> containerNodeSpecs = new ArrayList<>();
+ private final Map<String, List<ContainerAclSpec>> acls = new HashMap<>();
+ private final CallOrderVerifier callOrderVerifier;
+
public NodeRepoMock(CallOrderVerifier callOrderVerifier) {
this.callOrderVerifier = callOrderVerifier;
}
@@ -44,6 +50,14 @@ public class NodeRepoMock implements NodeRepository {
}
@Override
+ public List<ContainerAclSpec> getContainerAclSpecs(String hostName) {
+ synchronized (monitor) {
+ return Optional.ofNullable(acls.get(hostName))
+ .orElseGet(Collections::emptyList);
+ }
+ }
+
+ @Override
public void updateNodeAttributes(String hostName, NodeAttributes nodeAttributes) {
synchronized (monitor) {
callOrderVerifier.add("updateNodeAttributes with HostName: " + hostName + ", " + nodeAttributes);
@@ -96,4 +110,14 @@ public class NodeRepoMock implements NodeRepository {
return containerNodeSpecs.size();
}
}
+
+ public void addContainerAclSpecs(String hostname, List<ContainerAclSpec> containerAclSpecs) {
+ synchronized (monitor) {
+ if (this.acls.containsKey(hostname)) {
+ this.acls.get(hostname).addAll(containerAclSpecs);
+ } else {
+ this.acls.put(hostname, containerAclSpecs);
+ }
+ }
+ }
}
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/AclMaintainerTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/AclMaintainerTest.java
new file mode 100644
index 00000000000..d3849deeb67
--- /dev/null
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/AclMaintainerTest.java
@@ -0,0 +1,110 @@
+package com.yahoo.vespa.hosted.node.admin.maintenance;
+
+import com.yahoo.vespa.hosted.dockerapi.Container;
+import com.yahoo.vespa.hosted.dockerapi.ContainerName;
+import com.yahoo.vespa.hosted.dockerapi.DockerImage;
+import com.yahoo.vespa.hosted.node.admin.ContainerAclSpec;
+import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations;
+import com.yahoo.vespa.hosted.node.admin.integrationTests.CallOrderVerifier;
+import com.yahoo.vespa.hosted.node.admin.integrationTests.NodeRepoMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static org.mockito.AdditionalMatchers.aryEq;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class AclMaintainerTest {
+
+ private static final String NODE_ADMIN_HOSTNAME = "node-admin";
+
+ private AclMaintainer aclMaintainer;
+ private DockerOperations dockerOperations;
+ private NodeRepoMock nodeRepository;
+
+ @Before
+ public void before() {
+ this.dockerOperations = mock(DockerOperations.class);
+ this.nodeRepository = new NodeRepoMock(new CallOrderVerifier());
+ this.aclMaintainer = new AclMaintainer(dockerOperations, nodeRepository, () -> NODE_ADMIN_HOSTNAME);
+ }
+
+ @Test
+ public void configures_container_acl() {
+ Container container = makeContainer("container-1");
+ List<ContainerAclSpec> aclSpecs = makeAclSpecs(3, container.name);
+ nodeRepository.addContainerAclSpecs(NODE_ADMIN_HOSTNAME, aclSpecs);
+ aclMaintainer.run();
+ assertAclsApplied(container.name, aclSpecs);
+ }
+
+ @Test
+ public void does_not_configure_acl_for_stopped_container() {
+ Container stoppedContainer = makeContainer("container-1", false);
+ nodeRepository.addContainerAclSpecs(NODE_ADMIN_HOSTNAME, makeAclSpecs(1, stoppedContainer.name));
+ aclMaintainer.run();
+ verify(dockerOperations, never()).executeCommandInNetworkNamespace(any(), any());
+ }
+
+ @Test
+ public void rollback_is_attempted_when_applying_acl_fail() {
+ Container container = makeContainer("container-1");
+ nodeRepository.addContainerAclSpecs(NODE_ADMIN_HOSTNAME, makeAclSpecs(1, container.name));
+
+ doThrow(new RuntimeException("iptables command failed"))
+ .doNothing()
+ .when(dockerOperations)
+ .executeCommandInNetworkNamespace(any(), any());
+
+ aclMaintainer.run();
+
+ verify(dockerOperations).executeCommandInNetworkNamespace(
+ eq(container.name),
+ aryEq(new String[]{"ip6tables", "-P", "INPUT", "ACCEPT"})
+ );
+ }
+
+ private void assertAclsApplied(ContainerName containerName, List<ContainerAclSpec> containerAclSpecs) {
+ verify(dockerOperations).executeCommandInNetworkNamespace(
+ eq(containerName),
+ aryEq(new String[]{"ip6tables", "-F", "INPUT"})
+ );
+ containerAclSpecs.forEach(aclSpec -> verify(dockerOperations).executeCommandInNetworkNamespace(
+ eq(containerName),
+ aryEq(new String[]{"ip6tables", "-A", "INPUT", "-s", aclSpec.ipAddress(), "-j", "ACCEPT"})
+ ));
+ verify(dockerOperations).executeCommandInNetworkNamespace(
+ eq(containerName),
+ aryEq(new String[]{"ip6tables", "-P", "INPUT", "DROP"})
+ );
+ }
+
+ private Container makeContainer(String hostname) {
+ return makeContainer(hostname, true);
+ }
+
+ private Container makeContainer(String hostname, boolean running) {
+ final Container container = new Container(hostname, new DockerImage("mock"),
+ new ContainerName(hostname), running);
+ when(dockerOperations.getContainer(eq(hostname))).thenReturn(Optional.of(container));
+ return container;
+ }
+
+ private static List<ContainerAclSpec> makeAclSpecs(int count, ContainerName containerName) {
+ return IntStream.rangeClosed(1, count)
+ .mapToObj(i -> new ContainerAclSpec("node-" + i, "::" + i,
+ containerName.asString()))
+ .collect(Collectors.toList());
+ }
+
+}
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java
index a068ffc09f2..a7c5703f4cc 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java
@@ -51,7 +51,7 @@ public class NodeAdminImplTest {
final Function<String, NodeAgent> nodeAgentFactory = mock(NodeAgentFactory.class);
final NodeAdminImpl nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, Optional.empty(), 100,
- new MetricReceiverWrapper(MetricReceiver.nullImplementation));
+ new MetricReceiverWrapper(MetricReceiver.nullImplementation), Optional.empty());
final NodeAgent nodeAgent1 = mock(NodeAgentImpl.class);
final NodeAgent nodeAgent2 = mock(NodeAgentImpl.class);