diff --git a/ZookeeperNodeMigration/.gitignore b/ZookeeperNodeMigration/.gitignore
new file mode 100755
index 0000000000..3d7c2414b1
--- /dev/null
+++ b/ZookeeperNodeMigration/.gitignore
@@ -0,0 +1,3 @@
+/nbproject/private/
+/build/
+/dist/
diff --git a/ZookeeperNodeMigration/build.xml b/ZookeeperNodeMigration/build.xml
new file mode 100755
index 0000000000..9705ad3562
--- /dev/null
+++ b/ZookeeperNodeMigration/build.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+ Builds, tests, and runs the project ZookeeperNodeMigration.
+
+
+
diff --git a/ZookeeperNodeMigration/docs/README.TXT b/ZookeeperNodeMigration/docs/README.TXT
new file mode 100755
index 0000000000..9f5b8f4ca9
--- /dev/null
+++ b/ZookeeperNodeMigration/docs/README.TXT
@@ -0,0 +1,23 @@
+To run the project from the command line, go to the folder that contains "ZookeeperNodeMigration.jar" and
+type the following:
+
+java -jar ZookeeperNodeMigration.jar
+
+To distribute this project, zip up the dist folder (including the lib folder)
+and distribute the ZIP file.
+
+Usage:
+ZookeeperNodeMigration input needs to be:
+[Input Zookeeper IP Address or Hostname] [Input Zookeeper Port Number] [Output Zookeeper IP Address or Hostname] [Output Zookeeper Port Number]
+
+For example, if you execute the following command from command line line, the Zookeeper
+nodes will get copied from Zookeeper server on localhost:9983 to Zookeeper server on localhost:19983 :
+
+java -jar ZookeeperNodeMigration.jar localhost 9983 localhost 19983
+
+
+If you do not have Java installed on the machine, you can use the packaged version of Java that is distributed along
+with Autopsy. For example:
+
+"C:\Program Files\Autopsy-4.16.0\jre\bin\java.exe" -jar ZookeeperNodeMigration.jar localhost 9983 localhost 19983
+
diff --git a/ZookeeperNodeMigration/manifest.mf b/ZookeeperNodeMigration/manifest.mf
new file mode 100755
index 0000000000..328e8e5bc3
--- /dev/null
+++ b/ZookeeperNodeMigration/manifest.mf
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+X-COMMENT: Main-Class will be added automatically by build
+
diff --git a/ZookeeperNodeMigration/nbproject/build-impl.xml b/ZookeeperNodeMigration/nbproject/build-impl.xml
new file mode 100755
index 0000000000..76af3c9f63
--- /dev/null
+++ b/ZookeeperNodeMigration/nbproject/build-impl.xml
@@ -0,0 +1,1770 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must set src.dir
+ Must set test.src.dir
+ Must set build.dir
+ Must set dist.dir
+ Must set build.classes.dir
+ Must set dist.javadoc.dir
+ Must set build.test.classes.dir
+ Must set build.test.results.dir
+ Must set build.classes.excludes
+ Must set dist.jar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must set javac.includes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No tests executed.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must set JVM to use for profiling in profiler.info.jvm
+ Must set profiler agent JVM arguments in profiler.info.jvmargs.agent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select some files in the IDE or set javac.includes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ To run this application from the command line without Ant, try:
+
+ java -jar "${dist.jar.resolved}"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set run.class
+
+
+
+ Must select one file in the IDE or set run.class
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set debug.class
+
+
+
+
+ Must select one file in the IDE or set debug.class
+
+
+
+
+ Must set fix.includes
+
+
+
+
+
+
+
+
+
+ This target only works when run from inside the NetBeans IDE.
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set profile.class
+ This target only works when run from inside the NetBeans IDE.
+
+
+
+
+
+
+
+
+ This target only works when run from inside the NetBeans IDE.
+
+
+
+
+
+
+
+
+
+
+
+
+ This target only works when run from inside the NetBeans IDE.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set run.class
+
+
+
+
+
+ Must select some files in the IDE or set test.includes
+
+
+
+
+ Must select one file in the IDE or set run.class
+
+
+
+
+ Must select one file in the IDE or set applet.url
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select some files in the IDE or set javac.includes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Some tests failed; see details above.
+
+
+
+
+
+
+
+
+ Must select some files in the IDE or set test.includes
+
+
+
+ Some tests failed; see details above.
+
+
+
+ Must select some files in the IDE or set test.class
+ Must select some method in the IDE or set test.method
+
+
+
+ Some tests failed; see details above.
+
+
+
+
+ Must select one file in the IDE or set test.class
+
+
+
+ Must select one file in the IDE or set test.class
+ Must select some method in the IDE or set test.method
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set applet.url
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set applet.url
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ZookeeperNodeMigration/nbproject/project.properties b/ZookeeperNodeMigration/nbproject/project.properties
new file mode 100755
index 0000000000..26892390a5
--- /dev/null
+++ b/ZookeeperNodeMigration/nbproject/project.properties
@@ -0,0 +1,107 @@
+annotation.processing.enabled=true
+annotation.processing.enabled.in.editor=false
+annotation.processing.processors.list=
+annotation.processing.run.all.processors=true
+annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output
+application.title=ZookeeperNodeMigration
+application.vendor=elivis
+build.classes.dir=${build.dir}/classes
+build.classes.excludes=**/*.java,**/*.form
+# This directory is removed when the project is cleaned:
+build.dir=build
+build.generated.dir=${build.dir}/generated
+build.generated.sources.dir=${build.dir}/generated-sources
+# Only compile against the classpath explicitly listed here:
+build.sysclasspath=ignore
+build.test.classes.dir=${build.dir}/test/classes
+build.test.results.dir=${build.dir}/test/results
+# Uncomment to specify the preferred debugger connection transport:
+#debug.transport=dt_socket
+debug.classpath=\
+ ${run.classpath}
+debug.modulepath=\
+ ${run.modulepath}
+debug.test.classpath=\
+ ${run.test.classpath}
+debug.test.modulepath=\
+ ${run.test.modulepath}
+# Files in build.classes.dir which should be excluded from distribution jar
+dist.archive.excludes=
+# This directory is removed when the project is cleaned:
+dist.dir=dist
+dist.jar=${dist.dir}/ZookeeperNodeMigration.jar
+dist.javadoc.dir=${dist.dir}/javadoc
+endorsed.classpath=
+excludes=
+file.reference.curator-client-2.8.0.jar=release/curator-client-2.8.0.jar
+file.reference.curator-framework-2.8.0.jar=release/curator-framework-2.8.0.jar
+file.reference.curator-recipes-2.8.0.jar=release/curator-recipes-2.8.0.jar
+file.reference.guava-17.0.jar=release/guava-17.0.jar
+file.reference.log4j-1.2.17.jar=release/log4j-1.2.17.jar
+file.reference.slf4j-api-1.7.24.jar=release/slf4j-api-1.7.24.jar
+file.reference.slf4j-log4j12-1.7.6.jar=release/slf4j-log4j12-1.7.6.jar
+file.reference.zookeeper-3.4.6.jar=release/zookeeper-3.4.6.jar
+includes=**
+jar.compress=false
+javac.classpath=\
+ ${file.reference.curator-client-2.8.0.jar}:\
+ ${file.reference.curator-framework-2.8.0.jar}:\
+ ${file.reference.curator-recipes-2.8.0.jar}:\
+ ${file.reference.zookeeper-3.4.6.jar}:\
+ ${file.reference.slf4j-api-1.7.24.jar}:\
+ ${file.reference.slf4j-log4j12-1.7.6.jar}:\
+ ${file.reference.log4j-1.2.17.jar}:\
+ ${file.reference.guava-17.0.jar}
+# Space-separated list of extra javac options
+javac.compilerargs=
+javac.deprecation=false
+javac.external.vm=true
+javac.modulepath=
+javac.processormodulepath=
+javac.processorpath=\
+ ${javac.classpath}
+javac.source=1.8
+javac.target=1.8
+javac.test.classpath=\
+ ${javac.classpath}:\
+ ${build.classes.dir}
+javac.test.modulepath=\
+ ${javac.modulepath}
+javac.test.processorpath=\
+ ${javac.test.classpath}
+javadoc.additionalparam=
+javadoc.author=false
+javadoc.encoding=${source.encoding}
+javadoc.html5=false
+javadoc.noindex=false
+javadoc.nonavbar=false
+javadoc.notree=false
+javadoc.private=false
+javadoc.splitindex=true
+javadoc.use=true
+javadoc.version=false
+javadoc.windowtitle=
+jlink.launcher=false
+jlink.launcher.name=ZookeeperNodeMigration
+main.class=zookeepernodemigration.ZookeeperNodeMigration
+manifest.file=manifest.mf
+meta.inf.dir=${src.dir}/META-INF
+mkdist.disabled=false
+platform.active=default_platform
+run.classpath=\
+ ${javac.classpath}:\
+ ${build.classes.dir}
+# Space-separated list of JVM arguments used when running the project.
+# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value.
+# To set system properties for unit tests define test-sys-prop.name=value:
+run.jvmargs=
+run.modulepath=\
+ ${javac.modulepath}
+run.test.classpath=\
+ ${javac.test.classpath}:\
+ ${build.test.classes.dir}
+run.test.modulepath=\
+ ${javac.test.modulepath}
+source.encoding=UTF-8
+src.dir=src
+test.src.dir=test
diff --git a/ZookeeperNodeMigration/nbproject/project.xml b/ZookeeperNodeMigration/nbproject/project.xml
new file mode 100755
index 0000000000..1db04676a2
--- /dev/null
+++ b/ZookeeperNodeMigration/nbproject/project.xml
@@ -0,0 +1,15 @@
+
+
+ org.netbeans.modules.java.j2seproject
+
+
+ ZookeeperNodeMigration
+
+
+
+
+
+
+
+
+
diff --git a/ZookeeperNodeMigration/release/curator-client-2.8.0.jar b/ZookeeperNodeMigration/release/curator-client-2.8.0.jar
new file mode 100755
index 0000000000..4ccc265cc4
Binary files /dev/null and b/ZookeeperNodeMigration/release/curator-client-2.8.0.jar differ
diff --git a/ZookeeperNodeMigration/release/curator-framework-2.8.0.jar b/ZookeeperNodeMigration/release/curator-framework-2.8.0.jar
new file mode 100755
index 0000000000..5e488892d1
Binary files /dev/null and b/ZookeeperNodeMigration/release/curator-framework-2.8.0.jar differ
diff --git a/ZookeeperNodeMigration/release/curator-recipes-2.8.0.jar b/ZookeeperNodeMigration/release/curator-recipes-2.8.0.jar
new file mode 100755
index 0000000000..34eb9c9677
Binary files /dev/null and b/ZookeeperNodeMigration/release/curator-recipes-2.8.0.jar differ
diff --git a/ZookeeperNodeMigration/release/guava-17.0.jar b/ZookeeperNodeMigration/release/guava-17.0.jar
new file mode 100755
index 0000000000..661fc7473f
Binary files /dev/null and b/ZookeeperNodeMigration/release/guava-17.0.jar differ
diff --git a/ZookeeperNodeMigration/release/log4j-1.2.17.jar b/ZookeeperNodeMigration/release/log4j-1.2.17.jar
new file mode 100755
index 0000000000..1d425cf7d7
Binary files /dev/null and b/ZookeeperNodeMigration/release/log4j-1.2.17.jar differ
diff --git a/ZookeeperNodeMigration/release/slf4j-api-1.7.24.jar b/ZookeeperNodeMigration/release/slf4j-api-1.7.24.jar
new file mode 100755
index 0000000000..05941a12f0
Binary files /dev/null and b/ZookeeperNodeMigration/release/slf4j-api-1.7.24.jar differ
diff --git a/ZookeeperNodeMigration/release/slf4j-log4j12-1.7.6.jar b/ZookeeperNodeMigration/release/slf4j-log4j12-1.7.6.jar
new file mode 100755
index 0000000000..d1cc2456e4
Binary files /dev/null and b/ZookeeperNodeMigration/release/slf4j-log4j12-1.7.6.jar differ
diff --git a/ZookeeperNodeMigration/release/zookeeper-3.4.6.jar b/ZookeeperNodeMigration/release/zookeeper-3.4.6.jar
new file mode 100755
index 0000000000..7c340be9f5
Binary files /dev/null and b/ZookeeperNodeMigration/release/zookeeper-3.4.6.jar differ
diff --git a/ZookeeperNodeMigration/src/zookeepernodemigration/AutoIngestJobNodeData.java b/ZookeeperNodeMigration/src/zookeepernodemigration/AutoIngestJobNodeData.java
new file mode 100755
index 0000000000..82e75a7b1c
--- /dev/null
+++ b/ZookeeperNodeMigration/src/zookeepernodemigration/AutoIngestJobNodeData.java
@@ -0,0 +1,687 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2020 Basis Technology Corp.
+ * Contact: carrier sleuthkit org
+ *
+ * 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 zookeepernodemigration;
+
+import java.io.Serializable;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Date;
+import javax.lang.model.type.TypeKind;
+
+/**
+ * An object that converts auto ingest job data for an auto ingest job
+ * coordination service node to and from byte arrays.
+ */
+final class AutoIngestJobNodeData {
+
+ private static final int CURRENT_VERSION = 2;
+ private static final int DEFAULT_PRIORITY = 0;
+
+ /*
+ * This number is the sum of each piece of data, based on it's type. For the
+ * types boolean, int, and long, values 1, 4, and 8 will be added
+ * respectively. For String objects, the length of the string, plus either a
+ * byte or short respesenting the length of the string, will be added.
+ *
+ * This field is used to set the size of the buffer during the byte array
+ * creation in the 'toArray()' method. Since the final size of the array
+ * isn't immediately known at the time of creation, this number is used to
+ * create an array as large as could possibly be needed to store all the
+ * data. This avoids the need to continuously enlarge the buffer. Once the
+ * buffer has all the necessary data, it will be resized as appropriate.
+ */
+ private static final int MAX_POSSIBLE_NODE_DATA_SIZE = 131637;
+
+ /*
+ * Version 0 fields.
+ */
+ private int processingStatus;
+ private int priority;
+ private int numberOfCrashes;
+ private long completedDate;
+ private boolean errorsOccurred;
+
+ /*
+ * Version 1 fields.
+ */
+ private int version;
+ private String manifestFilePath; // 'short' length used in byte array
+ private long manifestFileDate;
+ private String caseName; // 'byte' length used in byte array
+ private String deviceId; // 'byte' length used in byte array
+ private String dataSourcePath; // 'short' length used in byte array
+ private String caseDirectoryPath; // 'short' length used in byte array
+ private String processingHostName; // 'short' length used in byte array
+ private byte processingStage;
+ private long processingStageStartDate;
+ private String processingStageDetailsDescription; // 'byte' length used in byte array
+ private long processingStageDetailsStartDate;
+
+ /*
+ * Version 2 fields.
+ */
+ private long dataSourceSize;
+
+ /**
+ * Processing statuses for an auto ingest job.
+ */
+ enum ProcessingStatus {
+ PENDING,
+ PROCESSING,
+ COMPLETED,
+ DELETED
+ }
+
+ /**
+ * Processing stages for an auto ingest job.
+ */
+ enum Stage {
+
+ PENDING("Pending"),
+ STARTING("Starting"),
+ UPDATING_SHARED_CONFIG("Updating shared configuration"),
+ CHECKING_SERVICES("Checking services"),
+ OPENING_CASE("Opening case"),
+ IDENTIFYING_DATA_SOURCE("Identifying data source type"),
+ ADDING_DATA_SOURCE("Adding data source"),
+ ANALYZING_DATA_SOURCE("Analyzing data source"),
+ ANALYZING_FILES("Analyzing files"),
+ EXPORTING_FILES("Exporting files"),
+ CANCELLING_MODULE("Cancelling module"),
+ CANCELLING("Cancelling"),
+ COMPLETED("Completed");
+
+ private final String displayText;
+
+ private Stage(String displayText) {
+ this.displayText = displayText;
+ }
+
+ String getDisplayText() {
+ return displayText;
+ }
+
+ }
+
+
+ /**
+ * Processing stage details for an auto ingest job.
+ */
+ static final class StageDetails implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+ private final String description;
+ private final Date startDate;
+
+ StageDetails(String description, Date startDate) {
+ this.description = description;
+ this.startDate = startDate;
+ }
+
+ String getDescription() {
+ return this.description;
+ }
+
+ Date getStartDate() {
+ return new Date(this.startDate.getTime());
+ }
+
+ }
+
+ /**
+ * Gets the current version of the auto ingest job coordination service node
+ * data.
+ *
+ * @return The version number.
+ */
+ static int getCurrentVersion() {
+ return AutoIngestJobNodeData.CURRENT_VERSION;
+ }
+
+ /**
+ * Uses a coordination service node data to construct an object that
+ * converts auto ingest job data for an auto ingest job coordination service
+ * node to and from byte arrays.
+ *
+ * @param nodeData The raw bytes received from the coordination service.
+ */
+ AutoIngestJobNodeData(byte[] nodeData) throws InvalidDataException {
+ if (null == nodeData || nodeData.length == 0) {
+ throw new InvalidDataException(null == nodeData ? "Null nodeData byte array" : "Zero-length nodeData byte array");
+ }
+
+ /*
+ * Set default values for all fields.
+ */
+ this.processingStatus = ProcessingStatus.PENDING.ordinal();
+ this.priority = DEFAULT_PRIORITY;
+ this.numberOfCrashes = 0;
+ this.completedDate = 0L;
+ this.errorsOccurred = false;
+ this.version = 0;
+ this.manifestFilePath = "";
+ this.manifestFileDate = 0L;
+ this.caseName = "";
+ this.deviceId = "";
+ this.dataSourcePath = "";
+ this.caseDirectoryPath = "";
+ this.processingHostName = "";
+ this.processingStage = (byte) Stage.PENDING.ordinal();
+ this.processingStageStartDate = 0L;
+ this.processingStageDetailsDescription = "";
+ this.processingStageDetailsStartDate = 0L;
+ this.dataSourceSize = 0L;
+
+ /*
+ * Get fields from node data.
+ */
+ ByteBuffer buffer = ByteBuffer.wrap(nodeData);
+ try {
+ if (buffer.hasRemaining()) {
+ /*
+ * Get version 0 fields.
+ */
+ this.processingStatus = buffer.getInt();
+ this.priority = buffer.getInt();
+ this.numberOfCrashes = buffer.getInt();
+ this.completedDate = buffer.getLong();
+ int errorFlag = buffer.getInt();
+ this.errorsOccurred = (1 == errorFlag);
+ }
+
+ if (buffer.hasRemaining()) {
+ /*
+ * Get version 1 fields.
+ */
+ this.version = buffer.getInt();
+ this.deviceId = getStringFromBuffer(buffer, TypeKind.BYTE);
+ this.caseName = getStringFromBuffer(buffer, TypeKind.BYTE);
+ this.caseDirectoryPath = getStringFromBuffer(buffer, TypeKind.SHORT);
+ this.manifestFileDate = buffer.getLong();
+ this.manifestFilePath = getStringFromBuffer(buffer, TypeKind.SHORT);
+ this.dataSourcePath = getStringFromBuffer(buffer, TypeKind.SHORT);
+ this.processingStage = buffer.get();
+ this.processingStageStartDate = buffer.getLong();
+ this.processingStageDetailsDescription = getStringFromBuffer(buffer, TypeKind.BYTE);
+ this.processingStageDetailsStartDate = buffer.getLong();
+ this.processingHostName = getStringFromBuffer(buffer, TypeKind.SHORT);
+ }
+
+ if (buffer.hasRemaining()) {
+ /*
+ * Get version 2 fields.
+ */
+ this.dataSourceSize = buffer.getLong();
+ }
+
+ } catch (BufferUnderflowException ex) {
+ throw new InvalidDataException("Node data is incomplete", ex);
+ }
+ }
+
+ /**
+ * Gets the processing status of the job.
+ *
+ * @return The processing status.
+ */
+ ProcessingStatus getProcessingStatus() {
+ return ProcessingStatus.values()[this.processingStatus];
+ }
+
+ /**
+ * Sets the processing status of the job.
+ *
+ * @param processingSatus The processing status.
+ */
+ void setProcessingStatus(ProcessingStatus processingStatus) {
+ this.processingStatus = processingStatus.ordinal();
+ }
+
+ /**
+ * Gets the priority of the job.
+ *
+ * @return The priority.
+ */
+ int getPriority() {
+ return this.priority;
+ }
+
+ /**
+ * Sets the priority of the job. A higher number indicates a higheer
+ * priority.
+ *
+ * @param priority The priority.
+ */
+ void setPriority(int priority) {
+ this.priority = priority;
+ }
+
+ /**
+ * Gets the number of times the job has crashed during processing.
+ *
+ * @return The number of crashes.
+ */
+ int getNumberOfCrashes() {
+ return this.numberOfCrashes;
+ }
+
+ /**
+ * Sets the number of times the job has crashed during processing.
+ *
+ * @param numberOfCrashes The number of crashes.
+ */
+ void setNumberOfCrashes(int numberOfCrashes) {
+ this.numberOfCrashes = numberOfCrashes;
+ }
+
+ /**
+ * Gets the date the job was completed. A completion date equal to the epoch
+ * (January 1, 1970, 00:00:00 GMT), i.e., Date.getTime() returns 0L,
+ * indicates the job has not been completed.
+ *
+ * @return The job completion date.
+ */
+ Date getCompletedDate() {
+ return new Date(this.completedDate);
+ }
+
+ /**
+ * Sets the date the job was completed. A completion date equal to the epoch
+ * (January 1, 1970, 00:00:00 GMT), i.e., Date.getTime() returns 0L,
+ * indicates the job has not been completed.
+ *
+ * @param completedDate The job completion date.
+ */
+ void setCompletedDate(Date completedDate) {
+ this.completedDate = completedDate.getTime();
+ }
+
+ /**
+ * Gets whether or not any errors occurred during the processing of the job.
+ *
+ * @return True or false.
+ */
+ boolean getErrorsOccurred() {
+ return this.errorsOccurred;
+ }
+
+ /**
+ * Sets whether or not any errors occurred during the processing of job.
+ *
+ * @param errorsOccurred True or false.
+ */
+ void setErrorsOccurred(boolean errorsOccurred) {
+ this.errorsOccurred = errorsOccurred;
+ }
+
+ /**
+ * Gets the node data version number.
+ *
+ * @return The version number.
+ */
+ int getVersion() {
+ return this.version;
+ }
+
+ /**
+ * Gets the device ID of the device associated with the data source for the
+ * job.
+ *
+ * @return The device ID.
+ */
+ String getDeviceId() {
+ return this.deviceId;
+ }
+
+ /**
+ * Sets the device ID of the device associated with the data source for the
+ * job.
+ *
+ * @param deviceId The device ID.
+ */
+ void setDeviceId(String deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ /**
+ * Gets the case name.
+ *
+ * @return The case name.
+ */
+ String getCaseName() {
+ return this.caseName;
+ }
+
+ /**
+ * Sets the case name.
+ *
+ * @param caseName The case name.
+ */
+ void setCaseName(String caseName) {
+ this.caseName = caseName;
+ }
+
+ /**
+ * Sets the path to the case directory of the case associated with the job.
+ *
+ * @param caseDirectoryPath The path to the case directory.
+ */
+ synchronized void setCaseDirectoryPath(Path caseDirectoryPath) {
+ if (caseDirectoryPath == null) {
+ this.caseDirectoryPath = "";
+ } else {
+ this.caseDirectoryPath = caseDirectoryPath.toString();
+ }
+ }
+
+ /**
+ * Gets the path to the case directory of the case associated with the job.
+ *
+ * @return The case directory path or an empty string path if the case
+ * directory has not been created yet.
+ */
+ synchronized Path getCaseDirectoryPath() {
+ if (!caseDirectoryPath.isEmpty()) {
+ return Paths.get(caseDirectoryPath);
+ } else {
+ return Paths.get("");
+ }
+ }
+
+ /**
+ * Gets the date the manifest was created.
+ *
+ * @return The date the manifest was created.
+ */
+ Date getManifestFileDate() {
+ return new Date(this.manifestFileDate);
+ }
+
+ /**
+ * Sets the date the manifest was created.
+ *
+ * @param manifestFileDate The date the manifest was created.
+ */
+ void setManifestFileDate(Date manifestFileDate) {
+ this.manifestFileDate = manifestFileDate.getTime();
+ }
+
+ /**
+ * Gets the manifest file path.
+ *
+ * @return The manifest file path.
+ */
+ Path getManifestFilePath() {
+ return Paths.get(this.manifestFilePath);
+ }
+
+ /**
+ * Sets the manifest file path.
+ *
+ * @param manifestFilePath The manifest file path.
+ */
+ void setManifestFilePath(Path manifestFilePath) {
+ if (manifestFilePath != null) {
+ this.manifestFilePath = manifestFilePath.toString();
+ } else {
+ this.manifestFilePath = "";
+ }
+ }
+
+ /**
+ * Gets the path of the data source for the job.
+ *
+ * @return The data source path.
+ */
+ Path getDataSourcePath() {
+ return Paths.get(dataSourcePath);
+ }
+
+ /**
+ * Get the file name portion of the path of the data source for the job.
+ *
+ * @return The data source file name.
+ */
+ public String getDataSourceFileName() {
+ return Paths.get(dataSourcePath).getFileName().toString();
+ }
+
+ /**
+ * Sets the path of the data source for the job.
+ *
+ * @param dataSourcePath The data source path.
+ */
+ void setDataSourcePath(Path dataSourcePath) {
+ if (dataSourcePath != null) {
+ this.dataSourcePath = dataSourcePath.toString();
+ } else {
+ this.dataSourcePath = "";
+ }
+ }
+
+ /**
+ * Get the processing stage of the job.
+ *
+ * @return The processing stage.
+ */
+ Stage getProcessingStage() {
+ return Stage.values()[this.processingStage];
+ }
+
+ /**
+ * Sets the processing stage job.
+ *
+ * @param processingStage The processing stage.
+ */
+ void setProcessingStage(Stage processingStage) {
+ this.processingStage = (byte) processingStage.ordinal();
+ }
+
+ /**
+ * Gets the processing stage start date.
+ *
+ * @return The processing stage start date.
+ */
+ Date getProcessingStageStartDate() {
+ return new Date(this.processingStageStartDate);
+ }
+
+ /**
+ * Sets the processing stage start date.
+ *
+ * @param processingStageStartDate The processing stage start date.
+ */
+ void setProcessingStageStartDate(Date processingStageStartDate) {
+ this.processingStageStartDate = processingStageStartDate.getTime();
+ }
+
+ /**
+ * Get the processing stage details.
+ *
+ * @return A processing stage details object.
+ */
+ StageDetails getProcessingStageDetails() {
+ return new StageDetails(this.processingStageDetailsDescription, new Date(this.processingStageDetailsStartDate));
+ }
+
+ /**
+ * Sets the details of the current processing stage.
+ *
+ * @param stageDetails A stage details object.
+ */
+ void setProcessingStageDetails(StageDetails stageDetails) {
+ this.processingStageDetailsDescription = stageDetails.getDescription();
+ this.processingStageDetailsStartDate = stageDetails.getStartDate().getTime();
+ }
+
+ /**
+ * Gets the processing host name, may be the empty string.
+ *
+ * @return The processing host. The empty string if the job is not currently
+ * being processed.
+ */
+ String getProcessingHostName() {
+ return this.processingHostName;
+ }
+
+ /**
+ * Sets the processing host name. May be the empty string.
+ *
+ * @param processingHost The processing host name. The empty string if the
+ * job is not currently being processed.
+ */
+ void setProcessingHostName(String processingHost) {
+ this.processingHostName = processingHost;
+ }
+
+ /**
+ * Gets the total size of the data source.
+ *
+ * @return The data source size.
+ */
+ long getDataSourceSize() {
+ return this.dataSourceSize;
+ }
+
+ /**
+ * Sets the total size of the data source.
+ *
+ * @param dataSourceSize The data source size.
+ */
+ void setDataSourceSize(long dataSourceSize) {
+ this.dataSourceSize = dataSourceSize;
+ }
+
+ /**
+ * Gets the node data as a byte array that can be sent to the coordination
+ * service.
+ *
+ * @return The node data as a byte array.
+ */
+ byte[] toArray() {
+ ByteBuffer buffer = ByteBuffer.allocate(MAX_POSSIBLE_NODE_DATA_SIZE);
+
+ // Write data (compatible with version 0)
+ buffer.putInt(this.processingStatus);
+ buffer.putInt(this.priority);
+ buffer.putInt(this.numberOfCrashes);
+ buffer.putLong(this.completedDate);
+ buffer.putInt(this.errorsOccurred ? 1 : 0);
+
+ if (this.version >= 1) {
+ // Write version
+ buffer.putInt(this.version);
+
+ // Write data
+ putStringIntoBuffer(deviceId, buffer, TypeKind.BYTE);
+ putStringIntoBuffer(caseName, buffer, TypeKind.BYTE);
+ putStringIntoBuffer(caseDirectoryPath, buffer, TypeKind.SHORT);
+ buffer.putLong(this.manifestFileDate);
+ putStringIntoBuffer(manifestFilePath, buffer, TypeKind.SHORT);
+ putStringIntoBuffer(dataSourcePath, buffer, TypeKind.SHORT);
+ buffer.put(this.processingStage);
+ buffer.putLong(this.processingStageStartDate);
+ putStringIntoBuffer(this.processingStageDetailsDescription, buffer, TypeKind.BYTE);
+ buffer.putLong(this.processingStageDetailsStartDate);
+ putStringIntoBuffer(processingHostName, buffer, TypeKind.SHORT);
+
+ if (this.version >= 2) {
+ buffer.putLong(this.dataSourceSize);
+ }
+ }
+
+ // Prepare the array
+ byte[] array = new byte[buffer.position()];
+ buffer.rewind();
+ buffer.get(array, 0, array.length);
+
+ return array;
+ }
+
+ /**
+ * This method retrieves a string from a given buffer. Depending on the type
+ * specified, either a 'byte' or a 'short' will first be read out of the
+ * buffer which gives the length of the string so it can be properly parsed.
+ *
+ * @param buffer The buffer from which the string will be read.
+ * @param lengthType The size of the length data.
+ *
+ * @return The string read from the buffer.
+ */
+ private String getStringFromBuffer(ByteBuffer buffer, TypeKind lengthType) {
+ int length = 0;
+ String output = "";
+
+ switch (lengthType) {
+ case BYTE:
+ length = buffer.get();
+ break;
+ case SHORT:
+ length = buffer.getShort();
+ break;
+ }
+
+ if (length > 0) {
+ byte[] array = new byte[length];
+ buffer.get(array, 0, length);
+ output = new String(array);
+ }
+
+ return output;
+ }
+
+ /**
+ * This method puts a given string into a given buffer. Depending on the
+ * type specified, either a 'byte' or a 'short' will be inserted prior to
+ * the string which gives the length of the string so it can be properly
+ * parsed.
+ *
+ * @param stringValue The string to write to the buffer.
+ * @param buffer The buffer to which the string will be written.
+ * @param lengthType The size of the length data.
+ */
+ private void putStringIntoBuffer(String stringValue, ByteBuffer buffer, TypeKind lengthType) {
+ switch (lengthType) {
+ case BYTE:
+ buffer.put((byte) stringValue.length());
+ break;
+ case SHORT:
+ buffer.putShort((short) stringValue.length());
+ break;
+ }
+
+ buffer.put(stringValue.getBytes());
+ }
+
+ final static class InvalidDataException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ private InvalidDataException(String message) {
+ super(message);
+ }
+
+ private InvalidDataException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+}
diff --git a/ZookeeperNodeMigration/src/zookeepernodemigration/TimeStampUtils.java b/ZookeeperNodeMigration/src/zookeepernodemigration/TimeStampUtils.java
new file mode 100755
index 0000000000..c04feca3c1
--- /dev/null
+++ b/ZookeeperNodeMigration/src/zookeepernodemigration/TimeStampUtils.java
@@ -0,0 +1,93 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2020 Basis Technology Corp.
+ * Contact: carrier sleuthkit org
+ *
+ * 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 zookeepernodemigration;
+
+import java.text.SimpleDateFormat;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility methods for working with time stamps of the form
+ * 'yyyy_MM_dd_HH_mm_ss'.
+ */
+final class TimeStampUtils {
+
+ /*
+ * Sample time stamp suffix: 2015_02_02_12_10_31
+ */
+ private static final Pattern TIME_STAMP_PATTERN = Pattern.compile("\\d{4}_\\d{2}_\\d{2}_\\d{2}_\\d{2}_\\d{2}$");
+ private static final int LENGTH_OF_DATE_TIME_STAMP = 20; // length of the above time stamp
+
+ /**
+ * Checks whether a string ends with a time stamp.
+ *
+ * @param inputString The string to check.
+ *
+ * @return True or false.
+ */
+ static boolean endsWithTimeStamp(String inputString) {
+ Matcher m = TIME_STAMP_PATTERN.matcher(inputString);
+ return m.find();
+ }
+
+ /**
+ * Gets the fixed length of the time stamp suffix.
+ *
+ * @return The length.
+ */
+ static int getTimeStampLength() {
+ return LENGTH_OF_DATE_TIME_STAMP;
+ }
+
+ /**
+ * Removes the time stamp suffix from a string, if present.
+ *
+ * @param inputString The string to trim.
+ *
+ * @return The trimmed string.
+ */
+ static String removeTimeStamp(String inputString) {
+ String trimmedString = inputString;
+ if (inputString != null && endsWithTimeStamp(inputString)) {
+ trimmedString = inputString.substring(0, inputString.length() - getTimeStampLength());
+ }
+ return trimmedString;
+ }
+
+ /**
+ * Gets the time stamp suffix from a string, if present.
+ *
+ * @param inputString the name to check for a timestamp
+ *
+ * @return The time stamp, may be the empty.
+ */
+ static String getTimeStampOnly(String inputString) {
+ String timeStamp = "";
+ if (inputString != null && endsWithTimeStamp(inputString)) {
+ timeStamp = inputString.substring(inputString.length() - getTimeStampLength(), inputString.length());
+ }
+ return timeStamp;
+ }
+
+ /*
+ * Private contructor to prevent instantiation.
+ */
+ private TimeStampUtils() {
+ }
+}
diff --git a/ZookeeperNodeMigration/src/zookeepernodemigration/ZookeeperNodeMigration.java b/ZookeeperNodeMigration/src/zookeepernodemigration/ZookeeperNodeMigration.java
new file mode 100755
index 0000000000..c62db26b84
--- /dev/null
+++ b/ZookeeperNodeMigration/src/zookeepernodemigration/ZookeeperNodeMigration.java
@@ -0,0 +1,464 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2020 Basis Technology Corp.
+ * Contact: carrier sleuthkit org
+ *
+ * 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 zookeepernodemigration;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.curator.RetryPolicy;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.locks.InterProcessMutex;
+import org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.zookeeper.CreateMode;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.KeeperException.NoNodeException;
+import org.apache.zookeeper.WatchedEvent;
+import org.apache.zookeeper.ZooDefs;
+import org.apache.zookeeper.ZooKeeper;
+
+/**
+ * Utility to migrate Autopsy coordination service data from one ZK database to
+ * another.
+ */
+public class ZookeeperNodeMigration {
+
+ private static final Logger LOGGER = Logger.getLogger(ZookeeperNodeMigration.class.getName());
+ private static final int SESSION_TIMEOUT_MILLISECONDS = 300000;
+ private static final int CONNECTION_TIMEOUT_MILLISECONDS = 300000;
+ private static final int ZOOKEEPER_SESSION_TIMEOUT_MILLIS = 3000;
+ private static final int ZOOKEEPER_CONNECTION_TIMEOUT_MILLIS = 15000;
+ private static final String DEFAULT_NAMESPACE_ROOT = "autopsy";
+ private static CuratorFramework inputCurator;
+ private static CuratorFramework outputCurator;
+ private static Map categoryNodeToPath;
+
+ private ZookeeperNodeMigration(){
+ }
+
+ /**
+ * Main method.
+ *
+ * @param args the command line arguments
+ */
+ public static void main(String[] args) {
+
+ String inputZkIpAddr, inputZkPort, outputZkIpAddr, outputZkPort;
+
+ if (args.length == 4) {
+ inputZkIpAddr = args[0];
+ inputZkPort = args[1];
+ outputZkIpAddr = args[2];
+ outputZkPort = args[3];
+ } else {
+ System.out.println("Input needs to be [Input Zookeeper IP Address] [Input Zookeeper Port Number] [Output Zookeeper IP Address] [Output Zookeeper Port Number]");
+ LOGGER.log(Level.SEVERE, "Input needs to be [Input Zookeeper IP Address] [Input Zookeeper Port Number] [Output Zookeeper IP Address] [Output Zookeeper Port Number]");
+ return;
+ }
+
+ if (inputZkIpAddr.isEmpty() || inputZkPort.isEmpty() || outputZkIpAddr.isEmpty() || outputZkPort.isEmpty()) {
+ System.out.println("Input needs to be [Input Zookeeper IP Address] [Input Zookeeper Port Number] [Output Zookeeper IP Address] [Output Zookeeper Port Number]");
+ LOGGER.log(Level.SEVERE, "Input needs to be [Input Zookeeper IP Address] [Input Zookeeper Port Number] [Output Zookeeper IP Address] [Output Zookeeper Port Number]");
+ return;
+ }
+
+ inputCurator = initializeCurator(inputZkIpAddr, inputZkPort);
+ if (inputCurator == null) {
+ System.out.println("Unable to initialize Zookeeper or Curator: " + inputZkIpAddr + ":" + inputZkPort);
+ LOGGER.log(Level.SEVERE, "Unable to initialize Zookeeper or Curator: {0}:{1}", new Object[]{inputZkIpAddr, inputZkPort});
+ return;
+ }
+
+ try {
+ categoryNodeToPath = populateCategoryNodes(inputCurator);
+ } catch (KeeperException | CoordinationServiceException ex) {
+ System.out.println("Unable to initialize Curator: " + inputZkIpAddr + ":" + inputZkPort);
+ LOGGER.log(Level.SEVERE, "Unable to initialize Curator: {0}:{1}", new Object[]{inputZkIpAddr, inputZkPort});
+ return;
+ }
+
+ outputCurator = initializeCurator(outputZkIpAddr, outputZkPort);
+ if (outputCurator == null) {
+ System.out.println("Unable to initialize Zookeeper or Curator: " + outputZkIpAddr + ":" + outputZkPort);
+ LOGGER.log(Level.SEVERE, "Unable to initialize Zookeeper or Curator: {0}:{1}", new Object[]{outputZkIpAddr, outputZkPort});
+ return;
+ }
+
+ try {
+ // if output ZK database is new, we may have to ceate root "autopsy" node and it's sub-nodes
+ populateCategoryNodes(outputCurator);
+ } catch (KeeperException | CoordinationServiceException ex) {
+ System.out.println("Unable to initialize Curator: " + outputZkIpAddr + ":" + outputZkPort);
+ LOGGER.log(Level.SEVERE, "Unable to initialize Curator: {0}:{1}", new Object[]{outputZkIpAddr, outputZkPort});
+ return;
+ }
+
+ copyAllCategoryNodes();
+
+ System.out.println("Done...");
+ }
+
+ /**
+ * Copy all Autopsy coordination service nodes from one ZK database to
+ * another.
+ */
+ private static void copyAllCategoryNodes() {
+
+ for (CategoryNode category : CategoryNode.values()) {
+ List inputNodeList = Collections.EMPTY_LIST;
+ try {
+ inputNodeList = getNodeList(category);
+ } catch (CoordinationServiceException ex) {
+ System.out.println("Unable to get ZK nodes for category: " + category.getDisplayName());
+ LOGGER.log(Level.SEVERE, "Unable to get ZK nodes for category: " + category.getDisplayName(), ex);
+ continue;
+ }
+
+ for (String zkNode : inputNodeList) {
+ try {
+ final byte[] nodeBytes = getNodeData(category, zkNode);
+ try (Lock manifestLock = tryGetExclusiveLock(outputCurator, category, zkNode)) {
+ setNodeData(outputCurator, category, zkNode, nodeBytes);
+ }
+ } catch (CoordinationServiceException | InterruptedException ex) {
+ System.out.println("Unable to write ZK node data for node: " + zkNode);
+ LOGGER.log(Level.SEVERE, "Unable to write ZK node data for node: " + zkNode, ex);
+ continue;
+ }
+ }
+ }
+ }
+
+ /**
+ * Initialize Curator framework.
+ *
+ * @param zkIpAddr Zookeeper server IP address.
+ * @param zookeeperPort Zookeeper server port number.
+ *
+ * @return CuratorFramework object
+ */
+ private static CuratorFramework initializeCurator(String zkIpAddr, String zookeeperPort) {
+
+ try {
+ if (!isZooKeeperAccessible(zkIpAddr, zookeeperPort)) {
+ System.out.println("Unable to connect to Zookeeper");
+ LOGGER.log(Level.SEVERE, "Unable to connect to Zookeeper");
+ return null;
+ }
+ } catch (InterruptedException | IOException ex) {
+ System.out.println("Unable to connect to Zookeeper");
+ LOGGER.log(Level.SEVERE, "Unable to connect to Zookeeper", ex);
+ return null;
+ }
+
+ /*
+ * Connect to ZooKeeper via Curator.
+ */
+ RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
+ String connectString = zkIpAddr + ":" + zookeeperPort;
+ CuratorFramework curator = CuratorFrameworkFactory.newClient(connectString, SESSION_TIMEOUT_MILLISECONDS, CONNECTION_TIMEOUT_MILLISECONDS, retryPolicy);
+ curator.start();
+ return curator;
+ }
+
+ /*
+ * Creates Autopsy coordination service root ZK nodes.
+ */
+ private static Map populateCategoryNodes(CuratorFramework curator) throws KeeperException, CoordinationServiceException {
+ /*
+ * Create the top-level root and category nodes.
+ */
+ String rootNodeName = DEFAULT_NAMESPACE_ROOT;
+ String rootNode = rootNodeName;
+
+ if (!rootNode.startsWith("/")) {
+ rootNode = "/" + rootNode;
+ }
+ Map categoryPaths = new ConcurrentHashMap<>();
+ for (CategoryNode node : CategoryNode.values()) {
+ String nodePath = rootNode + "/" + node.getDisplayName();
+ try {
+ curator.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE).forPath(nodePath);
+ } catch (KeeperException ex) {
+ if (ex.code() != KeeperException.Code.NODEEXISTS) {
+ throw ex;
+ }
+ } catch (Exception ex) {
+ throw new CoordinationServiceException("Curator experienced an error", ex);
+ }
+ categoryPaths.put(node.getDisplayName(), nodePath);
+ }
+ return categoryPaths;
+ }
+
+ /**
+ * Determines if ZooKeeper is accessible with the current settings. Closes
+ * the connection prior to returning.
+ *
+ * @return true if a connection was achieved, false otherwise
+ *
+ * @throws InterruptedException
+ * @throws IOException
+ */
+ private static boolean isZooKeeperAccessible(String solrIpAddr, String zookeeperPort) throws InterruptedException, IOException {
+ boolean result = false;
+ Object workerThreadWaitNotifyLock = new Object();
+ String connectString = solrIpAddr + ":" + zookeeperPort;
+ ZooKeeper zooKeeper = new ZooKeeper(connectString, ZOOKEEPER_SESSION_TIMEOUT_MILLIS,
+ (WatchedEvent event) -> {
+ synchronized (workerThreadWaitNotifyLock) {
+ workerThreadWaitNotifyLock.notifyAll();
+ }
+ });
+ synchronized (workerThreadWaitNotifyLock) {
+ workerThreadWaitNotifyLock.wait(ZOOKEEPER_CONNECTION_TIMEOUT_MILLIS);
+ }
+ ZooKeeper.States state = zooKeeper.getState();
+ if (state == ZooKeeper.States.CONNECTED || state == ZooKeeper.States.CONNECTEDREADONLY) {
+ result = true;
+ }
+ zooKeeper.close();
+ return result;
+ }
+
+ /**
+ * Tries to get an exclusive lock on a node path appended to a category path
+ * in the namespace managed by this coordination service. Returns
+ * immediately if the lock can not be acquired.
+ *
+ * IMPORTANT: The lock needs to be released in the same thread in which it
+ * is acquired.
+ *
+ * @param category The desired category in the namespace.
+ * @param nodePath The node path to use as the basis for the lock.
+ *
+ * @return The lock, or null if the lock could not be obtained.
+ *
+ * @throws CoordinationServiceException If there is an error during lock
+ * acquisition.
+ */
+ private static Lock tryGetExclusiveLock(CuratorFramework curator, CategoryNode category, String nodePath) throws CoordinationServiceException {
+ String fullNodePath = getFullyQualifiedNodePath(category, nodePath);
+ try {
+ InterProcessReadWriteLock lock = new InterProcessReadWriteLock(curator, fullNodePath);
+ if (!lock.writeLock().acquire(0, TimeUnit.SECONDS)) {
+ return null;
+ }
+ return new Lock(nodePath, lock.writeLock());
+ } catch (Exception ex) {
+ throw new CoordinationServiceException(String.format("Failed to get exclusive lock for %s", fullNodePath), ex);
+ }
+ }
+
+ /**
+ * Retrieve the data associated with the specified node.
+ *
+ * @param category The desired category in the namespace.
+ * @param nodePath The node to retrieve the data for.
+ *
+ * @return The data associated with the node, if any, or null if the node
+ * has not been created yet.
+ *
+ * @throws CoordinationServiceException If there is an error setting the
+ * node data.
+ * @throws InterruptedException If interrupted while blocked during
+ * setting of node data.
+ */
+ private static byte[] getNodeData(CategoryNode category, String nodePath) throws CoordinationServiceException, InterruptedException {
+ String fullNodePath = getFullyQualifiedNodePath(category, nodePath);
+ try {
+ return inputCurator.getData().forPath(fullNodePath);
+ } catch (NoNodeException ex) {
+ return null;
+ } catch (Exception ex) {
+ if (ex instanceof InterruptedException) {
+ throw (InterruptedException) ex;
+ } else {
+ throw new CoordinationServiceException(String.format("Failed to get data for %s", fullNodePath), ex);
+ }
+ }
+ }
+
+ /**
+ * Store the given data with the specified node.
+ *
+ * @param category The desired category in the namespace.
+ * @param nodePath The node to associate the data with.
+ * @param data The data to store with the node.
+ *
+ * @throws CoordinationServiceException If there is an error setting the
+ * node data.
+ * @throws InterruptedException If interrupted while blocked during
+ * setting of node data.
+ */
+ private static void setNodeData(CuratorFramework curator, CategoryNode category, String nodePath, byte[] data) throws CoordinationServiceException, InterruptedException {
+ String fullNodePath = getFullyQualifiedNodePath(category, nodePath);
+ try {
+ curator.setData().forPath(fullNodePath, data);
+ } catch (Exception ex) {
+ if (ex instanceof InterruptedException) {
+ throw (InterruptedException) ex;
+ } else {
+ throw new CoordinationServiceException(String.format("Failed to set data for %s", fullNodePath), ex);
+ }
+ }
+ }
+
+ /**
+ * Delete the specified node.
+ *
+ * @param category The desired category in the namespace.
+ * @param nodePath The node to delete.
+ *
+ * @throws CoordinationServiceException If there is an error setting the
+ * node data.
+ * @throws InterruptedException If interrupted while blocked during
+ * setting of node data.
+ */
+ private static void deleteNode(CategoryNode category, String nodePath) throws CoordinationServiceException, InterruptedException {
+ String fullNodePath = getFullyQualifiedNodePath(category, nodePath);
+ try {
+ inputCurator.delete().forPath(fullNodePath);
+ } catch (Exception ex) {
+ if (ex instanceof InterruptedException) {
+ throw (InterruptedException) ex;
+ } else {
+ throw new CoordinationServiceException(String.format("Failed to set data for %s", fullNodePath), ex);
+ }
+ }
+ }
+
+ /**
+ * Gets a list of the child nodes of a category in the namespace.
+ *
+ * @param category The desired category in the namespace.
+ *
+ * @return A list of child node names.
+ *
+ * @throws CoordinationServiceException If there is an error getting the
+ * node list.
+ */
+ private static List getNodeList(CategoryNode category) throws CoordinationServiceException {
+ try {
+ List list = inputCurator.getChildren().forPath(categoryNodeToPath.get(category.getDisplayName()));
+ return list;
+ } catch (Exception ex) {
+ throw new CoordinationServiceException(String.format("Failed to get node list for %s", category.getDisplayName()), ex);
+ }
+ }
+
+ /**
+ * Creates a node path within a given category.
+ *
+ * @param category A category node.
+ * @param nodePath A node path relative to a category node path.
+ *
+ * @return
+ */
+ private static String getFullyQualifiedNodePath(CategoryNode category, String nodePath) {
+ // nodePath on Unix systems starts with a "/" and ZooKeeper doesn't like two slashes in a row
+ if (nodePath.startsWith("/")) {
+ return categoryNodeToPath.get(category.getDisplayName()) + nodePath.toUpperCase();
+ } else {
+ return categoryNodeToPath.get(category.getDisplayName()) + "/" + nodePath.toUpperCase();
+ }
+ }
+
+ /**
+ * Exception type thrown by the coordination service.
+ */
+ private final static class CoordinationServiceException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ private CoordinationServiceException(String message) {
+ super(message);
+ }
+
+ private CoordinationServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ /**
+ * An opaque encapsulation of a lock for use in distributed synchronization.
+ * Instances are obtained by calling a get lock method and must be passed to
+ * a release lock method.
+ */
+ private static class Lock implements AutoCloseable {
+
+ /**
+ * This implementation uses the Curator read/write lock. see
+ * http://curator.apache.org/curator-recipes/shared-reentrant-read-write-lock.html
+ */
+ private final InterProcessMutex interProcessLock;
+ private final String nodePath;
+
+ private Lock(String nodePath, InterProcessMutex lock) {
+ this.nodePath = nodePath;
+ this.interProcessLock = lock;
+ }
+
+ public String getNodePath() {
+ return nodePath;
+ }
+
+ public void release() throws CoordinationServiceException {
+ try {
+ this.interProcessLock.release();
+ } catch (Exception ex) {
+ throw new CoordinationServiceException(String.format("Failed to release the lock on %s", nodePath), ex);
+ }
+ }
+
+ @Override
+ public void close() throws CoordinationServiceException {
+ release();
+ }
+ }
+
+ /**
+ * Category nodes are the immediate children of the root node of a shared
+ * hierarchical namespace managed by a coordination service.
+ */
+ public enum CategoryNode {
+
+ CASES("cases"),
+ MANIFESTS("manifests"),
+ CONFIG("config"),
+ CENTRAL_REPO("centralRepository"),
+ HEALTH_MONITOR("healthMonitor");
+
+ private final String displayName;
+
+ private CategoryNode(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+ }
+}
diff --git a/build.xml b/build.xml
index a7c64ef4ba..7377d59c04 100644
--- a/build.xml
+++ b/build.xml
@@ -106,8 +106,7 @@
-
-
+
@@ -115,6 +114,14 @@
+
+
+
+
+
+
+
+