diff options
5 files changed, 210 insertions, 103 deletions
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainer.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainer.java index 42bbe7e67e0..18ce6395ed3 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainer.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainer.java @@ -7,11 +7,17 @@ import com.yahoo.vespa.hosted.node.admin.component.Environment; import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.Acl; import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.task.util.file.LineEdit; +import com.yahoo.vespa.hosted.node.admin.task.util.file.LineEditor; import com.yahoo.vespa.hosted.node.admin.task.util.network.IPAddresses; import com.yahoo.vespa.hosted.node.admin.task.util.network.IPVersion; import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -51,21 +57,14 @@ public class AclMaintainer implements Runnable { private void applyRedirect(Container container, InetAddress address) { IPVersion ipVersion = IPVersion.get(address); - - String redirectStatements = String.join("\n" - , "-P PREROUTING ACCEPT" - , "-P INPUT ACCEPT" - , "-P OUTPUT ACCEPT" - , "-P POSTROUTING ACCEPT" - , "-A OUTPUT -d " + InetAddresses.toAddrString(address) + ipVersion.singleHostCidr() + " -j REDIRECT"); - - IPTablesRestore.syncTableLogOnError(dockerOperations, container.name, ipVersion, "nat", redirectStatements); + String redirectRule = "-A OUTPUT -d " + InetAddresses.toAddrString(address) + ipVersion.singleHostCidr() + " -j REDIRECT"; + IPTablesEditor.editLogOnError(dockerOperations, container.name, ipVersion, "nat", NatTableLineEditor.from(redirectRule)); } private void apply(Container container, Acl acl) { // Apply acl to the filter table - IPTablesRestore.syncTableFlushOnError(dockerOperations, container.name, IPVersion.IPv6, "filter", acl.toRules(IPVersion.IPv6)); - IPTablesRestore.syncTableFlushOnError(dockerOperations, container.name, IPVersion.IPv4, "filter", acl.toRules(IPVersion.IPv4)); + IPTablesEditor.editFlushOnError(dockerOperations, container.name, IPVersion.IPv6, "filter", FilterTableLineEditor.from(acl, IPVersion.IPv6)); + IPTablesEditor.editFlushOnError(dockerOperations, container.name, IPVersion.IPv4, "filter", FilterTableLineEditor.from(acl, IPVersion.IPv4)); // Apply redirect to the nat table if (this.environment.getCloud().equals("AWS")) { @@ -93,4 +92,81 @@ public class AclMaintainer implements Runnable { log.error("Failed to configure ACLs", t); } } + + /** + * An editor that assumes all lines are exactly as the the wanted rules + */ + private static class FilterTableLineEditor implements LineEditor { + + private final List<String> wantedRules; + private boolean removeRemaining = false; + + FilterTableLineEditor(List<String> wantedRules) { + this.wantedRules = new ArrayList<>(wantedRules); + } + + static FilterTableLineEditor from(Acl acl, IPVersion ipVersion) { + List<String> rules = Arrays.asList(acl.toRules(ipVersion).split("\n")); + return new FilterTableLineEditor(rules); + } + + @Override + public LineEdit edit(String line) { + if (removeRemaining) { + return LineEdit.remove(); + } + if (wantedRules.indexOf(line) == 0) { + wantedRules.remove(line); + return LineEdit.none(); + } else { + removeRemaining = true; + return LineEdit.remove(); + } + } + + @Override + public List<String> onComplete() { + return this.wantedRules; + } + } + + /** + * An editor that only cares about the REDIRECT statement + */ + private static class NatTableLineEditor implements LineEditor { + + private final String redirectRule; + private boolean redirectExists; + + NatTableLineEditor(String redirectRule) { + this.redirectRule = redirectRule; + } + + static NatTableLineEditor from(String redirectRule) { + return new NatTableLineEditor(redirectRule); + } + + @Override + public LineEdit edit(String line) { + if (line.endsWith("REDIRECT")) { + if (redirectExists) { + // Only allow one redirect rule + return LineEdit.remove(); + } else { + redirectExists = true; + if (line.equals(redirectRule)) { + return LineEdit.none(); + } else { + return LineEdit.replaceWith(redirectRule); + } + } + } else return LineEdit.none(); + } + + @Override + public List<String> onComplete() { + if (redirectExists) return new ArrayList<>(); + return Collections.singletonList(redirectRule); + } + } } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/IPTablesEditor.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/IPTablesEditor.java new file mode 100644 index 00000000000..454f0a83ebe --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/IPTablesEditor.java @@ -0,0 +1,87 @@ +package com.yahoo.vespa.hosted.node.admin.maintenance.acl; + +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.ProcessResult; +import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; +import com.yahoo.vespa.hosted.node.admin.task.util.file.Editor; +import com.yahoo.vespa.hosted.node.admin.task.util.file.LineEditor; +import com.yahoo.vespa.hosted.node.admin.task.util.network.IPVersion; +import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class IPTablesEditor { + + private static final PrefixLogger log = PrefixLogger.getNodeAdminLogger(AclMaintainer.class); + + public static boolean editFlushOnError(DockerOperations dockerOperations, ContainerName containerName, IPVersion ipVersion, String table, LineEditor lineEditor) { + return edit(dockerOperations, containerName, ipVersion, table, lineEditor, true); + } + + public static boolean editLogOnError(DockerOperations dockerOperations, ContainerName containerName, IPVersion ipVersion, String table, LineEditor lineEditor) { + return edit(dockerOperations, containerName, ipVersion, table, lineEditor, false); + } + + private static boolean edit(DockerOperations dockerOperations, ContainerName containerName, IPVersion ipVersion, String table, LineEditor lineEditor, boolean flush) { + Editor editor = new Editor(ipVersion.iptablesCmd() + " table: " + table, + listTable(dockerOperations, containerName, ipVersion, table), + restoreTable(dockerOperations, containerName, ipVersion, table, flush), lineEditor); + return editor.edit(log::info); + } + + private static Supplier<List<String>> listTable(DockerOperations dockerOperations, ContainerName containerName, IPVersion ipVersion, String table) { + return () -> { + ProcessResult currentRulesResult = + dockerOperations.executeCommandInNetworkNamespace(containerName, ipVersion.iptablesCmd(), "-S", "-t", table); + return Arrays.stream(currentRulesResult.getOutput().split("\n")) + .collect(Collectors.toList()); + }; + } + + private static Consumer<List<String>> restoreTable(DockerOperations dockerOperations, ContainerName containerName, IPVersion ipVersion, String table, boolean flush) { + return list -> { + File file = null; + try { + String rules = String.join("\n", list); + file = writeTempFile(ipVersion.name(), "*" + table + "\n" + rules + "\nCOMMIT\n"); + dockerOperations.executeCommandInNetworkNamespace(containerName, ipVersion.iptablesRestore(), file.getAbsolutePath()); + } catch (Exception e) { + if (flush) { + log.error("Exception occurred while syncing iptable " + table + " for " + containerName.asString() + ", attempting rollback", e); + try { + dockerOperations.executeCommandInNetworkNamespace(containerName, ipVersion.iptablesCmd(), "-F", "-t", table); + } catch (Exception ne) { + log.error("Rollback of table " + table + " for " + containerName.asString() + " failed, giving up", ne); + } + } else { + log.warning("Unable to sync iptables for " + table, e); + } + } finally { + if (file != null) { + file.delete(); + } + } + }; + } + + private static File writeTempFile(String postfix, String content) { + try { + Path path = Files.createTempFile("iptables-restore", "." + postfix); + File file = path.toFile(); + Files.write(path, content.getBytes(StandardCharsets.UTF_8)); + file.deleteOnExit(); + return file; + } catch (IOException e) { + throw new RuntimeException("Unable to write restore file for iptables.", e); + } + } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/IPTablesRestore.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/IPTablesRestore.java deleted file mode 100644 index 20bd50d0892..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/IPTablesRestore.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.yahoo.vespa.hosted.node.admin.maintenance.acl; - -import com.yahoo.vespa.hosted.dockerapi.ContainerName; -import com.yahoo.vespa.hosted.dockerapi.ProcessResult; -import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; -import com.yahoo.vespa.hosted.node.admin.task.util.network.IPVersion; -import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; - -/** - * Utility class to sync rules for a given iptables table in a container. - * - * @author smorgrav - */ -public class IPTablesRestore { - - private static final PrefixLogger log = PrefixLogger.getNodeAdminLogger(AclMaintainer.class); - - public static void syncTableFlushOnError(DockerOperations dockerOperations, ContainerName containerName, IPVersion ipVersion, String table, String rules) { - syncTable(dockerOperations, containerName, ipVersion, table, rules, true); - } - - public static void syncTableLogOnError(DockerOperations dockerOperations, ContainerName containerName, IPVersion ipVersion, String table, String rules) { - syncTable(dockerOperations, containerName, ipVersion, table, rules, false); - } - - private static void syncTable(DockerOperations dockerOperations, ContainerName containerName, IPVersion ipVersion, String table, String rules, boolean flush) { - File file = null; - try { - // Get current rules for table - ProcessResult currentRulesResult = - dockerOperations.executeCommandInNetworkNamespace(containerName, ipVersion.iptablesCmd(), "-S", "-t", table); - String currentRules = currentRulesResult.getOutput(); - - // Compare and apply wanted if different - if (!equalsWhenIgnoreSpaceAndCase(rules, currentRules)) { - log.info(ipVersion.iptablesCmd() + " table: " + table + " differs. Wanted:\n" + rules + "\nGot\n" + currentRules); - file = writeTempFile(ipVersion.name(), "*" + table + "\n" + rules + "\nCOMMIT\n"); - dockerOperations.executeCommandInNetworkNamespace(containerName, ipVersion.iptablesRestore(), file.getAbsolutePath()); - } - } catch (Exception e) { - if (flush) { - log.error("Exception occurred while syncing iptable " + table + " for " + containerName.asString() + ", attempting rollback", e); - try { - dockerOperations.executeCommandInNetworkNamespace(containerName, ipVersion.iptablesCmd(), "-F", "-t", table); - } catch (Exception ne) { - log.error("Rollback of table " + table + " for " + containerName.asString() + " failed, giving up", ne); - } - } else { - log.warning("Unable to sync iptables for " + table, e); - } - } finally { - if (file != null) { - file.delete(); - } - } - } - - private static File writeTempFile(String postfix, String content) { - try { - Path path = Files.createTempFile("iptables-restore", "." + postfix); - File file = path.toFile(); - Files.write(path, content.getBytes(StandardCharsets.UTF_8)); - file.deleteOnExit(); - return file; - } catch (IOException e) { - throw new RuntimeException("Unable to write restore file for iptables.", e); - } - } - - /** - * to be agnostic to potential variances in output (and simplify test cases) - */ - private static boolean equalsWhenIgnoreSpaceAndCase(String a, String b) { - return a.trim().replaceAll("\\s+", " ").equalsIgnoreCase(b.trim().replaceAll("\\s+", " ")); - } -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/Editor.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/Editor.java index 83954551905..3d4dbcca0e6 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/Editor.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/Editor.java @@ -9,6 +9,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.logging.Logger; import static com.yahoo.vespa.hosted.node.admin.task.util.file.IOExceptionUtil.uncheck; @@ -24,20 +26,36 @@ public class Editor { private static int maxLength = 300; - private final Path path; + private final Supplier<List<String>> supplier; + private final Consumer<List<String>> consumer; + private final String name; private final LineEditor editor; public Editor(Path path, LineEditor editor) { - this.path = path; - this.editor = editor; + this(path.toString(), + () -> uncheck(() -> Files.readAllLines(path, ENCODING)), + (newLines) -> uncheck(() -> Files.write(path, newLines, ENCODING)), + editor); } /** - * Read the file which must be encoded in UTF-8, use the LineEditor to edit it, - * and any modifications were done write it back and return true. + * @param name The name of what is being edited - used in logging + * @param supplier Supplies the editor with a list of lines to edit + * @param consumer Consumes the lines to presist if any changes is detected + * @param editor The line operations to execute on the lines supplied */ - public boolean converge(TaskContext context) { - List<String> lines = uncheck(() -> Files.readAllLines(path, ENCODING)); + public Editor(String name, + Supplier<List<String>> supplier, + Consumer<List<String>> consumer, + LineEditor editor) { + this.supplier = supplier; + this.consumer = consumer; + this.name = name; + this.editor = editor; + } + + public boolean edit(Consumer<String> logConsumer) { + List<String> lines = supplier.get(); List<String> newLines = new ArrayList<>(); StringBuilder diff = new StringBuilder(); boolean modified = false; @@ -80,11 +98,19 @@ public class Editor { } String diffDescription = diffTooLarge(diff) ? "" : ":\n" + diff.toString(); - context.recordSystemModification(logger, "Patching file " + path + diffDescription); - uncheck(() -> Files.write(path, newLines, ENCODING)); + logConsumer.accept("Patching " + name + diffDescription); + consumer.accept(newLines); return true; } + /** + * Read the file which must be encoded in UTF-8, use the LineEditor to edit it, + * and any modifications were done write it back and return true. + */ + public boolean converge(TaskContext context) { + return this.edit(line -> context.recordSystemModification(logger, line)); + } + private static void maybeAdd(StringBuilder diff, List<String> lines) { for (String line : lines) { if (!diffTooLarge(diff)) { diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEdit.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEdit.java index 78e7a3e71b6..1745e63be56 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEdit.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEdit.java @@ -16,7 +16,7 @@ import static com.yahoo.vespa.hosted.node.admin.task.util.file.LineEdit.Type.NON */ @Immutable public class LineEdit { - enum Type { NONE, REPLACE } + public enum Type { NONE, REPLACE } public static LineEdit none() { return insert(Collections.emptyList(), Collections.emptyList()); } public static LineEdit remove() { return replaceWith(Collections.emptyList()); } |