summaryrefslogtreecommitdiffstats
path: root/node-admin
diff options
context:
space:
mode:
authorHåkon Hallingstad <hakon@oath.com>2018-02-08 01:45:37 +0100
committerHåkon Hallingstad <hakon@oath.com>2018-02-08 01:45:37 +0100
commit03a2d26265433a2f055a3bb2f470ba5400df7c44 (patch)
treec1edb2458a2a0bf893a79b6864cebb211cce523d /node-admin
parent74b3ef7b54e8ac8b0473c016185f1476a3fd3db4 (diff)
Add file editor
Diffstat (limited to 'node-admin')
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/Editor.java98
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/EditorFactory.java13
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEdit.java31
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEditor.java21
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/EditorTest.java76
5 files changed, 239 insertions, 0 deletions
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
new file mode 100644
index 00000000000..cfd7756ad0e
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/Editor.java
@@ -0,0 +1,98 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import com.yahoo.vespa.hosted.node.admin.component.TaskContext;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static com.yahoo.vespa.hosted.node.admin.task.util.file.IOExceptionUtil.uncheck;
+
+/**
+ * An editor meant to edit small line-based files like /etc/fstab.
+ *
+ * @author hakonhall
+ */
+public class Editor {
+ private static final Logger logger = Logger.getLogger(Editor.class.getName());
+ private static final Charset ENCODING = StandardCharsets.UTF_8;
+
+ private static int maxLength = 300;
+
+ private final Path path;
+ private final LineEditor editor;
+
+ public Editor(Path path, LineEditor editor) {
+ this.path = path;
+ this.editor = 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.
+ */
+ public boolean converge(TaskContext context) {
+ List<String> lines = uncheck(() -> Files.readAllLines(path, ENCODING));
+ List<String> newLines = new ArrayList<>();
+ StringBuilder diff = new StringBuilder();
+ boolean modified = false;
+
+ for (String line : lines) {
+ LineEdit edit = editor.edit(line);
+ switch (edit.getType()) {
+ case REMOVE:
+ modified = true;
+ maybeRemove(diff, line);
+ break;
+ case REPLACE:
+ modified = true;
+ String replacementLine = edit.replacementLine();
+ newLines.add(replacementLine);
+ maybeRemove(diff, line);
+ maybeAdd(diff, replacementLine);
+ break;
+ case NONE:
+ newLines.add(line);
+ break;
+ default: throw new IllegalArgumentException("Unknown EditType " + edit.getType());
+ }
+ }
+
+ List<String> linesToAppend = editor.onComplete();
+ if (!linesToAppend.isEmpty()) {
+ modified = true;
+ newLines.addAll(linesToAppend);
+ linesToAppend.forEach(line -> maybeAdd(diff, line));
+ }
+
+ if (!modified) {
+ return false;
+ }
+
+ String diffDescription = diffTooLarge(diff) ? "" : ":\n" + diff.toString();
+ context.recordSystemModification(logger, "Patching file " + path + diffDescription);
+ uncheck(() -> Files.write(path, newLines, ENCODING));
+ return true;
+ }
+
+ private static void maybeAdd(StringBuilder diff, String line) {
+ if (!diffTooLarge(diff)) {
+ diff.append('+').append(line).append('\n');
+ }
+ }
+
+ private static void maybeRemove(StringBuilder diff, String line) {
+ if (!diffTooLarge(diff)) {
+ diff.append('-').append(line).append('\n');
+ }
+ }
+
+ private static boolean diffTooLarge(StringBuilder diff) {
+ return diff.length() > maxLength;
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/EditorFactory.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/EditorFactory.java
new file mode 100644
index 00000000000..a167840a8a6
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/EditorFactory.java
@@ -0,0 +1,13 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import java.nio.file.Path;
+
+/**
+ * @author hakonhall
+ */
+public class EditorFactory {
+ public Editor create(Path path, LineEditor editor) {
+ return new Editor(path, editor);
+ }
+}
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
new file mode 100644
index 00000000000..d822b6fae80
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEdit.java
@@ -0,0 +1,31 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import javax.annotation.concurrent.Immutable;
+import java.util.Optional;
+
+import static com.yahoo.vespa.hosted.node.admin.task.util.file.LineEdit.Type.REMOVE;
+import static com.yahoo.vespa.hosted.node.admin.task.util.file.LineEdit.Type.REPLACE;
+import static com.yahoo.vespa.hosted.node.admin.task.util.file.LineEdit.Type.NONE;
+
+/**
+ * @author hakonhall
+ */
+@Immutable
+public class LineEdit {
+ enum Type { NONE, REPLACE, REMOVE}
+ private final Type type;
+ private final Optional<String> line;
+
+ public static LineEdit none() { return new LineEdit(NONE, Optional.empty()); }
+ public static LineEdit remove() { return new LineEdit(REMOVE, Optional.empty()); }
+ public static LineEdit replaceWith(String line) { return new LineEdit(REPLACE, Optional.of(line)); }
+
+ private LineEdit(Type type, Optional<String> newLine) {
+ this.type = type;
+ this.line = newLine;
+ }
+
+ public Type getType() { return type; }
+ public String replacementLine() { return line.get(); }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEditor.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEditor.java
new file mode 100644
index 00000000000..de43eb57074
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/LineEditor.java
@@ -0,0 +1,21 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import java.util.List;
+
+/**
+ * @author hakonhall
+ */
+public interface LineEditor {
+ /**
+ * @param line The line of a file.
+ * @return The edited line, or empty if the line should be removed.
+ */
+ LineEdit edit(String line);
+
+ /**
+ * Called after edit() has been called on all lines in the file.
+ * @return Lines to append to the file.
+ */
+ List<String> onComplete();
+}
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/EditorTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/EditorTest.java
new file mode 100644
index 00000000000..c89164e6cfb
--- /dev/null
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/EditorTest.java
@@ -0,0 +1,76 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import com.yahoo.vespa.hosted.node.admin.component.TaskContext;
+import com.yahoo.vespa.test.file.TestFileSystem;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.nio.file.FileSystem;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class EditorTest {
+ private final FileSystem fileSystem = TestFileSystem.create();
+ private final UnixPath path = new UnixPath(fileSystem.getPath("/file"));
+
+ @Test
+ public void testEdit() {
+ path.writeUtf8File("first\n" +
+ "second\n" +
+ "third\n");
+
+ LineEditor lineEditor = mock(LineEditor.class);
+ when(lineEditor.edit(any())).thenReturn(
+ LineEdit.none(), // don't edit the first line
+ LineEdit.remove(), // remove the second
+ LineEdit.replaceWith("replacement")); // replace the third
+
+ Editor editor = new Editor(path.toPath(), lineEditor);
+ TaskContext context = mock(TaskContext.class);
+
+ assertTrue(editor.converge(context));
+
+ verify(lineEditor, times(3)).edit(any());
+
+ // Verify the system modification message
+ ArgumentCaptor<String> modificationMessage = ArgumentCaptor.forClass(String.class);
+ verify(context).recordSystemModification(any(), modificationMessage.capture());
+ assertEquals(
+ "Patching file /file:\n-second\n-third\n+replacement\n",
+ modificationMessage.getValue());
+
+ // Verify the new contents of the file:
+ assertEquals("first\n" +
+ "replacement\n", path.readUtf8File());
+ }
+
+ @Test
+ public void noop() {
+ path.writeUtf8File("line\n");
+
+ LineEditor lineEditor = mock(LineEditor.class);
+ when(lineEditor.edit(any())).thenReturn(LineEdit.none());
+
+ Editor editor = new Editor(path.toPath(), lineEditor);
+ TaskContext context = mock(TaskContext.class);
+
+ assertFalse(editor.converge(context));
+
+ verify(lineEditor, times(1)).edit(any());
+
+ // Verify the system modification message
+ verify(context, times(0)).recordSystemModification(any(), any());
+
+ // Verify same contents
+ assertEquals("line\n", path.readUtf8File());
+ }
+} \ No newline at end of file