NIFI-2569 - Multiple instances on same host, range enhancements, 
nifi.properties incrementing ports NIFI-2569 - Updating main class in windows 
bat file

This closes #861.

Signed-off-by: Bryan Bende <[email protected]>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/2fd39676
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/2fd39676
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/2fd39676

Branch: refs/heads/master
Commit: 2fd39676a887151aa4124a824fc0cd756b9fac3b
Parents: b0122c6
Author: Bryan Rosander <[email protected]>
Authored: Thu Aug 11 16:32:32 2016 -0400
Committer: Bryan Bende <[email protected]>
Committed: Tue Aug 16 15:20:46 2016 -0400

----------------------------------------------------------------------
 .../src/main/resources/bin/tls-toolkit.bat      |   2 +-
 .../main/resources/classpath/overlay.properties |  23 ++
 .../apache/nifi/toolkit/tls/TlsToolkitMain.java |  33 +--
 .../tls/configuration/InstanceDefinition.java   | 109 ++++++++++
 .../tls/configuration/InstanceIdentifier.java   | 174 +++++++++++++++
 .../tls/configuration/StandaloneConfig.java     |  54 +----
 .../NifiPropertiesTlsClientConfigWriter.java    |  76 +++++--
 .../tls/standalone/TlsToolkitStandalone.java    |  31 +--
 .../TlsToolkitStandaloneCommandLine.java        |  57 ++---
 .../nifi/toolkit/tls/util/PasswordUtil.java     |  22 ++
 .../tls/util/PasswordsExhaustedException.java   |  24 +++
 .../apache/nifi/toolkit/tls/ExitException.java  |  31 +++
 .../nifi/toolkit/tls/SystemExitCapturer.java    |  67 ++++++
 .../nifi/toolkit/tls/TlsToolkitMainTest.java    | 114 ++++++++++
 .../configuration/InstanceDefinitionTest.java   | 132 ++++++++++++
 .../configuration/InstanceIdentifierTest.java   | 162 ++++++++++++++
 ...NifiPropertiesTlsClientConfigWriterTest.java | 153 ++++++++++++++
 .../TlsToolkitStandaloneCommandLineTest.java    | 210 +++++++++++--------
 .../standalone/TlsToolkitStandaloneTest.java    |  52 +----
 .../nifi/toolkit/tls/util/PasswordUtilTest.java |   9 +
 .../src/test/resources/overlay.properties       |  23 ++
 21 files changed, 1312 insertions(+), 246 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/tls-toolkit.bat
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/tls-toolkit.bat 
b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/tls-toolkit.bat
index b875e1a..025ec10 100644
--- a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/tls-toolkit.bat
+++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/tls-toolkit.bat
@@ -33,7 +33,7 @@ goto startConfig
 :startConfig
 set LIB_DIR=%~dp0..\classpath;%~dp0..\lib
 
-SET JAVA_PARAMS=-cp %LIB_DIR%\* -Xms12m -Xmx24m %JAVA_ARGS% 
org.apache.nifi.toolkit.tls.service.server.TlsCertificateAuthorityService
+SET JAVA_PARAMS=-cp %LIB_DIR%\* -Xms12m -Xmx24m %JAVA_ARGS% 
org.apache.nifi.toolkit.tls.TlsToolkitMain
 
 cmd.exe /C "%JAVA_EXE%" %JAVA_PARAMS% %*
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/classpath/overlay.properties
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/classpath/overlay.properties
 
b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/classpath/overlay.properties
new file mode 100644
index 0000000..c4d02c2
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/classpath/overlay.properties
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+# Comma separated list of properties to put the hostname into
+hostname.properties=nifi.remote.input.host,nifi.web.https.host,nifi.cluster.node.address
+
+nifi.web.https.port=9443
+nifi.remote.input.socket.port=10443
+nifi.cluster.node.protocol.port=11443
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/TlsToolkitMain.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/TlsToolkitMain.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/TlsToolkitMain.java
index 7d445a5..4614558 100644
--- 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/TlsToolkitMain.java
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/TlsToolkitMain.java
@@ -33,6 +33,7 @@ import java.util.Map;
  */
 public class TlsToolkitMain {
     public static final String DESCRIPTION = "DESCRIPTION";
+    public static final String UNABLE_TO_GET_DESCRIPTION = "Unable to get 
description. (";
     private final Map<String, Class<?>> mainMap;
 
     public TlsToolkitMain() {
@@ -46,7 +47,7 @@ public class TlsToolkitMain {
         new TlsToolkitMain().doMain(args);
     }
 
-    private void printUsageAndExit(String message, ExitCode exitCode) {
+    private <T> T printUsageAndExit(String message, ExitCode exitCode) {
         System.out.println(message);
         System.out.println();
         System.out.println("Usage: tls-toolkit service [-h] [args]");
@@ -55,38 +56,44 @@ public class TlsToolkitMain {
         mainMap.forEach((s, aClass) -> System.out.println("   " + s + ": " + 
getDescription(aClass)));
         System.out.println();
         System.exit(exitCode.ordinal());
+        return null;
     }
 
-    private String getDescription(Class<?> clazz) {
+    protected String getDescription(Class<?> clazz) {
         try {
             Field declaredField = clazz.getDeclaredField(DESCRIPTION);
             return String.valueOf(declaredField.get(null));
         } catch (Exception e) {
-            return "Unable to get description. (" + e.getMessage() + ")";
+            return UNABLE_TO_GET_DESCRIPTION + e.getMessage() + ")";
         }
     }
 
-    public void doMain(String[] args) {
-        if (args.length < 1) {
-            printUsageAndExit("Expected at least a service argument.", 
ExitCode.INVALID_ARGS);
-        }
+    protected Map<String, Class<?>> getMainMap() {
+        return mainMap;
+    }
 
-        String service = args[0].toLowerCase();
+    protected Method getMain(String service) {
         Class<?> mainClass = mainMap.get(service);
         if (mainClass == null) {
             printUsageAndExit("Unknown service: " + service, 
ExitCode.INVALID_ARGS);
         }
 
-        Method main;
         try {
-            main = mainClass.getDeclaredMethod("main", String[].class);
+            return mainClass.getDeclaredMethod("main", String[].class);
         } catch (NoSuchMethodException e) {
-            printUsageAndExit("Service " + service + " is missing main 
method.", ExitCode.SERVICE_ERROR);
-            return;
+            return printUsageAndExit("Service " + service + " is missing main 
method.", ExitCode.SERVICE_ERROR);
+        }
+    }
+
+    public void doMain(String[] args) {
+        if (args.length < 1) {
+            printUsageAndExit("Expected at least a service argument.", 
ExitCode.INVALID_ARGS);
         }
 
+        String service = args[0].toLowerCase();
+
         try {
-            main.invoke(null, (Object) args);
+            getMain(service).invoke(null, (Object) args);
         } catch (IllegalAccessException e) {
             printUsageAndExit("Service " + service + " has invalid main 
method.", ExitCode.SERVICE_ERROR);
         } catch (InvocationTargetException e) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/InstanceDefinition.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/InstanceDefinition.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/InstanceDefinition.java
new file mode 100644
index 0000000..84b633f
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/InstanceDefinition.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.toolkit.tls.configuration;
+
+import org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Contains an instance identifier, a number that either corresponds to the 
instance identifier or to the global order and relevant passwords
+ */
+public class InstanceDefinition {
+    private final InstanceIdentifier instanceIdentifier;
+    private final int number;
+    private final String keyStorePassword;
+    private final String keyPassword;
+    private final String trustStorePassword;
+
+    public InstanceDefinition(InstanceIdentifier instanceIdentifier, int 
number, String keyStorePassword, String keyPassword, String trustStorePassword) 
{
+        this.number = number;
+        this.instanceIdentifier = instanceIdentifier;
+        this.keyStorePassword = keyStorePassword;
+        this.keyPassword = keyPassword;
+        this.trustStorePassword = trustStorePassword;
+    }
+
+    /**
+     * Creates a list of instance definitions
+     *
+     * @param fullHostNameExpressions    the host expressions defining the 
global order (null if not relevant)
+     * @param currentHostnameExpressions the host expressions to create 
instance definitions for
+     * @param keyStorePasswords          a supplier for keyStorePasswords
+     * @param keyPasswords               a supplier for keyPasswords
+     * @param trustStorePasswords        a supplier for trustStorePasswords
+     * @return a list of instance definitions
+     */
+    public static List<InstanceDefinition> createDefinitions(Stream<String> 
fullHostNameExpressions, Stream<String> currentHostnameExpressions, 
Supplier<String> keyStorePasswords,
+                                                             Supplier<String> 
keyPasswords, Supplier<String> trustStorePasswords) {
+        if (fullHostNameExpressions == null) {
+            return 
InstanceIdentifier.createIdentifiers(currentHostnameExpressions).map(id -> 
createDefinition(id, id.getNumber(), keyStorePasswords, keyPasswords, 
trustStorePasswords))
+                    .collect(Collectors.toList());
+        } else {
+            Map<InstanceIdentifier, Integer> orderMap = 
InstanceIdentifier.createOrderMap(fullHostNameExpressions);
+            return 
InstanceIdentifier.createIdentifiers(currentHostnameExpressions).map(id -> {
+                Integer number = orderMap.get(id);
+                if (number == null) {
+                    throw new IllegalArgumentException("Unable to find " + 
id.getHostname() + " in specified " + 
TlsToolkitStandaloneCommandLine.GLOBAL_PORT_SEQUENCE_ARG + " expression(s).");
+                }
+                return createDefinition(id, number, keyStorePasswords, 
keyPasswords, trustStorePasswords);
+            }).collect(Collectors.toList());
+        }
+    }
+
+    protected static InstanceDefinition createDefinition(InstanceIdentifier 
instanceIdentifier, int number, Supplier<String> keyStorePasswords, 
Supplier<String> keyPasswords,
+                                                         Supplier<String> 
trustStorePasswords) {
+        String keyStorePassword = keyStorePasswords.get();
+        String keyPassword;
+        if (keyPasswords == null) {
+            keyPassword = keyStorePassword;
+        } else {
+            keyPassword = keyPasswords.get();
+        }
+        String trustStorePassword = trustStorePasswords.get();
+        return new InstanceDefinition(instanceIdentifier, number, 
keyStorePassword, keyPassword, trustStorePassword);
+    }
+
+    public String getHostname() {
+        return instanceIdentifier.getHostname();
+    }
+
+    public int getNumber() {
+        return number;
+    }
+
+    public String getKeyStorePassword() {
+        return keyStorePassword;
+    }
+
+    public String getKeyPassword() {
+        return keyPassword;
+    }
+
+    public String getTrustStorePassword() {
+        return trustStorePassword;
+    }
+
+    public InstanceIdentifier getInstanceIdentifier() {
+        return instanceIdentifier;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/InstanceIdentifier.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/InstanceIdentifier.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/InstanceIdentifier.java
new file mode 100644
index 0000000..9699236
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/InstanceIdentifier.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.toolkit.tls.configuration;
+
+import org.apache.nifi.util.StringUtils;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+/**
+ * Each instance is uniquely identified by its hostname and instance number
+ */
+public class InstanceIdentifier {
+    public static final Comparator<InstanceIdentifier> 
HOST_IDENTIFIER_COMPARATOR = (o1, o2) -> {
+        int i = o1.getHostname().compareTo(o2.getHostname());
+        if (i == 0) {
+            return o1.getNumber() - o2.getNumber();
+        }
+        return i;
+    };
+    private static final Pattern RANGE_PATTERN = 
Pattern.compile("^[0-9]+(-[0-9]+)?$");
+    private final String hostname;
+    private final int number;
+
+    public InstanceIdentifier(String hostname, int number) {
+        this.hostname = hostname;
+        this.number = number;
+    }
+
+    /**
+     * Creates a map that can be used to deterministically assign global 
instance numbers
+     *
+     * @param hostnameExpressions the hostname expressions to expand
+     * @return a map that can be used to deterministically assign global 
instance numbers
+     */
+    public static Map<InstanceIdentifier, Integer> 
createOrderMap(Stream<String> hostnameExpressions) {
+        List<InstanceIdentifier> instanceIdentifiers = 
createIdentifiers(hostnameExpressions).sorted(HOST_IDENTIFIER_COMPARATOR).collect(Collectors.toList());
+        Map<InstanceIdentifier, Integer> result = new HashMap<>();
+        for (int i = 0; i < instanceIdentifiers.size(); i++) {
+            result.put(instanceIdentifiers.get(i), i + 1);
+        }
+        return result;
+    }
+
+    /**
+     * Creates a stream of hostname identifiers from a stream of hostname 
expressions
+     *
+     * @param hostnameExpressions the hostname expressions
+     * @return the hostname identifiers
+     */
+    public static Stream<InstanceIdentifier> createIdentifiers(Stream<String> 
hostnameExpressions) {
+        return hostnameExpressions.flatMap(hostnameExpression -> 
extractHostnames(hostnameExpression).flatMap(hostname -> {
+            ExtractedRange extractedRange = new ExtractedRange(hostname, '(', 
')');
+            if (extractedRange.range == null) {
+                return Stream.of(new InstanceIdentifier(hostname, 1));
+            }
+            if (!StringUtils.isEmpty(extractedRange.afterClose)) {
+                throw new IllegalArgumentException("No characters expected 
after )");
+            }
+            return extractedRange.range.map(numString -> new 
InstanceIdentifier(extractedRange.beforeOpen, Integer.parseInt(numString)));
+        }));
+    }
+
+    protected static Stream<String> extractHostnames(String hostname) {
+        ExtractedRange extractedRange = new ExtractedRange(hostname, '[', ']');
+        if (extractedRange.range == null) {
+            return Stream.of(hostname);
+        }
+        return extractedRange.range.map(s -> extractedRange.beforeOpen + s + 
extractedRange.afterClose).flatMap(InstanceIdentifier::extractHostnames);
+    }
+
+    private static Stream<String> extractRange(String range) {
+        if (!RANGE_PATTERN.matcher(range).matches()) {
+            throw new IllegalArgumentException("Expected either one number or 
two separated by a single hyphen");
+        }
+        String[] split = range.split("-");
+        if (split.length == 1) {
+            String prefix = "1-";
+            if (split[0].charAt(0) == '0') {
+                prefix = String.format("%0" + split[0].length() + "d-", 1);
+            }
+            return extractRange(prefix + split[0]);
+        } else {
+            int baseLength = split[0].length();
+            int low = Integer.parseInt(split[0]);
+            String padding = split[0].substring(0, split[0].length() - 
Integer.toString(low).length());
+            int high = Integer.parseInt(split[1]);
+            return IntStream.range(low, high + 1).mapToObj(i -> {
+                String s = Integer.toString(i);
+                int length = s.length();
+                if (length >= baseLength) {
+                    return s;
+                } else {
+                    return padding.substring(0, baseLength - length) + s;
+                }
+            });
+        }
+    }
+
+    public String getHostname() {
+        return hostname;
+    }
+
+    public int getNumber() {
+        return number;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        InstanceIdentifier that = (InstanceIdentifier) o;
+
+        if (number != that.number) return false;
+        return hostname != null ? hostname.equals(that.hostname) : 
that.hostname == null;
+
+    }
+
+    @Override
+    public int hashCode() {
+        int result = hostname != null ? hostname.hashCode() : 0;
+        result = 31 * result + number;
+        return result;
+    }
+
+    private static class ExtractedRange {
+        private final String beforeOpen;
+        private final Stream<String> range;
+        private final String afterClose;
+
+        public ExtractedRange(String string, char rangeOpen, char rangeClose) {
+            int openBracket = string.indexOf(rangeOpen);
+            if (openBracket >= 0) {
+                int closeBracket = string.indexOf(rangeClose, openBracket);
+                if (closeBracket < 0) {
+                    throw new IllegalArgumentException("Unable to find 
matching " + rangeClose + " for " + rangeOpen + " in " + string);
+                }
+                beforeOpen = string.substring(0, openBracket);
+                if (closeBracket + 1 < string.length()) {
+                    afterClose = string.substring(closeBracket + 1);
+                } else {
+                    afterClose = "";
+                }
+                range = extractRange(string.substring(openBracket + 1, 
closeBracket));
+            } else {
+                beforeOpen = string;
+                range = null;
+                afterClose = "";
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/StandaloneConfig.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/StandaloneConfig.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/StandaloneConfig.java
index 342e1e5..740a9a2 100644
--- 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/StandaloneConfig.java
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/StandaloneConfig.java
@@ -25,14 +25,10 @@ import java.util.List;
 public class StandaloneConfig extends TlsConfig {
     private File baseDir;
     private NiFiPropertiesWriterFactory niFiPropertiesWriterFactory;
-    private List<String> hostnames;
-    private List<String> keyStorePasswords;
-    private List<String> keyPasswords;
-    private List<String> trustStorePasswords;
+    private List<InstanceDefinition> instanceDefinitions;
     private List<String> clientDns;
     private List<String> clientPasswords;
     private boolean clientPasswordsGenerated;
-    private int httpsPort;
     private boolean overwrite;
 
     public List<String> getClientDns() {
@@ -67,38 +63,6 @@ public class StandaloneConfig extends TlsConfig {
         this.niFiPropertiesWriterFactory = niFiPropertiesWriterFactory;
     }
 
-    public List<String> getHostnames() {
-        return hostnames;
-    }
-
-    public void setHostnames(List<String> hostnames) {
-        this.hostnames = hostnames;
-    }
-
-    public List<String> getKeyStorePasswords() {
-        return keyStorePasswords;
-    }
-
-    public void setKeyStorePasswords(List<String> keyStorePasswords) {
-        this.keyStorePasswords = keyStorePasswords;
-    }
-
-    public List<String> getKeyPasswords() {
-        return keyPasswords;
-    }
-
-    public void setKeyPasswords(List<String> keyPasswords) {
-        this.keyPasswords = keyPasswords;
-    }
-
-    public List<String> getTrustStorePasswords() {
-        return trustStorePasswords;
-    }
-
-    public void setTrustStorePasswords(List<String> trustStorePasswords) {
-        this.trustStorePasswords = trustStorePasswords;
-    }
-
     public List<String> getClientPasswords() {
         return clientPasswords;
     }
@@ -107,14 +71,6 @@ public class StandaloneConfig extends TlsConfig {
         this.clientPasswords = clientPasswords;
     }
 
-    public int getHttpsPort() {
-        return httpsPort;
-    }
-
-    public void setHttpsPort(int httpsPort) {
-        this.httpsPort = httpsPort;
-    }
-
     public boolean isClientPasswordsGenerated() {
         return clientPasswordsGenerated;
     }
@@ -122,4 +78,12 @@ public class StandaloneConfig extends TlsConfig {
     public void setClientPasswordsGenerated(boolean clientPasswordsGenerated) {
         this.clientPasswordsGenerated = clientPasswordsGenerated;
     }
+
+    public List<InstanceDefinition> getInstanceDefinitions() {
+        return instanceDefinitions;
+    }
+
+    public void setInstanceDefinitions(List<InstanceDefinition> 
instanceDefinitions) {
+        this.instanceDefinitions = instanceDefinitions;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/writer/NifiPropertiesTlsClientConfigWriter.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/writer/NifiPropertiesTlsClientConfigWriter.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/writer/NifiPropertiesTlsClientConfigWriter.java
index e1b03da..c2ecca4 100644
--- 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/writer/NifiPropertiesTlsClientConfigWriter.java
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/writer/NifiPropertiesTlsClientConfigWriter.java
@@ -27,48 +27,92 @@ import org.apache.nifi.util.StringUtils;
 import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 public class NifiPropertiesTlsClientConfigWriter implements 
ConfigurationWriter<TlsClientConfig> {
+    public static final String HOSTNAME_PROPERTIES = "hostname.properties";
+    public static final String OVERLAY_PROPERTIES = "overlay.properties";
+    public static final String CONF = "./conf/";
     private final NiFiPropertiesWriterFactory niFiPropertiesWriterFactory;
     private final OutputStreamFactory outputStreamFactory;
-    private final File file;
+    private final File outputFile;
     private final String hostname;
-    private final int httpsPort;
+    private final int hostNum;
+    private final Properties overlayProperties;
+    private final Set<String> explicitProperties;
 
-    public NifiPropertiesTlsClientConfigWriter(NiFiPropertiesWriterFactory 
niFiPropertiesWriterFactory, OutputStreamFactory outputStreamFactory, File 
file, String hostname, int httpsPort) {
+    public NifiPropertiesTlsClientConfigWriter(NiFiPropertiesWriterFactory 
niFiPropertiesWriterFactory, OutputStreamFactory outputStreamFactory, File 
outputFile, String hostname, int hostNum)
+            throws IOException {
         this.niFiPropertiesWriterFactory = niFiPropertiesWriterFactory;
         this.outputStreamFactory = outputStreamFactory;
-        this.file = file;
+        this.outputFile = outputFile;
         this.hostname = hostname;
-        this.httpsPort = httpsPort;
+        this.hostNum = hostNum;
+        this.overlayProperties = new Properties();
+        
this.overlayProperties.load(getClass().getClassLoader().getResourceAsStream(OVERLAY_PROPERTIES));
+        HashSet<String> explicitProperties = new HashSet<>();
+        explicitProperties.add(HOSTNAME_PROPERTIES);
+        this.explicitProperties = 
Collections.unmodifiableSet(explicitProperties);
     }
 
     @Override
     public void write(TlsClientConfig tlsClientConfig) throws IOException {
         NiFiPropertiesWriter niFiPropertiesWriter = 
niFiPropertiesWriterFactory.create();
         updateProperties(niFiPropertiesWriter, tlsClientConfig);
-        try (OutputStream stream = outputStreamFactory.create(file)) {
+        try (OutputStream stream = outputStreamFactory.create(outputFile)) {
             niFiPropertiesWriter.writeNiFiProperties(stream);
         }
     }
 
-    protected void updateProperties(NiFiPropertiesWriter niFiPropertiesWriter, 
TlsClientConfig tlsClientConfig) {
-        Path parentPath = Paths.get(file.getParentFile().getAbsolutePath());
-        
niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_KEYSTORE, 
parentPath.relativize(Paths.get(tlsClientConfig.getKeyStore())).toString());
+    protected void updateProperties(NiFiPropertiesWriter niFiPropertiesWriter, 
TlsClientConfig tlsClientConfig) throws IOException {
+        
niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_KEYSTORE, CONF + 
new File(tlsClientConfig.getKeyStore()).getName());
         
niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_KEYSTORE_TYPE, 
tlsClientConfig.getKeyStoreType());
         
niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_KEYSTORE_PASSWD, 
tlsClientConfig.getKeyStorePassword());
         
niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_KEY_PASSWD, 
tlsClientConfig.getKeyPassword());
-        
niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_TRUSTSTORE, 
parentPath.relativize(Paths.get(tlsClientConfig.getTrustStore())).toString());
+
+        
niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_TRUSTSTORE, CONF 
+ new File(tlsClientConfig.getTrustStore()).getName());
         
niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_TRUSTSTORE_TYPE, 
tlsClientConfig.getTrustStoreType());
         
niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD,
 tlsClientConfig.getTrustStorePassword());
-        if (!StringUtils.isEmpty(hostname)) {
-            
niFiPropertiesWriter.setPropertyValue(NiFiProperties.WEB_HTTPS_HOST, hostname);
-        }
-        niFiPropertiesWriter.setPropertyValue(NiFiProperties.WEB_HTTPS_PORT, 
Integer.toString(httpsPort));
+
         niFiPropertiesWriter.setPropertyValue(NiFiProperties.WEB_HTTP_HOST, 
"");
         niFiPropertiesWriter.setPropertyValue(NiFiProperties.WEB_HTTP_PORT, 
"");
         
niFiPropertiesWriter.setPropertyValue(NiFiProperties.SITE_TO_SITE_SECURE, 
"true");
+
+        getHostnamePropertyStream().forEach(s -> 
niFiPropertiesWriter.setPropertyValue(s, hostname));
+
+        getPropertyPortMap().entrySet().forEach(nameToPortEntry -> 
niFiPropertiesWriter.setPropertyValue(nameToPortEntry.getKey(), 
Integer.toString(nameToPortEntry.getValue())));
+    }
+
+    protected Properties getOverlayProperties() {
+        return overlayProperties;
+    }
+
+    protected Map<String, Integer> getPropertyPortMap() {
+        return overlayProperties.stringPropertyNames().stream().filter(s -> 
!explicitProperties.contains(s)).collect(Collectors.toMap(Function.identity(), 
portProperty -> {
+            String portVal = overlayProperties.getProperty(portProperty);
+            int startingPort;
+            try {
+                startingPort = Integer.parseInt(portVal);
+            } catch (NumberFormatException e) {
+                throw new NumberFormatException("Expected numeric values in " 
+ OVERLAY_PROPERTIES + " (" + portProperty + " was " + portVal + ")");
+            }
+            return startingPort + hostNum - 1;
+        }));
+    }
+
+    protected Stream<String> getHostnamePropertyStream() {
+        String hostnamePropertyString = 
overlayProperties.getProperty(HOSTNAME_PROPERTIES);
+        if (!StringUtils.isEmpty(hostnamePropertyString)) {
+            return 
Arrays.stream(hostnamePropertyString.split(",")).map(String::trim);
+        }
+        return Stream.of();
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java
index c91e7fa..8dc5186 100644
--- 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java
@@ -18,6 +18,7 @@
 package org.apache.nifi.toolkit.tls.standalone;
 
 import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.toolkit.tls.configuration.InstanceDefinition;
 import org.apache.nifi.toolkit.tls.configuration.StandaloneConfig;
 import org.apache.nifi.toolkit.tls.configuration.TlsClientConfig;
 import org.apache.nifi.toolkit.tls.configuration.TlsConfig;
@@ -84,7 +85,6 @@ public class TlsToolkitStandalone {
         X509Certificate certificate;
         KeyPair caKeyPair;
 
-        List<String> hostnames = standaloneConfig.getHostnames();
         if (logger.isInfoEnabled()) {
             logger.info("Running standalone certificate generation with output 
directory " + baseDir);
         }
@@ -128,19 +128,22 @@ public class TlsToolkitStandalone {
             }
         }
 
-        List<String> keyStorePasswords = 
standaloneConfig.getKeyStorePasswords();
-        List<String> keyPasswords = standaloneConfig.getKeyPasswords();
-        List<String> trustStorePasswords = 
standaloneConfig.getTrustStorePasswords();
         NiFiPropertiesWriterFactory niFiPropertiesWriterFactory = 
standaloneConfig.getNiFiPropertiesWriterFactory();
-        int httpsPort = standaloneConfig.getHttpsPort();
         boolean overwrite = standaloneConfig.isOverwrite();
 
-        if (hostnames.isEmpty() && logger.isInfoEnabled()) {
+        List<InstanceDefinition> instanceDefinitions = 
standaloneConfig.getInstanceDefinitions();
+        if (!instanceDefinitions.isEmpty() && logger.isInfoEnabled()) {
             logger.info("No " + TlsToolkitStandaloneCommandLine.HOSTNAMES_ARG 
+ " specified, not generating any host certificates or configuration.");
         }
-        for (int i = 0; i < hostnames.size(); i++) {
-            String hostname = hostnames.get(i);
-            File hostDir = new File(baseDir, hostname);
+        for (InstanceDefinition instanceDefinition : instanceDefinitions) {
+            String hostname = instanceDefinition.getHostname();
+            File hostDir;
+            int hostIdentifierNumber = 
instanceDefinition.getInstanceIdentifier().getNumber();
+            if (hostIdentifierNumber == 1) {
+                hostDir = new File(baseDir, hostname);
+            } else {
+                hostDir = new File(baseDir, hostname + "_" + 
hostIdentifierNumber);
+            }
 
             TlsClientConfig tlsClientConfig = new 
TlsClientConfig(standaloneConfig);
             File keystore = new File(hostDir, "keystore." + 
tlsClientConfig.getKeyStoreType().toLowerCase());
@@ -171,20 +174,20 @@ public class TlsToolkitStandalone {
             }
 
             tlsClientConfig.setKeyStore(keystore.getAbsolutePath());
-            tlsClientConfig.setKeyStorePassword(keyStorePasswords.get(i));
-            tlsClientConfig.setKeyPassword(keyPasswords.get(i));
+            
tlsClientConfig.setKeyStorePassword(instanceDefinition.getKeyStorePassword());
+            
tlsClientConfig.setKeyPassword(instanceDefinition.getKeyPassword());
             tlsClientConfig.setTrustStore(truststore.getAbsolutePath());
-            tlsClientConfig.setTrustStorePassword(trustStorePasswords.get(i));
+            
tlsClientConfig.setTrustStorePassword(instanceDefinition.getTrustStorePassword());
             TlsClientManager tlsClientManager = new 
TlsClientManager(tlsClientConfig);
             KeyPair keyPair = TlsHelper.generateKeyPair(keyPairAlgorithm, 
keySize);
             tlsClientManager.addPrivateKeyToKeyStore(keyPair, NIFI_KEY, 
CertificateUtils.generateIssuedCertificate(TlsConfig.calcDefaultDn(hostname),
                     keyPair.getPublic(), certificate, caKeyPair, 
signingAlgorithm, days), certificate);
             tlsClientManager.setCertificateEntry(NIFI_CERT, certificate);
             tlsClientManager.addClientConfigurationWriter(new 
NifiPropertiesTlsClientConfigWriter(niFiPropertiesWriterFactory, 
outputStreamFactory, new File(hostDir, "nifi.properties"),
-                    hostname, httpsPort));
+                    hostname, instanceDefinition.getNumber()));
             tlsClientManager.write(outputStreamFactory);
             if (logger.isInfoEnabled()) {
-                logger.info("Successfully generated TLS configuration for " + 
hostname + ":" + httpsPort + " in " + hostDir);
+                logger.info("Successfully generated TLS configuration for " + 
hostname + " " + hostIdentifierNumber + " in " + hostDir);
             }
         }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLine.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLine.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLine.java
index 602fce4..4ba0746 100644
--- 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLine.java
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLine.java
@@ -21,6 +21,7 @@ import org.apache.commons.cli.CommandLine;
 import org.apache.nifi.toolkit.tls.commandLine.BaseCommandLine;
 import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
 import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.configuration.InstanceDefinition;
 import org.apache.nifi.toolkit.tls.configuration.StandaloneConfig;
 import org.apache.nifi.toolkit.tls.properties.NiFiPropertiesWriterFactory;
 import org.apache.nifi.toolkit.tls.util.PasswordUtil;
@@ -33,12 +34,13 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.nio.file.Paths;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
+import java.util.stream.Stream;
 
 public class TlsToolkitStandaloneCommandLine extends BaseCommandLine {
     public static final String OUTPUT_DIRECTORY_ARG = "outputDirectory";
@@ -47,13 +49,12 @@ public class TlsToolkitStandaloneCommandLine extends 
BaseCommandLine {
     public static final String TRUST_STORE_PASSWORD_ARG = "trustStorePassword";
     public static final String KEY_PASSWORD_ARG = "keyPassword";
     public static final String HOSTNAMES_ARG = "hostnames";
-    public static final String HTTPS_PORT_ARG = "httpsPort";
     public static final String OVERWRITE_ARG = "isOverwrite";
     public static final String CLIENT_CERT_DN_ARG = "clientCertDn";
     public static final String CLIENT_CERT_PASSWORD_ARG = "clientCertPassword";
+    public static final String GLOBAL_PORT_SEQUENCE_ARG = "globalPortSequence";
 
     public static final String DEFAULT_OUTPUT_DIRECTORY = "../" + 
Paths.get(".").toAbsolutePath().normalize().getFileName().toString();
-    public static final int DEFAULT_HTTPS_PORT = 9091;
 
     public static final String DESCRIPTION = "Creates certificates and config 
files for nifi cluster.";
 
@@ -61,12 +62,8 @@ public class TlsToolkitStandaloneCommandLine extends 
BaseCommandLine {
 
     private final PasswordUtil passwordUtil;
     private File baseDir;
-    private List<String> hostnames;
-    private int httpsPort;
+    private List<InstanceDefinition> instanceDefinitions;
     private NiFiPropertiesWriterFactory niFiPropertiesWriterFactory;
-    private List<String> keyStorePasswords;
-    private List<String> keyPasswords;
-    private List<String> trustStorePasswords;
     private List<String> clientDns;
     private List<String> clientPasswords;
     private boolean clientPasswordsGenerated;
@@ -81,13 +78,14 @@ public class TlsToolkitStandaloneCommandLine extends 
BaseCommandLine {
         this.passwordUtil = passwordUtil;
         addOptionWithArg("o", OUTPUT_DIRECTORY_ARG, "The directory to output 
keystores, truststore, config files.", DEFAULT_OUTPUT_DIRECTORY);
         addOptionWithArg("n", HOSTNAMES_ARG, "Comma separated list of 
hostnames.");
-        addOptionWithArg("p", HTTPS_PORT_ARG, "Https port to use.", 
DEFAULT_HTTPS_PORT);
         addOptionWithArg("f", NIFI_PROPERTIES_FILE_ARG, "Base nifi.properties 
file to update. (Embedded file identical to the one in a default NiFi install 
will be used if not specified.)");
         addOptionWithArg("S", KEY_STORE_PASSWORD_ARG, "Keystore password to 
use.  Must either be one value or one for each host. (autogenerate if not 
specified)");
         addOptionWithArg("K", KEY_PASSWORD_ARG, "Key password to use.  Must 
either be one value or one for each host. (autogenerate if not specified)");
         addOptionWithArg("P", TRUST_STORE_PASSWORD_ARG, "Keystore password to 
use.  Must either be one value or one for each host. (autogenerate if not 
specified)");
         addOptionWithArg("C", CLIENT_CERT_DN_ARG, "Generate client certificate 
suitable for use in browser with specified DN. (Can be specified multiple 
times.)");
         addOptionWithArg("B", CLIENT_CERT_PASSWORD_ARG, "Password for client 
certificate.  Must either be one value or one for each client DN. (autogenerate 
if not specified)");
+        addOptionWithArg("G", GLOBAL_PORT_SEQUENCE_ARG, "Use sequential ports 
that are calculated for all hosts according to the provided hostname 
expressions. " +
+                "(Can be specified multiple times, MUST BE SAME FROM RUN TO 
RUN.)");
         addOptionNoArg("O", OVERWRITE_ARG, "Overwrite existing host output.");
     }
 
@@ -114,10 +112,20 @@ public class TlsToolkitStandaloneCommandLine extends 
BaseCommandLine {
         String outputDirectory = 
commandLine.getOptionValue(OUTPUT_DIRECTORY_ARG, DEFAULT_OUTPUT_DIRECTORY);
         baseDir = new File(outputDirectory);
 
+        Stream<String> globalOrderExpressions = null;
+        if (commandLine.hasOption(GLOBAL_PORT_SEQUENCE_ARG)) {
+            globalOrderExpressions = 
Arrays.stream(commandLine.getOptionValues(GLOBAL_PORT_SEQUENCE_ARG)).flatMap(s 
-> Arrays.stream(s.split(","))).map(String::trim);
+        }
+
         if (commandLine.hasOption(HOSTNAMES_ARG)) {
-            hostnames = 
Collections.unmodifiableList(Arrays.stream(commandLine.getOptionValue(HOSTNAMES_ARG).split(",")).map(String::trim).collect(Collectors.toList()));
+            instanceDefinitions = Collections.unmodifiableList(
+                    
InstanceDefinition.createDefinitions(globalOrderExpressions,
+                    
Arrays.stream(commandLine.getOptionValues(HOSTNAMES_ARG)).flatMap(s -> 
Arrays.stream(s.split(",")).map(String::trim)),
+                    parsePasswordSupplier(commandLine, KEY_STORE_PASSWORD_ARG, 
passwordUtil.passwordSupplier()),
+                    parsePasswordSupplier(commandLine, KEY_PASSWORD_ARG, 
commandLine.hasOption(DIFFERENT_KEY_AND_KEYSTORE_PASSWORDS_ARG) ? 
passwordUtil.passwordSupplier() : null),
+                    parsePasswordSupplier(commandLine, 
TRUST_STORE_PASSWORD_ARG, passwordUtil.passwordSupplier())));
         } else {
-            hostnames = Collections.emptyList();
+            instanceDefinitions = Collections.emptyList();
         }
 
         String[] clientDnValues = 
commandLine.getOptionValues(CLIENT_CERT_DN_ARG);
@@ -127,12 +135,6 @@ public class TlsToolkitStandaloneCommandLine extends 
BaseCommandLine {
             clientDns = Collections.emptyList();
         }
 
-        httpsPort = getIntValue(commandLine, HTTPS_PORT_ARG, 
DEFAULT_HTTPS_PORT);
-
-        int numHosts = hostnames.size();
-        keyStorePasswords = 
Collections.unmodifiableList(getPasswords(KEY_STORE_PASSWORD_ARG, commandLine, 
numHosts, HOSTNAMES_ARG));
-        keyPasswords = 
Collections.unmodifiableList(getKeyPasswords(commandLine, keyStorePasswords));
-        trustStorePasswords = 
Collections.unmodifiableList(getPasswords(TRUST_STORE_PASSWORD_ARG, 
commandLine, numHosts, HOSTNAMES_ARG));
         clientPasswords = 
Collections.unmodifiableList(getPasswords(CLIENT_CERT_PASSWORD_ARG, 
commandLine, clientDns.size(), CLIENT_CERT_DN_ARG));
         clientPasswordsGenerated = 
commandLine.getOptionValues(CLIENT_CERT_PASSWORD_ARG) == null;
         overwrite = commandLine.hasOption(OVERWRITE_ARG);
@@ -165,11 +167,18 @@ public class TlsToolkitStandaloneCommandLine extends 
BaseCommandLine {
         return printUsageAndThrow("Expected either 1 value or " + num + " (the 
number of " + numArg + ") values for " + arg, 
ExitCode.ERROR_INCORRECT_NUMBER_OF_PASSWORDS);
     }
 
-    private List<String> getKeyPasswords(CommandLine commandLine, List<String> 
keyStorePasswords) throws CommandLineParseException {
-        if (differentPasswordForKeyAndKeystore() || 
commandLine.hasOption(KEY_PASSWORD_ARG)) {
-            return getPasswords(KEY_PASSWORD_ARG, commandLine, 
keyStorePasswords.size(), HOSTNAMES_ARG);
+    private Supplier<String> parsePasswordSupplier(CommandLine commandLine, 
String option, Supplier<String> defaultSupplier) {
+        if (commandLine.hasOption(option)) {
+            String[] values = commandLine.getOptionValues(option);
+            if (values.length == 1) {
+                return PasswordUtil.passwordSupplier(values[0]);
+            } else {
+                return PasswordUtil.passwordSupplier("Provided " + option + " 
exhausted, please don't specify " + option
+                        + ", specify one value to be used for all NiFi 
instances, or specify one value for each NiFi instance.", values);
+            }
+        } else {
+            return defaultSupplier;
         }
-        return new ArrayList<>(keyStorePasswords);
     }
 
     public StandaloneConfig createConfig() {
@@ -177,11 +186,7 @@ public class TlsToolkitStandaloneCommandLine extends 
BaseCommandLine {
 
         standaloneConfig.setBaseDir(baseDir);
         
standaloneConfig.setNiFiPropertiesWriterFactory(niFiPropertiesWriterFactory);
-        standaloneConfig.setHostnames(hostnames);
-        standaloneConfig.setKeyStorePasswords(keyStorePasswords);
-        standaloneConfig.setKeyPasswords(keyPasswords);
-        standaloneConfig.setTrustStorePasswords(trustStorePasswords);
-        standaloneConfig.setHttpsPort(httpsPort);
+        standaloneConfig.setInstanceDefinitions(instanceDefinitions);
         standaloneConfig.setOverwrite(overwrite);
         standaloneConfig.setClientDns(clientDns);
         standaloneConfig.setClientPasswords(clientPasswords);

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/PasswordUtil.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/PasswordUtil.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/PasswordUtil.java
index c26fd7f..2c7a6aa 100644
--- 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/PasswordUtil.java
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/PasswordUtil.java
@@ -20,6 +20,8 @@ package org.apache.nifi.toolkit.tls.util;
 import java.math.BigInteger;
 import java.security.SecureRandom;
 import java.util.Base64;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
 
 public class PasswordUtil {
     private final SecureRandom secureRandom;
@@ -40,4 +42,24 @@ public class PasswordUtil {
         }
         return string;
     }
+
+    public Supplier<String> passwordSupplier() {
+        return () -> generatePassword();
+    }
+
+    public static Supplier<String> passwordSupplier(String password) {
+        return () -> password;
+    }
+
+    public static Supplier<String> passwordSupplier(String exhaustedMessage, 
String[] passwords) {
+        AtomicInteger index = new AtomicInteger(0);
+        return () -> {
+            int i = index.getAndIncrement();
+            if (i < passwords.length) {
+                return passwords[i];
+            } else {
+                throw new PasswordsExhaustedException(exhaustedMessage);
+            }
+        };
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/PasswordsExhaustedException.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/PasswordsExhaustedException.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/PasswordsExhaustedException.java
new file mode 100644
index 0000000..6d26a19
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/PasswordsExhaustedException.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.toolkit.tls.util;
+
+public class PasswordsExhaustedException extends RuntimeException {
+    public PasswordsExhaustedException(String message) {
+        super(message);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/ExitException.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/ExitException.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/ExitException.java
new file mode 100644
index 0000000..5134353
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/ExitException.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.toolkit.tls;
+
+public class ExitException extends SecurityException {
+    // [see 
http://stackoverflow.com/questions/309396/java-how-to-test-methods-that-call-system-exit#answer-309427]
+    private final int exitCode;
+
+    public ExitException(int exitCode) {
+        this.exitCode = exitCode;
+    }
+
+    public int getExitCode() {
+        return exitCode;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/SystemExitCapturer.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/SystemExitCapturer.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/SystemExitCapturer.java
new file mode 100644
index 0000000..e4552a3
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/SystemExitCapturer.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.toolkit.tls;
+
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.security.Permission;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class SystemExitCapturer implements Closeable {
+    private final SecurityManager originalSecurityManager;
+
+    public SystemExitCapturer() {
+        originalSecurityManager = System.getSecurityManager();
+        // [see 
http://stackoverflow.com/questions/309396/java-how-to-test-methods-that-call-system-exit#answer-309427]
+        System.setSecurityManager(new SecurityManager() {
+            @Override
+            public void checkPermission(Permission perm) {
+                // Noop
+            }
+
+            @Override
+            public void checkPermission(Permission perm, Object context) {
+                // Noop
+            }
+
+            @Override
+            public void checkExit(int status) {
+                super.checkExit(status);
+                throw new ExitException(status);
+            }
+        });
+    }
+
+    public void runAndAssertExitCode(Runnable runnable, ExitCode exitCode) {
+        try {
+            runnable.run();
+            fail("Expecting exit code " + exitCode);
+        } catch (ExitException e) {
+            assertEquals("Expecting exit code: " + exitCode + ", got " + 
ExitCode.values()[e.getExitCode()], exitCode.ordinal(), e.getExitCode());
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        System.setSecurityManager(originalSecurityManager);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/TlsToolkitMainTest.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/TlsToolkitMainTest.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/TlsToolkitMainTest.java
new file mode 100644
index 0000000..c9a74dd
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/TlsToolkitMainTest.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.toolkit.tls;
+
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.util.StringUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class TlsToolkitMainTest {
+    private TlsToolkitMain tlsToolkitMain;
+    private SystemExitCapturer systemExitCapturer;
+
+    @Before
+    public void setup() {
+        systemExitCapturer = new SystemExitCapturer();
+        tlsToolkitMain = new TlsToolkitMain();
+    }
+
+    @After
+    public void tearDown() throws IOException {
+        systemExitCapturer.close();
+    }
+
+    @Test
+    public void testAllMainClassesHaveDescription() {
+        tlsToolkitMain.getMainMap().values().forEach(mainClass -> {
+            String description = tlsToolkitMain.getDescription(mainClass);
+            assertFalse(StringUtils.isEmpty(description));
+            
assertFalse(description.contains(TlsToolkitMain.UNABLE_TO_GET_DESCRIPTION));
+        });
+    }
+
+    @Test
+    public void testGetDescriptionClassWithNoDescription() {
+        
assertTrue(tlsToolkitMain.getDescription(TlsToolkitMainTest.class).startsWith(TlsToolkitMain.UNABLE_TO_GET_DESCRIPTION));
+    }
+
+    @Test
+    public void testAllMainClassesHaveMain() {
+        
tlsToolkitMain.getMainMap().keySet().stream().map(String::toLowerCase).forEach(service
 -> {
+            assertNotNull(tlsToolkitMain.getMain(service));
+        });
+    }
+
+    @Test
+    public void testWrongServiceName() {
+        systemExitCapturer.runAndAssertExitCode(() -> 
tlsToolkitMain.doMain(new String[] {"fakeService"}), ExitCode.INVALID_ARGS);
+    }
+
+    @Test
+    public void testNoArguments() {
+        systemExitCapturer.runAndAssertExitCode(() -> 
tlsToolkitMain.doMain(new String[0]), ExitCode.INVALID_ARGS);
+    }
+
+    @Test
+    public void testInaccessibleMain() {
+        String privateMain = "privatemain";
+        tlsToolkitMain.getMainMap().put(privateMain, PrivateMain.class);
+        systemExitCapturer.runAndAssertExitCode(() -> 
tlsToolkitMain.doMain(new String[]{privateMain}), ExitCode.SERVICE_ERROR);
+    }
+
+    @Test
+    public void testInvocationTargetException() {
+        String throwingMain = "throwingmain";
+        tlsToolkitMain.getMainMap().put(throwingMain, ThrowingMain.class);
+        systemExitCapturer.runAndAssertExitCode(() -> 
tlsToolkitMain.doMain(new String[]{throwingMain}), ExitCode.SERVICE_ERROR);
+    }
+
+    @Test
+    public void testNoMain() {
+        String noMain = "nomain";
+        tlsToolkitMain.getMainMap().put(noMain, NoMain.class);
+        systemExitCapturer.runAndAssertExitCode(() -> 
tlsToolkitMain.doMain(new String[]{noMain}), ExitCode.SERVICE_ERROR);
+    }
+
+    private static class PrivateMain {
+        private static void main(String[] args) {
+
+        }
+    }
+
+    private static class ThrowingMain {
+        public static void main(String[] args) {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    private static class NoMain {
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/configuration/InstanceDefinitionTest.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/configuration/InstanceDefinitionTest.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/configuration/InstanceDefinitionTest.java
new file mode 100644
index 0000000..9d4fcd0
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/configuration/InstanceDefinitionTest.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.toolkit.tls.configuration;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class InstanceDefinitionTest {
+
+    @Test
+    public void testCreateDefinitionKeyPassword() {
+        testCreateDefinition("testHostname", 4, "keyStorePassword", 
"keyPassword", "trustStorePassword");
+    }
+
+    @Test
+    public void testCreateDefinitionNoKeyPassword() {
+        testCreateDefinition("testHostname", 5, "keyStorePassword", null, 
"trustStorePassword");
+    }
+
+    @Test
+    public void testCreateDefinitionsSingleHostSingleName() {
+        testCreateDefinitions(Arrays.asList("hostname"), 
Arrays.asList("hostname"), Arrays.asList(1), false);
+    }
+
+    @Test
+    public void testCreateDefinitionsSingleHostnameOneNumberInParens() {
+        testCreateDefinitions(Arrays.asList("hostname(20)"),
+                IntStream.range(1, 21).mapToObj(operand -> 
"hostname").collect(Collectors.toList()),
+                integerRange(1, 20).collect(Collectors.toList()), false);
+    }
+
+    @Test
+    public void testCreateDefinitionsSingleHostnameTwoNumbersInParens() {
+        testCreateDefinitions(Arrays.asList("hostname(5-20)"),
+                IntStream.range(5, 21).mapToObj(operand -> 
"hostname").collect(Collectors.toList()),
+                integerRange(5, 20).collect(Collectors.toList()), false);
+    }
+
+    @Test
+    public void testCreateDefinitionsMultipleHostnamesWithMultipleNumbers() {
+        testCreateDefinitions(Arrays.asList("host[10]name[02-5](20)"),
+                integerRange(1, 10).flatMap(v -> integerRange(2, 5).flatMap(v2 
-> integerRange(1, 20).map(v3 -> "host" + v + "name" + String.format("%02d", 
v2)))).collect(Collectors.toList()),
+                integerRange(1, 10).flatMap(val -> integerRange(2, 
5).flatMap(val2 -> integerRange(1, 20))).collect(Collectors.toList()), false);
+    }
+
+    @Test
+    public void testCreateDefinitionsStream() {
+        testCreateDefinitions(Arrays.asList("host", "name"), 
Arrays.asList("host", "name"), Arrays.asList(1, 1), true);
+    }
+
+    @Test
+    public void testCreateDefinitionsStreamNonNullKeyPasswords() {
+        testCreateDefinitions(Arrays.asList("host", "name"), 
Arrays.asList("host", "name"), Arrays.asList(1, 1), false);
+    }
+
+    private Stream<Integer> integerRange(int start, int endInclusive) {
+        return IntStream.range(start, endInclusive + 1).mapToObj(value -> 
value);
+    }
+
+    private void testCreateDefinitions(List<String> hostnameExpressions, 
List<String> expectedHostnames, List<Integer> expectedNumbers, boolean 
nullForKeyPasswords) {
+        List<String> keyStorePasswords = IntStream.range(0, 
expectedHostnames.size()).mapToObj(value -> "testKeyStorePassword" + 
value).collect(Collectors.toList());
+        List<String> keyPasswords;
+        if (nullForKeyPasswords) {
+            keyPasswords = null;
+        } else {
+            keyPasswords = IntStream.range(0, 
expectedHostnames.size()).mapToObj(value -> "testKeyPassword" + 
value).collect(Collectors.toList());
+        }
+        List<String> trustStorePasswords = IntStream.range(0, 
expectedHostnames.size()).mapToObj(value -> "testTrustStorePassword" + 
value).collect(Collectors.toList());
+        List<InstanceDefinition> instanceDefinitions = 
InstanceDefinition.createDefinitions(null, hostnameExpressions.stream(),
+                mockSupplier(keyStorePasswords.toArray(new 
String[keyStorePasswords.size()])), keyPasswords == null ? null : 
mockSupplier(keyPasswords.toArray(new String[keyPasswords.size()])),
+                mockSupplier(trustStorePasswords.toArray(new 
String[trustStorePasswords.size()])));
+        testCreateDefinitionsOutput(instanceDefinitions, expectedHostnames, 
expectedNumbers, keyStorePasswords, keyPasswords, trustStorePasswords);
+    }
+
+    private void testCreateDefinitionsOutput(List<InstanceDefinition> 
instanceDefinitions, List<String> expectedHostnames, List<Integer> 
expectedNumbers, List<String> keyStorePasswords,
+                                             List<String> keyPasswords, 
List<String> trustStorePasswords) {
+        assertEquals(instanceDefinitions.size(), expectedHostnames.size());
+        for (int i = 0; i < instanceDefinitions.size(); i++) {
+            assertDefinitionEquals(instanceDefinitions.get(i), 
expectedHostnames.get(i), expectedNumbers.get(i), keyStorePasswords.get(i),
+                    keyPasswords == null ? null : keyPasswords.get(i), 
trustStorePasswords.get(i));
+        }
+    }
+
+    private void testCreateDefinition(String hostname, int num, String 
keyStorePassword, String keyPassword, String trustStorePassword) {
+        InstanceDefinition definition = 
InstanceDefinition.createDefinition(new InstanceIdentifier(hostname, num), num, 
mockSupplier(keyStorePassword),
+                keyPassword == null ? null : mockSupplier(keyPassword), 
mockSupplier(trustStorePassword));
+        assertDefinitionEquals(definition, hostname, num, keyStorePassword, 
keyPassword, trustStorePassword);
+    }
+
+    private void assertDefinitionEquals(InstanceDefinition definition, String 
hostname, int num, String keyStorePassword, String keyPassword, String 
trustStorePassword) {
+        assertEquals(hostname, definition.getHostname());
+        assertEquals(num, definition.getNumber());
+        assertEquals(keyStorePassword, definition.getKeyStorePassword());
+        assertEquals(keyPassword == null ? keyStorePassword : keyPassword, 
definition.getKeyPassword());
+        assertEquals(trustStorePassword, definition.getTrustStorePassword());
+    }
+
+    private <T> Supplier<T> mockSupplier(T... values) {
+        Supplier<T> supplier = mock(Supplier.class);
+        if (values.length == 1) {
+            when(supplier.get()).thenReturn(values[0]);
+        } else if (values.length > 1) {
+            when(supplier.get()).thenReturn(values[0], 
Arrays.copyOfRange(values, 1, values.length));
+        }
+        return supplier;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/configuration/InstanceIdentifierTest.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/configuration/InstanceIdentifierTest.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/configuration/InstanceIdentifierTest.java
new file mode 100644
index 0000000..b75c02f
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/configuration/InstanceIdentifierTest.java
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.toolkit.tls.configuration;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import static org.junit.Assert.assertEquals;
+
+public class InstanceIdentifierTest {
+
+    @Test
+    public void testExtractHostnamesSingle() {
+        testExtractHostnames("test[1-3]", "test1", "test2", "test3");
+    }
+
+    @Test
+    public void testExtractHostnamesPadding() {
+        testExtractHostnames("test[0001-3]", "test0001", "test0002", 
"test0003");
+    }
+
+    @Test
+    public void testExtractHostnamesLowGreaterThanHigh() {
+        testExtractHostnames("test[3-1]");
+    }
+
+    @Test
+    public void testExtractHostnamesLowEqualToHigh() {
+        testExtractHostnames("test[3-3]", "test3");
+    }
+
+    @Test
+    public void testExtractHostnamesSingleNumber() {
+        testExtractHostnames("test[2]", "test1", "test2");
+    }
+
+    @Test
+    public void testExtractHostnamesSingleNumberPadding() {
+        testExtractHostnames("test[002]", "test001", "test002");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testExtractHostnamesNoNumber() {
+        testExtractHostnames("test[]", "test");
+    }
+
+    @Test
+    public void testExtractHostnamesMultiple() {
+        testExtractHostnames("test[1-3]name[1-3]", "test1name1", "test1name2", 
"test1name3", "test2name1", "test2name2", "test2name3", "test3name1", 
"test3name2", "test3name3");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testExtractHostnamesUnmatched() {
+        testExtractHostnames("test[");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testExtractHostnamesSpace() {
+        testExtractHostnames("test[ 1-2]");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testExtractHostnamesMultipleHyphens() {
+        testExtractHostnames("test[1-2-3]");
+    }
+
+    @Test
+    public void testCreateDefinitionsSingleHostSingleName() {
+        testCreateIdentifiers(Arrays.asList("hostname"), 
Arrays.asList("hostname"), Arrays.asList(1));
+    }
+
+    @Test
+    public void testCreateDefinitionsSingleHostnameOneNumberInParens() {
+        testCreateIdentifiers(Arrays.asList("hostname(20)"),
+                IntStream.range(1, 21).mapToObj(operand -> 
"hostname").collect(Collectors.toList()),
+                integerRange(1, 20).collect(Collectors.toList()));
+    }
+
+    @Test
+    public void testCreateDefinitionsSingleHostnameTwoNumbersInParens() {
+        testCreateIdentifiers(Arrays.asList("hostname(5-20)"),
+                IntStream.range(5, 21).mapToObj(operand -> 
"hostname").collect(Collectors.toList()),
+                integerRange(5, 20).collect(Collectors.toList()));
+    }
+
+    @Test
+    public void testCreateDefinitionsMultipleHostnamesWithMultipleNumbers() {
+        testCreateIdentifiers(Arrays.asList("host[10]name[02-5](20)"),
+                integerRange(1, 10).flatMap(v -> integerRange(2, 5).flatMap(v2 
-> integerRange(1, 20).map(v3 -> "host" + v + "name" + String.format("%02d", 
v2)))).collect(Collectors.toList()),
+                integerRange(1, 10).flatMap(val -> integerRange(2, 
5).flatMap(val2 -> integerRange(1, 20))).collect(Collectors.toList()));
+    }
+
+    @Test
+    public void testCreateDefinitionsStream() {
+        testCreateIdentifiers(Arrays.asList("host", "name"), 
Arrays.asList("host", "name"), Arrays.asList(1, 1));
+    }
+
+    @Test
+    public void testCreateOrderMap() {
+        String abc123 = "abc[1-3]";
+        String abc0123 = "abc[01-3]";
+        String b = "b";
+
+        Map<InstanceIdentifier, Integer> orderMap = 
InstanceIdentifier.createOrderMap(Stream.of(abc123, abc0123 + "(2)", b));
+
+        AtomicInteger num = new AtomicInteger(1);
+        Consumer<InstanceIdentifier> action = id -> {
+            int i = num.getAndIncrement();
+            assertEquals(i, orderMap.get(id).intValue());
+        };
+
+        InstanceIdentifier.extractHostnames(abc0123).flatMap(s -> 
Stream.of(new InstanceIdentifier(s, 1), new InstanceIdentifier(s, 
2))).forEach(action);
+        InstanceIdentifier.extractHostnames(abc123).map(s -> new 
InstanceIdentifier(s, 1)).forEach(action);
+        InstanceIdentifier.extractHostnames(b).map(s -> new 
InstanceIdentifier(s, 1)).forEach(action);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testCreateIdentifiersCharactersAfterNumber() {
+        InstanceIdentifier.createIdentifiers(Stream.of("test(2)a")).count();
+    }
+
+    private Stream<Integer> integerRange(int start, int endInclusive) {
+        return IntStream.range(start, endInclusive + 1).mapToObj(value -> 
value);
+    }
+
+    private void testExtractHostnames(String hostnameWithRange, String... 
expectedHostnames) {
+        
assertEquals(Stream.of(expectedHostnames).collect(Collectors.toList()), 
InstanceIdentifier.extractHostnames(hostnameWithRange).collect(Collectors.toList()));
+    }
+
+    private void testCreateIdentifiers(List<String> hostnameExpressions, 
List<String> expectedHostnames, List<Integer> expectedNumbers) {
+        List<InstanceIdentifier> instanceIdentifiers = 
InstanceIdentifier.createIdentifiers(hostnameExpressions.stream()).collect(Collectors.toList());
+        assertEquals(instanceIdentifiers.size(), expectedHostnames.size());
+        for (int i = 0; i < instanceIdentifiers.size(); i++) {
+            InstanceIdentifier identifier = instanceIdentifiers.get(i);
+            assertEquals(expectedHostnames.get(i), identifier.getHostname());
+            assertEquals((int) expectedNumbers.get(i), identifier.getNumber());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/2fd39676/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/manager/writer/NifiPropertiesTlsClientConfigWriterTest.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/manager/writer/NifiPropertiesTlsClientConfigWriterTest.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/manager/writer/NifiPropertiesTlsClientConfigWriterTest.java
new file mode 100644
index 0000000..c96d906
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/manager/writer/NifiPropertiesTlsClientConfigWriterTest.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.toolkit.tls.manager.writer;
+
+import org.apache.nifi.toolkit.tls.configuration.TlsClientConfig;
+import org.apache.nifi.toolkit.tls.configuration.TlsConfig;
+import org.apache.nifi.toolkit.tls.properties.NiFiPropertiesWriter;
+import org.apache.nifi.toolkit.tls.properties.NiFiPropertiesWriterFactory;
+import org.apache.nifi.toolkit.tls.util.OutputStreamFactory;
+import org.apache.nifi.util.NiFiProperties;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class NifiPropertiesTlsClientConfigWriterTest {
+    @Mock
+    NiFiPropertiesWriterFactory niFiPropertiesWriterFactory;
+    @Mock
+    OutputStreamFactory outputStreamFactory;
+    private NiFiPropertiesWriter niFiPropertiesWriter;
+    private int hostNum;
+    private String testHostname;
+    private File outputFile;
+    private NifiPropertiesTlsClientConfigWriter 
nifiPropertiesTlsClientConfigWriter;
+    private TlsClientConfig tlsClientConfig;
+    private ByteArrayOutputStream outputStream;
+    private String keyStore;
+    private String keyStorePassword;
+    private String trustStore;
+    private String trustStorePassword;
+    private Properties overlayProperties;
+    private String keyPassword;
+    private String keyStoreType;
+    private String trustStoreType;
+
+    @Before
+    public void setup() throws IOException {
+        testHostname = "testHostname";
+        hostNum = 22;
+
+        keyStore = "testKeyStore.jks";
+        keyStoreType = TlsConfig.DEFAULT_KEY_STORE_TYPE;
+        keyStorePassword = "badKeyStorePassword";
+        keyPassword = "badKeyPassword";
+
+        trustStore = "testTrustStore.jks";
+        trustStoreType = TlsConfig.DEFAULT_KEY_STORE_TYPE;
+        trustStorePassword = "badTrustStorePassword";
+
+        outputFile = File.createTempFile("temp", "nifi");
+        outputStream = new ByteArrayOutputStream();
+        when(outputStreamFactory.create(outputFile)).thenReturn(outputStream);
+
+        tlsClientConfig = new TlsClientConfig();
+        tlsClientConfig.setKeyStore(keyStore);
+        tlsClientConfig.setKeyStoreType(keyStoreType);
+        tlsClientConfig.setKeyStorePassword(keyStorePassword);
+        tlsClientConfig.setKeyPassword(keyPassword);
+
+        tlsClientConfig.setTrustStore(trustStore);
+        tlsClientConfig.setTrustStoreType(trustStoreType);
+        tlsClientConfig.setTrustStorePassword(trustStorePassword);
+
+        niFiPropertiesWriter = new NiFiPropertiesWriter(new ArrayList<>());
+        
when(niFiPropertiesWriterFactory.create()).thenReturn(niFiPropertiesWriter);
+        nifiPropertiesTlsClientConfigWriter = new 
NifiPropertiesTlsClientConfigWriter(niFiPropertiesWriterFactory, 
outputStreamFactory, outputFile, testHostname, hostNum);
+        overlayProperties = 
nifiPropertiesTlsClientConfigWriter.getOverlayProperties();
+    }
+
+    @Test
+    public void testDefaults() throws IOException {
+        nifiPropertiesTlsClientConfigWriter.write(tlsClientConfig);
+        testHostnamesAndPorts();
+        assertNotEquals(0, 
nifiPropertiesTlsClientConfigWriter.getPropertyPortMap().size());
+    }
+
+    @Test(expected = NumberFormatException.class)
+    public void testBadPortNum() throws IOException {
+        
nifiPropertiesTlsClientConfigWriter.getOverlayProperties().setProperty(nifiPropertiesTlsClientConfigWriter.getPropertyPortMap().keySet().iterator().next(),
 "notAnInt");
+        nifiPropertiesTlsClientConfigWriter.write(tlsClientConfig);
+    }
+
+    @Test
+    public void testNoHostnameProperties() throws IOException {
+        
nifiPropertiesTlsClientConfigWriter.getOverlayProperties().setProperty(NifiPropertiesTlsClientConfigWriter.HOSTNAME_PROPERTIES,
 "");
+        nifiPropertiesTlsClientConfigWriter.write(tlsClientConfig);
+        testHostnamesAndPorts();
+        Properties nifiProperties = getNifiProperties();
+        nifiProperties.stringPropertyNames().forEach(s -> 
assertNotEquals(testHostname, nifiProperties.getProperty(s)));
+    }
+
+    private void testHostnamesAndPorts() {
+        Properties nifiProperties = getNifiProperties();
+
+        assertEquals(NifiPropertiesTlsClientConfigWriter.CONF + keyStore, 
nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE));
+        assertEquals(keyStoreType, 
nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE));
+        assertEquals(keyStorePassword, 
nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD));
+        assertEquals(keyPassword, 
nifiProperties.getProperty(NiFiProperties.SECURITY_KEY_PASSWD));
+
+        assertEquals(NifiPropertiesTlsClientConfigWriter.CONF + trustStore, 
nifiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE));
+        assertEquals(trustStoreType, 
nifiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE));
+        assertEquals(trustStorePassword, 
nifiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD));
+
+        assertEquals("", 
nifiProperties.getProperty(NiFiProperties.WEB_HTTP_HOST));
+        assertEquals("", 
nifiProperties.getProperty(NiFiProperties.WEB_HTTP_PORT));
+        assertEquals(Boolean.toString(true), 
nifiProperties.getProperty(NiFiProperties.SITE_TO_SITE_SECURE));
+
+        
nifiPropertiesTlsClientConfigWriter.getHostnamePropertyStream().forEach(s -> 
assertEquals(testHostname, nifiProperties.getProperty(s)));
+        
nifiPropertiesTlsClientConfigWriter.getPropertyPortMap().entrySet().forEach(propertyToPortEntry
 -> {
+            assertEquals(Integer.toString(propertyToPortEntry.getValue()), 
nifiProperties.getProperty(propertyToPortEntry.getKey()));
+            
assertEquals(Integer.parseInt(overlayProperties.getProperty(propertyToPortEntry.getKey()))
 + hostNum - 1, propertyToPortEntry.getValue().intValue());
+        });
+    }
+
+    private Properties getNifiProperties() {
+        Properties properties = new Properties();
+        try {
+            properties.load(new 
ByteArrayInputStream(outputStream.toByteArray()));
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        return properties;
+    }
+}

Reply via email to