# ignite-149: WIP.

Project: http://git-wip-us.apache.org/repos/asf/incubator-ignite/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-ignite/commit/67376020
Tree: http://git-wip-us.apache.org/repos/asf/incubator-ignite/tree/67376020
Diff: http://git-wip-us.apache.org/repos/asf/incubator-ignite/diff/67376020

Branch: refs/heads/ignite-188
Commit: 673760208f800118608cf372ff3052fcd9ecb14b
Parents: bb8b07d
Author: vozerov-gridgain <voze...@gridgain.com>
Authored: Thu Feb 5 17:49:45 2015 +0300
Committer: vozerov-gridgain <voze...@gridgain.com>
Committed: Thu Feb 5 17:49:45 2015 +0300

----------------------------------------------------------------------
 bin/include/IgniteService.exe                   |  Bin 0 -> 6144 bytes
 bin/include/ggservice.exe                       |  Bin 5632 -> 0 bytes
 .../java/org/apache/ignite/IgniteCluster.java   |    6 +-
 .../apache/ignite/internal/IgniteKernal.java    |   16 +-
 .../ignite/internal/util/IgniteUtils.java       |    2 +-
 .../util/nodestart/GridNodeCallable.java        |   29 -
 .../util/nodestart/GridNodeStartUtils.java      |  390 -------
 .../nodestart/GridRemoteStartSpecification.java |  279 -----
 .../util/nodestart/IgniteNodeCallable.java      |   30 +
 .../util/nodestart/IgniteNodeStartUtils.java    |  391 +++++++
 .../IgniteRemoteStartSpecification.java         |  279 +++++
 .../util/nodestart/IgniteSshProcessor.java      |    2 +-
 .../core/src/test/bin/start-nodes-custom.bat    |    2 +-
 modules/core/src/test/config/start-nodes.ini    |    6 +-
 .../util/nodestart/GridNodeCallableImpl.java    |  344 ------
 .../util/nodestart/IgniteNodeCallableImpl.java  |  344 ++++++
 .../util/nodestart/IgniteSshProcessorImpl.java  |    4 +-
 .../internal/GridNodeStartUtilsSelfTest.java    |   89 --
 .../GridProjectionStartStopRestartSelfTest.java | 1032 ------------------
 .../internal/IgniteNodeStartUtilsSelfTest.java  |   88 ++
 ...gniteProjectionStartStopRestartSelfTest.java | 1032 ++++++++++++++++++
 .../IgniteStartStopRestartTestSuite.java        |    4 +-
 .../commands/start/VisorStartCommand.scala      |    4 +-
 modules/winservice/.gitignore                   |    2 +
 modules/winservice/IgniteService.sln            |   22 +
 .../winservice/IgniteService/IgniteService.cs   |  170 +++
 .../IgniteService/IgniteService.csproj          |   90 ++
 27 files changed, 2471 insertions(+), 2186 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/bin/include/IgniteService.exe
----------------------------------------------------------------------
diff --git a/bin/include/IgniteService.exe b/bin/include/IgniteService.exe
new file mode 100644
index 0000000..66deb9c
Binary files /dev/null and b/bin/include/IgniteService.exe differ

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/bin/include/ggservice.exe
----------------------------------------------------------------------
diff --git a/bin/include/ggservice.exe b/bin/include/ggservice.exe
deleted file mode 100644
index 7d3783b..0000000
Binary files a/bin/include/ggservice.exe and /dev/null differ

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/core/src/main/java/org/apache/ignite/IgniteCluster.java
----------------------------------------------------------------------
diff --git a/modules/core/src/main/java/org/apache/ignite/IgniteCluster.java 
b/modules/core/src/main/java/org/apache/ignite/IgniteCluster.java
index d8f16ed..0350487 100644
--- a/modules/core/src/main/java/org/apache/ignite/IgniteCluster.java
+++ b/modules/core/src/main/java/org/apache/ignite/IgniteCluster.java
@@ -232,7 +232,7 @@ public interface IgniteCluster extends ClusterGroup, 
IgniteAsyncSupport {
      *             </td>
      *         </tr>
      *         <tr>
-     *             <td><b>ggHome</b></td>
+     *             <td><b>igniteHome</b></td>
      *             <td>String</td>
      *             <td>
      *                 Path to GridGain installation folder. If not defined, 
IGNITE_HOME
@@ -242,13 +242,13 @@ public interface IgniteCluster extends ClusterGroup, 
IgniteAsyncSupport {
      *         <tr>
      *             <td><b>cfg</b></td>
      *             <td>String</td>
-     *             <td>Path to configuration file (relative to {@code 
ggHome}).</td>
+     *             <td>Path to configuration file (relative to {@code 
igniteHome}).</td>
      *         </tr>
      *         <tr>
      *             <td><b>script</b></td>
      *             <td>String</td>
      *             <td>
-     *                 Custom startup script file name and path (relative to 
{@code ggHome}).
+     *                 Custom startup script file name and path (relative to 
{@code igniteHome}).
      *                 You can also specify a space-separated list of 
parameters in the same
      *                 string (for example: {@code "bin/my-custom-script.sh 
-v"}).
      *             </td>

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java
----------------------------------------------------------------------
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java 
b/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java
index 2b0d014..5dc4bc8 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java
@@ -98,7 +98,7 @@ import static org.apache.ignite.internal.GridNodeAttributes.*;
 import static org.apache.ignite.internal.GridProductImpl.*;
 import static org.apache.ignite.internal.IgniteComponentType.*;
 import static 
org.apache.ignite.internal.processors.license.GridLicenseSubsystem.*;
-import static org.apache.ignite.internal.util.nodestart.GridNodeStartUtils.*;
+import static org.apache.ignite.internal.util.nodestart.IgniteNodeStartUtils.*;
 import static org.apache.ignite.lifecycle.LifecycleEventType.*;
 
 /**
@@ -2713,9 +2713,9 @@ public class IgniteKernal extends ClusterGroupAdapter 
implements IgniteEx, Ignit
         try {
             IgniteSshProcessor sshProcessor = 
IgniteComponentType.SSH.create(false);
 
-            Map<String, Collection<GridRemoteStartSpecification>> specsMap = 
specifications(hosts, dflts);
+            Map<String, Collection<IgniteRemoteStartSpecification>> specsMap = 
specifications(hosts, dflts);
 
-            Map<String, ConcurrentLinkedQueue<GridNodeCallable>> runMap = new 
HashMap<>();
+            Map<String, ConcurrentLinkedQueue<IgniteNodeCallable>> runMap = 
new HashMap<>();
 
             int nodeCallCnt = 0;
 
@@ -2760,11 +2760,11 @@ public class IgniteKernal extends ClusterGroupAdapter 
implements IgniteEx, Ignit
                         startIdx = neighbors.size() + 1;
                 }
 
-                ConcurrentLinkedQueue<GridNodeCallable> nodeRuns = new 
ConcurrentLinkedQueue<>();
+                ConcurrentLinkedQueue<IgniteNodeCallable> nodeRuns = new 
ConcurrentLinkedQueue<>();
 
                 runMap.put(host, nodeRuns);
 
-                for (GridRemoteStartSpecification spec : specsMap.get(host)) {
+                for (IgniteRemoteStartSpecification spec : specsMap.get(host)) 
{
                     assert spec.host().equals(host);
 
                     for (int i = startIdx; i <= spec.nodes(); i++) {
@@ -2790,7 +2790,7 @@ public class IgniteKernal extends ClusterGroupAdapter 
implements IgniteEx, Ignit
             AtomicInteger cnt = new AtomicInteger(nodeCallCnt);
 
             // Limit maximum simultaneous connection number per host.
-            for (ConcurrentLinkedQueue<GridNodeCallable> queue : 
runMap.values()) {
+            for (ConcurrentLinkedQueue<IgniteNodeCallable> queue : 
runMap.values()) {
                 for (int i = 0; i < maxConn; i++) {
                     if (!runNextNodeCallable(queue, fut, cnt))
                         break;
@@ -2839,10 +2839,10 @@ public class IgniteKernal extends ClusterGroupAdapter 
implements IgniteEx, Ignit
      * @param cnt Atomic counter to check if all futures are added to compound 
future.
      * @return {@code True} if task was started, {@code false} if queue was 
empty.
      */
-    private boolean runNextNodeCallable(final 
ConcurrentLinkedQueue<GridNodeCallable> queue,
+    private boolean runNextNodeCallable(final 
ConcurrentLinkedQueue<IgniteNodeCallable> queue,
         final GridCompoundFuture<GridTuple3<String, Boolean, String>,
         Collection<GridTuple3<String, Boolean, String>>> comp, final 
AtomicInteger cnt) {
-        GridNodeCallable call = queue.poll();
+        IgniteNodeCallable call = queue.poll();
 
         if (call == null)
             return false;

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
----------------------------------------------------------------------
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java 
b/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
index e322979..c9577d4 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
@@ -2533,7 +2533,7 @@ public abstract class IgniteUtils {
 
         if (ggHome0 != null && !ggHome0.equals(path))
             throw new IgniteException("Failed to set IGNITE_HOME after it has 
been already resolved " +
-                "[ggHome=" + ggHome0 + ", newGgHome=" + path + ']');
+                "[igniteHome=" + ggHome0 + ", newIgniteHome=" + path + ']');
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/GridNodeCallable.java
----------------------------------------------------------------------
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/GridNodeCallable.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/GridNodeCallable.java
deleted file mode 100644
index a76c861..0000000
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/GridNodeCallable.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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.ignite.internal.util.nodestart;
-
-import org.apache.ignite.internal.util.lang.*;
-
-import java.util.concurrent.*;
-
-/**
- * SSH-based node starter, returns tuple which contains hostname, success flag 
and error message
- * if attempt was not successful.
- */
-public interface GridNodeCallable extends Callable<GridTuple3<String, Boolean, 
String>> {
-}

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/GridNodeStartUtils.java
----------------------------------------------------------------------
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/GridNodeStartUtils.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/GridNodeStartUtils.java
deleted file mode 100644
index 44306ab..0000000
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/GridNodeStartUtils.java
+++ /dev/null
@@ -1,390 +0,0 @@
-/*
- * 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.ignite.internal.util.nodestart;
-
-import org.apache.ignite.*;
-import org.apache.ignite.internal.util.typedef.*;
-import org.apache.ignite.internal.util.typedef.internal.*;
-import org.apache.ignite.lang.*;
-import org.jetbrains.annotations.*;
-
-import java.io.*;
-import java.util.*;
-import java.util.concurrent.*;
-
-/**
- * Util methods for {@code GridProjection.startNodes(..)} methods.
- */
-public class GridNodeStartUtils {
-    /** Key for hostname. */
-    public static final String HOST = "host";
-
-    /** Key for port number. */
-    public static final String PORT = "port";
-
-    /** Key for username. */
-    public static final String UNAME = "uname";
-
-    /** Key for password. */
-    public static final String PASSWD = "passwd";
-
-    /** Key for private key file. */
-    public static final String KEY = "key";
-
-    /** Key for number of nodes. */
-    public static final String NODES = "nodes";
-
-    /** Key for GridGain home folder. */
-    public static final String IGNITE_HOME = "ggHome";
-
-    /** Key for configuration path. */
-    public static final String CFG = "cfg";
-
-    /** Key for script path. */
-    public static final String SCRIPT = "script";
-
-    /** Key for logger. */
-    public static final String LOGGER = "logger";
-
-    /** Default connection timeout. */
-    public static final int DFLT_TIMEOUT = 10000;
-
-    /** Default maximum number of parallel connections. */
-    public static final int DFLT_MAX_CONN = 5;
-
-    /** Symbol that specifies range of IPs. */
-    private static final String RANGE_SMB = "~";
-
-    /** Default port. */
-    private static final int DFLT_PORT = 22;
-
-    /** Default number of nodes. */
-    private static final int DFLT_NODES = 1;
-
-    /** Default configuration path. */
-    private static final String DFLT_CFG = "";
-
-    /** Defaults section name. */
-    private static final String DFLT_SECTION = "defaults";
-
-    /**
-     * Ensure singleton.
-     */
-    private GridNodeStartUtils() {
-        // No-op.
-    }
-
-    /**
-     * Parses INI file.
-     *
-     * @param file File.
-     * @return Tuple with host maps and default values.
-     * @throws IgniteCheckedException In case of error.
-     */
-    public static IgniteBiTuple<Collection<Map<String, Object>>, Map<String, 
Object>> parseFile(
-        File file) throws IgniteCheckedException {
-        assert file != null;
-        assert file.exists();
-        assert file.isFile();
-
-        BufferedReader br = null;
-
-        int lineCnt = 1;
-
-        try {
-            br = new BufferedReader(new InputStreamReader(new 
FileInputStream(file), "UTF-8"));
-
-            String section = null;
-
-            Collection<Map<String, Object>> hosts = new LinkedList<>();
-            Map<String, Object> dflts = null;
-            Map<String, Object> props = null;
-
-            for (String line; (line = br.readLine()) != null; lineCnt++) {
-                String l = line.trim();
-
-                if (l.isEmpty() || l.startsWith("#") || l.startsWith(";"))
-                    continue;
-
-                if (l.startsWith("[") && l.endsWith("]")) {
-                    Map<String, Object> dfltsTmp = processSection(section, 
hosts, dflts, props);
-
-                    if (dfltsTmp != null)
-                        dflts = dfltsTmp;
-
-                    props = new HashMap<>();
-
-                    section = l.substring(1, l.length() - 1);
-                }
-                else if (l.contains("=")) {
-                    if (section == null)
-                        throw new IgniteCheckedException("GridGain ini format 
doesn't support unnamed section.");
-
-                    String key = l.substring(0, l.indexOf('='));
-                    String val = line.substring(line.indexOf('=') + 1);
-
-                    switch (key) {
-                        case HOST:
-                        case UNAME:
-                        case PASSWD:
-                        case IGNITE_HOME:
-                        case CFG:
-                        case SCRIPT:
-                            props.put(key, val);
-                            break;
-
-                        case PORT:
-                        case NODES:
-                            props.put(key, Integer.valueOf(val));
-                            break;
-
-                        case KEY:
-                            props.put(KEY, new File(val));
-                            break;
-                    }
-                }
-                else
-                    throw new IgniteCheckedException("Failed to parse INI file 
(line " + lineCnt + ").");
-            }
-
-            Map<String, Object> dfltsTmp = processSection(section, hosts, 
dflts, props);
-
-            if (dfltsTmp != null)
-                dflts = dfltsTmp;
-
-            return F.t(hosts, dflts);
-        }
-        catch (IOException | NumberFormatException e) {
-            throw new IgniteCheckedException("Failed to parse INI file (line " 
+ lineCnt + ").", e);
-        }
-        finally {
-            U.closeQuiet(br);
-        }
-    }
-
-    /**
-     * Processes section of parsed INI file.
-     *
-     * @param section Name of the section.
-     * @param hosts Already parsed properties for sections excluding default.
-     * @param dflts Parsed properties for default section.
-     * @param props Current properties.
-     * @return Default properties if specified section is default, {@code 
null} otherwise.
-     * @throws IgniteCheckedException If INI file contains several default 
sections.
-     */
-    private static Map<String, Object> processSection(String section, 
Collection<Map<String, Object>> hosts,
-        Map<String, Object> dflts, Map<String, Object> props) throws 
IgniteCheckedException {
-        if (section == null || props == null)
-            return null;
-
-        if (DFLT_SECTION.equalsIgnoreCase(section)) {
-            if (dflts != null)
-                throw new IgniteCheckedException("Only one '" + DFLT_SECTION + 
"' section is allowed.");
-
-            return props;
-        }
-        else {
-            hosts.add(props);
-
-            return null;
-        }
-    }
-
-    /**
-     * Makes specifications.
-     *
-     * @param hosts Host configurations.
-     * @param dflts Default values.
-     * @return Specification grouped by hosts.
-     * @throws IgniteCheckedException In case of error.
-     */
-    public static Map<String, Collection<GridRemoteStartSpecification>> 
specifications(
-        Collection<Map<String, Object>> hosts, @Nullable Map<String, Object> 
dflts)
-        throws IgniteCheckedException {
-        Map<String, Collection<GridRemoteStartSpecification>> specsMap = 
U.newHashMap(hosts.size());
-
-        GridRemoteStartSpecification dfltSpec = processDefaults(dflts);
-
-        for (Map<String, Object> host : hosts) {
-            Collection<GridRemoteStartSpecification> specs = processHost(host, 
dfltSpec);
-
-            for (GridRemoteStartSpecification spec : specs)
-                F.addIfAbsent(specsMap, spec.host(), new 
Callable<Collection<GridRemoteStartSpecification>>() {
-                    @Override public Collection<GridRemoteStartSpecification> 
call() throws Exception {
-                        return new HashSet<>();
-                    }
-                }).add(spec);
-        }
-
-        return specsMap;
-    }
-
-    /**
-     * Converts properties map to default specification.
-     *
-     * @param dflts Properties.
-     * @return Specification.
-     * @throws IgniteCheckedException If properties are invalid.
-     */
-    private static GridRemoteStartSpecification processDefaults(@Nullable 
Map<String, Object> dflts)
-        throws IgniteCheckedException {
-        int port = DFLT_PORT;
-        String uname = System.getProperty("user.name");
-        String passwd = null;
-        File key = null;
-        int nodes = DFLT_NODES;
-        String ggHome = null;
-        String cfg = DFLT_CFG;
-        String script = null;
-        IgniteLogger log = null;
-
-        if (dflts != null) {
-            if (dflts.get(PORT) != null)
-                port = (Integer)dflts.get(PORT);
-
-            if (dflts.get(UNAME) != null)
-                uname = (String)dflts.get(UNAME);
-
-            if (dflts.get(PASSWD) != null)
-                passwd = (String)dflts.get(PASSWD);
-
-            if (dflts.get(KEY) != null)
-                key = (File)dflts.get(KEY);
-
-            if (dflts.get(NODES) != null)
-                nodes = (Integer)dflts.get(NODES);
-
-            if (dflts.get(IGNITE_HOME) != null)
-                ggHome = (String)dflts.get(IGNITE_HOME);
-
-            if (dflts.get(CFG) != null)
-                cfg = (String)dflts.get(CFG);
-
-            if (dflts.get(SCRIPT) != null)
-                script = (String)dflts.get(SCRIPT);
-
-            if (dflts.get(LOGGER) != null)
-                log = (IgniteLogger)dflts.get(LOGGER);
-        }
-
-        if (port <= 0)
-            throw new IgniteCheckedException("Invalid port number: " + port);
-
-        if (nodes <= 0)
-            throw new IgniteCheckedException("Invalid number of nodes: " + 
nodes);
-
-        return new GridRemoteStartSpecification(null, port, uname, passwd,
-            key, nodes, ggHome, cfg, script, log);
-    }
-
-    /**
-     * Converts properties map to specification.
-     *
-     * @param props Properties.
-     * @param dfltSpec Default specification.
-     * @return Specification.
-     * @throws IgniteCheckedException If properties are invalid.
-     */
-    private static Collection<GridRemoteStartSpecification> 
processHost(Map<String, Object> props,
-        GridRemoteStartSpecification dfltSpec) throws IgniteCheckedException {
-        assert props != null;
-        assert dfltSpec != null;
-
-        if (props.get(HOST) == null)
-            throw new IgniteCheckedException("Host must be specified.");
-
-        Set<String> hosts = expandHost((String)props.get(HOST));
-        int port = props.get(PORT) != null ? (Integer)props.get(PORT) : 
dfltSpec.port();
-        String uname = props.get(UNAME) != null ? (String)props.get(UNAME) : 
dfltSpec.username();
-        String passwd = props.get(PASSWD) != null ? (String)props.get(PASSWD) 
: dfltSpec.password();
-        File key = props.get(KEY) != null ? (File)props.get(KEY) : 
dfltSpec.key();
-        int nodes = props.get(NODES) != null ? (Integer)props.get(NODES) : 
dfltSpec.nodes();
-        String ggHome = props.get(IGNITE_HOME) != null ? 
(String)props.get(IGNITE_HOME) : dfltSpec.ggHome();
-        String cfg = props.get(CFG) != null ? (String)props.get(CFG) : 
dfltSpec.configuration();
-        String script = props.get(SCRIPT) != null ? (String)props.get(SCRIPT) 
: dfltSpec.script();
-
-        if (port<= 0)
-            throw new IgniteCheckedException("Invalid port number: " + port);
-
-        if (nodes <= 0)
-            throw new IgniteCheckedException("Invalid number of nodes: " + 
nodes);
-
-        if (passwd == null && key == null)
-            throw new IgniteCheckedException("Password or private key file 
must be specified.");
-
-        if (passwd != null && key != null)
-            passwd = null;
-
-        Collection<GridRemoteStartSpecification> specs =
-            new ArrayList<>(hosts.size());
-
-        for (String host : hosts)
-            specs.add(new GridRemoteStartSpecification(host, port, uname, 
passwd,
-                key, nodes, ggHome, cfg, script, dfltSpec.logger()));
-
-        return specs;
-    }
-
-    /**
-     * Parses and expands range of IPs, if needed. Host names without the range
-     * returned as is.
-     *
-     * @param addr Host with or without `~` range.
-     * @return Set of individual host names (IPs).
-     * @throws IgniteCheckedException In case of error.
-     */
-    public static Set<String> expandHost(String addr) throws 
IgniteCheckedException {
-        assert addr != null;
-
-        Set<String> addrs = new HashSet<>();
-
-        if (addr.contains(RANGE_SMB)) {
-            String[] parts = addr.split(RANGE_SMB);
-
-            if (parts.length != 2)
-                throw new IgniteCheckedException("Invalid IP range: " + addr);
-
-            int lastDot = parts[0].lastIndexOf('.');
-
-            if (lastDot < 0)
-                throw new IgniteCheckedException("Invalid IP range: " + addr);
-
-            String base = parts[0].substring(0, lastDot);
-            String begin = parts[0].substring(lastDot + 1);
-            String end = parts[1];
-
-            try {
-                int a = Integer.valueOf(begin);
-                int b = Integer.valueOf(end);
-
-                if (a > b)
-                    throw new IgniteCheckedException("Invalid IP range: " + 
addr);
-
-                for (int i = a; i <= b; i++)
-                    addrs.add(base + "." + i);
-            }
-            catch (NumberFormatException e) {
-                throw new IgniteCheckedException("Invalid IP range: " + addr, 
e);
-            }
-        }
-        else
-            addrs.add(addr);
-
-        return addrs;
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/GridRemoteStartSpecification.java
----------------------------------------------------------------------
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/GridRemoteStartSpecification.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/GridRemoteStartSpecification.java
deleted file mode 100644
index f0d7862..0000000
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/GridRemoteStartSpecification.java
+++ /dev/null
@@ -1,279 +0,0 @@
-/*
- * 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.ignite.internal.util.nodestart;
-
-import org.apache.ignite.*;
-import org.apache.ignite.internal.util.typedef.*;
-import org.jetbrains.annotations.*;
-
-import java.io.*;
-
-/**
- * Host data.
- */
-public class GridRemoteStartSpecification {
-    /** Hostname. */
-    private final String host;
-
-    /** Port number. */
-    private final int port;
-
-    /** Username. */
-    private final String uname;
-
-    /** Password. */
-    private final String passwd;
-
-    /** Private key file. */
-    private final File key;
-
-    /** Private key filename. */
-    private final String keyName;
-
-    /** Number of nodes to start. */
-    private final int nodes;
-
-    /** GridGain installation folder. */
-    private String ggHome;
-
-    /** Configuration path. */
-    private String cfg;
-
-    /** Configuration filename. */
-    private String cfgName;
-
-    /** Script path. */
-    private String script;
-
-    /** Custom logger. */
-    private IgniteLogger logger;
-
-    /** Valid flag */
-    private boolean valid;
-
-    /**
-     * @param host Hostname.
-     * @param port Port number.
-     * @param uname Username.
-     * @param passwd Password (can be {@code null} if private key 
authentication is used).
-     * @param key Private key file path.
-     * @param nodes Number of nodes to start.
-     * @param ggHome GridGain installation folder.
-     * @param cfg Configuration path.
-     * @param script Script path.
-     */
-    public GridRemoteStartSpecification(@Nullable String host, int port, 
@Nullable String uname,
-        @Nullable String passwd, @Nullable File key, int nodes, @Nullable 
String ggHome,
-        @Nullable String cfg, @Nullable String script) {
-        this(host, port, uname, passwd, key, nodes, ggHome, cfg, script, null);
-    }
-
-    /**
-     * @param host Hostname.
-     * @param port Port number.
-     * @param uname Username.
-     * @param passwd Password (can be {@code null} if private key 
authentication is used).
-     * @param key Private key file path.
-     * @param nodes Number of nodes to start.
-     * @param ggHome GridGain installation folder.
-     * @param cfg Configuration path.
-     * @param script Script path.
-     * @param logger Custom logger.
-     */
-    public GridRemoteStartSpecification(@Nullable String host, int port, 
@Nullable String uname,
-        @Nullable String passwd, @Nullable File key, int nodes, @Nullable 
String ggHome,
-        @Nullable String cfg, @Nullable String script, @Nullable IgniteLogger 
logger) {
-        assert port > 0;
-        assert nodes > 0;
-
-        this.host = !F.isEmpty(host) ? host : null;
-        this.port = port;
-        this.uname = !F.isEmpty(uname) ? uname : null;
-        this.passwd = !F.isEmpty(passwd) ? passwd : null;
-        this.key = key;
-        this.nodes = nodes;
-        this.ggHome = !F.isEmpty(ggHome) ? ggHome : null;
-        this.cfg = !F.isEmpty(cfg) ? cfg : null;
-        cfgName = cfg == null ? null : shorten(cfg);
-        keyName = key == null ? "" : shorten(key.getAbsolutePath());
-        this.script = !F.isEmpty(script) ? script : null;
-        this.logger = logger;
-    }
-
-    /** {@inheritDoc} */
-    @Override public boolean equals(Object o) {
-        if (this == o) return true;
-
-        if (!(o instanceof GridRemoteStartSpecification)) return false;
-
-        GridRemoteStartSpecification that = (GridRemoteStartSpecification)o;
-
-        return (host == null ? that.host == null : host.equals(that.host)) &&
-            (uname == null ? that.uname == null : uname.equals(that.uname)) &&
-            (passwd == null ? that.passwd == null : 
passwd.equals(that.passwd)) &&
-            (key == null ? that.key == null : key.equals(that.key)) &&
-            (ggHome == null ? that.ggHome == null : 
ggHome.equals(that.ggHome)) &&
-            (cfg == null ? that.cfg == null : cfg.equals(that.cfg)) &&
-            (script == null ? that.script == null : 
script.equals(that.script)) &&
-            port == that.port && nodes == that.nodes;
-    }
-
-    /** {@inheritDoc} */
-    @Override public int hashCode() {
-        int res = host == null ? 0 : host.hashCode();
-
-        res = 31 * res + (uname == null ? 0 : uname.hashCode());
-        res = 31 * res + (passwd == null ? 0 : passwd.hashCode());
-        res = 31 * res + (key == null ? 0 : key.hashCode());
-        res = 31 * res + (ggHome == null ? 0 : ggHome.hashCode());
-        res = 31 * res + (cfg == null ? 0 : cfg.hashCode());
-        res = 31 * res + (script == null ? 0 : script.hashCode());
-        res = 31 * res + port;
-        res = 31 * res + nodes;
-
-        return res;
-    }
-
-    /**
-     * Get filename from path.
-     *
-     * @param path Path.
-     * @return Filename.
-     */
-    private static String shorten(String path) {
-        int idx1 = path.lastIndexOf('/');
-        int idx2 = path.lastIndexOf('\\');
-        int idx = Math.max(idx1, idx2);
-
-        return idx == -1 ? path : path.substring(idx + 1);
-    }
-
-    /**
-     * @return Hostname.
-     */
-    public String host() {
-        return host;
-    }
-
-    /**
-     * @return Port number.
-     */
-    public int port() {
-        return port;
-    }
-
-    /**
-     * @return Username.
-     */
-    public String username() {
-        return uname;
-    }
-
-    /**
-     * @return Password.
-     */
-    public String password() {
-        return passwd;
-    }
-
-    /**
-     * @return Private key file path.
-     */
-    public File key() {
-        return key;
-    }
-
-    /**
-     * @return Private key file name.
-     */
-    public String keyName() {
-        return keyName;
-    }
-
-    /**
-     * @return Number of nodes to start.
-     */
-    public int nodes() {
-        return nodes;
-    }
-
-    /**
-     * @return GridGain installation folder.
-     */
-    public String ggHome() {
-        return ggHome;
-    }
-
-    /**
-     * @return Configuration full path.
-     */
-    public String configuration() {
-        return cfg;
-    }
-
-    /**
-     * @return Configuration path short version - just file name.
-     */
-    public String configurationName() {
-        return cfgName;
-    }
-
-    /**
-     * @return Script path.
-     */
-    public String script() {
-        return script;
-    }
-
-    /**
-     * @return Custom logger.
-     */
-    public IgniteLogger logger() {
-        return logger;
-    }
-
-    /**
-     * @return Valid flag.
-     */
-    public boolean valid() {
-        return valid;
-    }
-
-    /**
-     * @param valid Valid flag.
-     */
-    public void valid(boolean valid) {
-        this.valid = valid;
-    }
-
-    /**
-     * Sets correct separator in paths.
-     *
-     * @param separator Separator.
-     */
-    public void fixPaths(char separator) {
-        if (ggHome != null)
-            ggHome = ggHome.replace('\\', separator).replace('/', separator);
-
-        if (script != null)
-            script = script.replace('\\', separator).replace('/', separator);
-
-        if (cfg != null)
-            cfg = cfg.replace('\\', separator).replace('/', separator);
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteNodeCallable.java
----------------------------------------------------------------------
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteNodeCallable.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteNodeCallable.java
new file mode 100644
index 0000000..6c34efb
--- /dev/null
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteNodeCallable.java
@@ -0,0 +1,30 @@
+/*
+ * 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.ignite.internal.util.nodestart;
+
+import org.apache.ignite.internal.util.lang.*;
+
+import java.util.concurrent.*;
+
+/**
+ * SSH-based node starter, returns tuple which contains hostname, success flag 
and error message
+ * if attempt was not successful.
+ */
+public interface IgniteNodeCallable extends Callable<GridTuple3<String, 
Boolean, String>> {
+    // No-op.
+}

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteNodeStartUtils.java
----------------------------------------------------------------------
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteNodeStartUtils.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteNodeStartUtils.java
new file mode 100644
index 0000000..c91ca21
--- /dev/null
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteNodeStartUtils.java
@@ -0,0 +1,391 @@
+/*
+ * 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.ignite.internal.util.nodestart;
+
+import org.apache.ignite.*;
+import org.apache.ignite.internal.util.typedef.*;
+import org.apache.ignite.internal.util.typedef.internal.*;
+import org.apache.ignite.lang.*;
+import org.jetbrains.annotations.*;
+
+import java.io.*;
+import java.util.*;
+import java.util.concurrent.*;
+
+/**
+ * Util methods for {@code IgniteCluster.startNodes(..)} methods.
+ */
+public class IgniteNodeStartUtils {
+    /** Key for hostname. */
+    public static final String HOST = "host";
+
+    /** Key for port number. */
+    public static final String PORT = "port";
+
+    /** Key for username. */
+    public static final String UNAME = "uname";
+
+    /** Key for password. */
+    public static final String PASSWD = "passwd";
+
+    /** Key for private key file. */
+    public static final String KEY = "key";
+
+    /** Key for number of nodes. */
+    public static final String NODES = "nodes";
+
+    /** Key for Ignite home folder. */
+    public static final String IGNITE_HOME = "igniteHome";
+
+    /** Key for configuration path. */
+    public static final String CFG = "cfg";
+
+    /** Key for script path. */
+    public static final String SCRIPT = "script";
+
+    /** Key for logger. */
+    public static final String LOGGER = "logger";
+
+    /** Default connection timeout. */
+    public static final int DFLT_TIMEOUT = 10000;
+
+    /** Default maximum number of parallel connections. */
+    public static final int DFLT_MAX_CONN = 5;
+
+    /** Symbol that specifies range of IPs. */
+    private static final String RANGE_SMB = "~";
+
+    /** Default port. */
+    private static final int DFLT_PORT = 22;
+
+    /** Default number of nodes. */
+    private static final int DFLT_NODES = 1;
+
+    /** Default configuration path. */
+    private static final String DFLT_CFG = "";
+
+    /** Defaults section name. */
+    private static final String DFLT_SECTION = "defaults";
+
+    /**
+     * Ensure singleton.
+     */
+    private IgniteNodeStartUtils() {
+        // No-op.
+    }
+
+    /**
+     * Parses INI file.
+     *
+     * @param file File.
+     * @return Tuple with host maps and default values.
+     * @throws IgniteCheckedException In case of error.
+     */
+    public static IgniteBiTuple<Collection<Map<String, Object>>, Map<String, 
Object>> parseFile(
+        File file) throws IgniteCheckedException {
+        assert file != null;
+        assert file.exists();
+        assert file.isFile();
+
+        BufferedReader br = null;
+
+        int lineCnt = 1;
+
+        try {
+            br = new BufferedReader(new InputStreamReader(new 
FileInputStream(file), "UTF-8"));
+
+            String section = null;
+
+            Collection<Map<String, Object>> hosts = new LinkedList<>();
+            Map<String, Object> dflts = null;
+            Map<String, Object> props = null;
+
+            for (String line; (line = br.readLine()) != null; lineCnt++) {
+                String l = line.trim();
+
+                if (l.isEmpty() || l.startsWith("#") || l.startsWith(";"))
+                    continue;
+
+                if (l.startsWith("[") && l.endsWith("]")) {
+                    Map<String, Object> dfltsTmp = processSection(section, 
hosts, dflts, props);
+
+                    if (dfltsTmp != null)
+                        dflts = dfltsTmp;
+
+                    props = new HashMap<>();
+
+                    section = l.substring(1, l.length() - 1);
+                }
+                else if (l.contains("=")) {
+                    if (section == null)
+                        throw new IgniteCheckedException("Ignite ini format 
doesn't support unnamed section.");
+
+                    String key = l.substring(0, l.indexOf('='));
+                    String val = line.substring(line.indexOf('=') + 1);
+
+                    switch (key) {
+                        case HOST:
+                        case UNAME:
+                        case PASSWD:
+                        case IGNITE_HOME:
+                        case CFG:
+                        case SCRIPT:
+                            props.put(key, val);
+                            break;
+
+                        case PORT:
+                        case NODES:
+                            props.put(key, Integer.valueOf(val));
+                            break;
+
+                        case KEY:
+                            props.put(KEY, new File(val));
+                            break;
+                    }
+                }
+                else
+                    throw new IgniteCheckedException("Failed to parse INI file 
(line " + lineCnt + ").");
+            }
+
+            Map<String, Object> dfltsTmp = processSection(section, hosts, 
dflts, props);
+
+            if (dfltsTmp != null)
+                dflts = dfltsTmp;
+
+            return F.t(hosts, dflts);
+        }
+        catch (IOException | NumberFormatException e) {
+            throw new IgniteCheckedException("Failed to parse INI file (line " 
+ lineCnt + ").", e);
+        }
+        finally {
+            U.closeQuiet(br);
+        }
+    }
+
+    /**
+     * Processes section of parsed INI file.
+     *
+     * @param section Name of the section.
+     * @param hosts Already parsed properties for sections excluding default.
+     * @param dflts Parsed properties for default section.
+     * @param props Current properties.
+     * @return Default properties if specified section is default, {@code 
null} otherwise.
+     * @throws IgniteCheckedException If INI file contains several default 
sections.
+     */
+    private static Map<String, Object> processSection(String section, 
Collection<Map<String, Object>> hosts,
+        Map<String, Object> dflts, Map<String, Object> props) throws 
IgniteCheckedException {
+        if (section == null || props == null)
+            return null;
+
+        if (DFLT_SECTION.equalsIgnoreCase(section)) {
+            if (dflts != null)
+                throw new IgniteCheckedException("Only one '" + DFLT_SECTION + 
"' section is allowed.");
+
+            return props;
+        }
+        else {
+            hosts.add(props);
+
+            return null;
+        }
+    }
+
+    /**
+     * Makes specifications.
+     *
+     * @param hosts Host configurations.
+     * @param dflts Default values.
+     * @return Specification grouped by hosts.
+     * @throws IgniteCheckedException In case of error.
+     */
+    @SuppressWarnings("ConstantConditions")
+    public static Map<String, Collection<IgniteRemoteStartSpecification>> 
specifications(
+        Collection<Map<String, Object>> hosts, @Nullable Map<String, Object> 
dflts)
+        throws IgniteCheckedException {
+        Map<String, Collection<IgniteRemoteStartSpecification>> specsMap = 
U.newHashMap(hosts.size());
+
+        IgniteRemoteStartSpecification dfltSpec = processDefaults(dflts);
+
+        for (Map<String, Object> host : hosts) {
+            Collection<IgniteRemoteStartSpecification> specs = 
processHost(host, dfltSpec);
+
+            for (IgniteRemoteStartSpecification spec : specs)
+                F.addIfAbsent(specsMap, spec.host(), new 
Callable<Collection<IgniteRemoteStartSpecification>>() {
+                    @Override public 
Collection<IgniteRemoteStartSpecification> call() throws Exception {
+                        return new HashSet<>();
+                    }
+                }).add(spec);
+        }
+
+        return specsMap;
+    }
+
+    /**
+     * Converts properties map to default specification.
+     *
+     * @param dflts Properties.
+     * @return Specification.
+     * @throws IgniteCheckedException If properties are invalid.
+     */
+    private static IgniteRemoteStartSpecification processDefaults(@Nullable 
Map<String, Object> dflts)
+        throws IgniteCheckedException {
+        int port = DFLT_PORT;
+        String uname = System.getProperty("user.name");
+        String passwd = null;
+        File key = null;
+        int nodes = DFLT_NODES;
+        String igniteHome = null;
+        String cfg = DFLT_CFG;
+        String script = null;
+        IgniteLogger log = null;
+
+        if (dflts != null) {
+            if (dflts.get(PORT) != null)
+                port = (Integer)dflts.get(PORT);
+
+            if (dflts.get(UNAME) != null)
+                uname = (String)dflts.get(UNAME);
+
+            if (dflts.get(PASSWD) != null)
+                passwd = (String)dflts.get(PASSWD);
+
+            if (dflts.get(KEY) != null)
+                key = (File)dflts.get(KEY);
+
+            if (dflts.get(NODES) != null)
+                nodes = (Integer)dflts.get(NODES);
+
+            if (dflts.get(IGNITE_HOME) != null)
+                igniteHome = (String)dflts.get(IGNITE_HOME);
+
+            if (dflts.get(CFG) != null)
+                cfg = (String)dflts.get(CFG);
+
+            if (dflts.get(SCRIPT) != null)
+                script = (String)dflts.get(SCRIPT);
+
+            if (dflts.get(LOGGER) != null)
+                log = (IgniteLogger)dflts.get(LOGGER);
+        }
+
+        if (port <= 0)
+            throw new IgniteCheckedException("Invalid port number: " + port);
+
+        if (nodes <= 0)
+            throw new IgniteCheckedException("Invalid number of nodes: " + 
nodes);
+
+        return new IgniteRemoteStartSpecification(null, port, uname, passwd,
+            key, nodes, igniteHome, cfg, script, log);
+    }
+
+    /**
+     * Converts properties map to specification.
+     *
+     * @param props Properties.
+     * @param dfltSpec Default specification.
+     * @return Specification.
+     * @throws IgniteCheckedException If properties are invalid.
+     */
+    private static Collection<IgniteRemoteStartSpecification> 
processHost(Map<String, Object> props,
+        IgniteRemoteStartSpecification dfltSpec) throws IgniteCheckedException 
{
+        assert props != null;
+        assert dfltSpec != null;
+
+        if (props.get(HOST) == null)
+            throw new IgniteCheckedException("Host must be specified.");
+
+        Set<String> hosts = expandHost((String)props.get(HOST));
+        int port = props.get(PORT) != null ? (Integer)props.get(PORT) : 
dfltSpec.port();
+        String uname = props.get(UNAME) != null ? (String)props.get(UNAME) : 
dfltSpec.username();
+        String passwd = props.get(PASSWD) != null ? (String)props.get(PASSWD) 
: dfltSpec.password();
+        File key = props.get(KEY) != null ? (File)props.get(KEY) : 
dfltSpec.key();
+        int nodes = props.get(NODES) != null ? (Integer)props.get(NODES) : 
dfltSpec.nodes();
+        String igniteHome = props.get(IGNITE_HOME) != null ? 
(String)props.get(IGNITE_HOME) : dfltSpec.igniteHome();
+        String cfg = props.get(CFG) != null ? (String)props.get(CFG) : 
dfltSpec.configuration();
+        String script = props.get(SCRIPT) != null ? (String)props.get(SCRIPT) 
: dfltSpec.script();
+
+        if (port<= 0)
+            throw new IgniteCheckedException("Invalid port number: " + port);
+
+        if (nodes <= 0)
+            throw new IgniteCheckedException("Invalid number of nodes: " + 
nodes);
+
+        if (passwd == null && key == null)
+            throw new IgniteCheckedException("Password or private key file 
must be specified.");
+
+        if (passwd != null && key != null)
+            passwd = null;
+
+        Collection<IgniteRemoteStartSpecification> specs =
+            new ArrayList<>(hosts.size());
+
+        for (String host : hosts)
+            specs.add(new IgniteRemoteStartSpecification(host, port, uname, 
passwd,
+                key, nodes, igniteHome, cfg, script, dfltSpec.logger()));
+
+        return specs;
+    }
+
+    /**
+     * Parses and expands range of IPs, if needed. Host names without the range
+     * returned as is.
+     *
+     * @param addr Host with or without `~` range.
+     * @return Set of individual host names (IPs).
+     * @throws IgniteCheckedException In case of error.
+     */
+    public static Set<String> expandHost(String addr) throws 
IgniteCheckedException {
+        assert addr != null;
+
+        Set<String> addrs = new HashSet<>();
+
+        if (addr.contains(RANGE_SMB)) {
+            String[] parts = addr.split(RANGE_SMB);
+
+            if (parts.length != 2)
+                throw new IgniteCheckedException("Invalid IP range: " + addr);
+
+            int lastDot = parts[0].lastIndexOf('.');
+
+            if (lastDot < 0)
+                throw new IgniteCheckedException("Invalid IP range: " + addr);
+
+            String base = parts[0].substring(0, lastDot);
+            String begin = parts[0].substring(lastDot + 1);
+            String end = parts[1];
+
+            try {
+                int a = Integer.valueOf(begin);
+                int b = Integer.valueOf(end);
+
+                if (a > b)
+                    throw new IgniteCheckedException("Invalid IP range: " + 
addr);
+
+                for (int i = a; i <= b; i++)
+                    addrs.add(base + "." + i);
+            }
+            catch (NumberFormatException e) {
+                throw new IgniteCheckedException("Invalid IP range: " + addr, 
e);
+            }
+        }
+        else
+            addrs.add(addr);
+
+        return addrs;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteRemoteStartSpecification.java
----------------------------------------------------------------------
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteRemoteStartSpecification.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteRemoteStartSpecification.java
new file mode 100644
index 0000000..b3f9bb1
--- /dev/null
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteRemoteStartSpecification.java
@@ -0,0 +1,279 @@
+/*
+ * 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.ignite.internal.util.nodestart;
+
+import org.apache.ignite.*;
+import org.apache.ignite.internal.util.typedef.*;
+import org.jetbrains.annotations.*;
+
+import java.io.*;
+
+/**
+ * Host data.
+ */
+public class IgniteRemoteStartSpecification {
+    /** Hostname. */
+    private final String host;
+
+    /** Port number. */
+    private final int port;
+
+    /** Username. */
+    private final String uname;
+
+    /** Password. */
+    private final String passwd;
+
+    /** Private key file. */
+    private final File key;
+
+    /** Private key filename. */
+    private final String keyName;
+
+    /** Number of nodes to start. */
+    private final int nodes;
+
+    /** Ignite installation folder. */
+    private String igniteHome;
+
+    /** Configuration path. */
+    private String cfg;
+
+    /** Configuration filename. */
+    private String cfgName;
+
+    /** Script path. */
+    private String script;
+
+    /** Custom logger. */
+    private IgniteLogger logger;
+
+    /** Valid flag */
+    private boolean valid;
+
+    /**
+     * @param host Hostname.
+     * @param port Port number.
+     * @param uname Username.
+     * @param passwd Password (can be {@code null} if private key 
authentication is used).
+     * @param key Private key file path.
+     * @param nodes Number of nodes to start.
+     * @param igniteHome Ignite installation folder.
+     * @param cfg Configuration path.
+     * @param script Script path.
+     */
+    public IgniteRemoteStartSpecification(@Nullable String host, int port, 
@Nullable String uname,
+        @Nullable String passwd, @Nullable File key, int nodes, @Nullable 
String igniteHome,
+        @Nullable String cfg, @Nullable String script) {
+        this(host, port, uname, passwd, key, nodes, igniteHome, cfg, script, 
null);
+    }
+
+    /**
+     * @param host Hostname.
+     * @param port Port number.
+     * @param uname Username.
+     * @param passwd Password (can be {@code null} if private key 
authentication is used).
+     * @param key Private key file path.
+     * @param nodes Number of nodes to start.
+     * @param igniteHome Ignite installation folder.
+     * @param cfg Configuration path.
+     * @param script Script path.
+     * @param logger Custom logger.
+     */
+    public IgniteRemoteStartSpecification(@Nullable String host, int port, 
@Nullable String uname,
+        @Nullable String passwd, @Nullable File key, int nodes, @Nullable 
String igniteHome,
+        @Nullable String cfg, @Nullable String script, @Nullable IgniteLogger 
logger) {
+        assert port > 0;
+        assert nodes > 0;
+
+        this.host = !F.isEmpty(host) ? host : null;
+        this.port = port;
+        this.uname = !F.isEmpty(uname) ? uname : null;
+        this.passwd = !F.isEmpty(passwd) ? passwd : null;
+        this.key = key;
+        this.nodes = nodes;
+        this.igniteHome = !F.isEmpty(igniteHome) ? igniteHome : null;
+        this.cfg = !F.isEmpty(cfg) ? cfg : null;
+        cfgName = cfg == null ? null : shorten(cfg);
+        keyName = key == null ? "" : shorten(key.getAbsolutePath());
+        this.script = !F.isEmpty(script) ? script : null;
+        this.logger = logger;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o) return true;
+
+        if (!(o instanceof IgniteRemoteStartSpecification)) return false;
+
+        IgniteRemoteStartSpecification that = 
(IgniteRemoteStartSpecification)o;
+
+        return (host == null ? that.host == null : host.equals(that.host)) &&
+            (uname == null ? that.uname == null : uname.equals(that.uname)) &&
+            (passwd == null ? that.passwd == null : 
passwd.equals(that.passwd)) &&
+            (key == null ? that.key == null : key.equals(that.key)) &&
+            (igniteHome == null ? that.igniteHome == null : 
igniteHome.equals(that.igniteHome)) &&
+            (cfg == null ? that.cfg == null : cfg.equals(that.cfg)) &&
+            (script == null ? that.script == null : 
script.equals(that.script)) &&
+            port == that.port && nodes == that.nodes;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        int res = host == null ? 0 : host.hashCode();
+
+        res = 31 * res + (uname == null ? 0 : uname.hashCode());
+        res = 31 * res + (passwd == null ? 0 : passwd.hashCode());
+        res = 31 * res + (key == null ? 0 : key.hashCode());
+        res = 31 * res + (igniteHome == null ? 0 : igniteHome.hashCode());
+        res = 31 * res + (cfg == null ? 0 : cfg.hashCode());
+        res = 31 * res + (script == null ? 0 : script.hashCode());
+        res = 31 * res + port;
+        res = 31 * res + nodes;
+
+        return res;
+    }
+
+    /**
+     * Get filename from path.
+     *
+     * @param path Path.
+     * @return Filename.
+     */
+    private static String shorten(String path) {
+        int idx1 = path.lastIndexOf('/');
+        int idx2 = path.lastIndexOf('\\');
+        int idx = Math.max(idx1, idx2);
+
+        return idx == -1 ? path : path.substring(idx + 1);
+    }
+
+    /**
+     * @return Hostname.
+     */
+    public String host() {
+        return host;
+    }
+
+    /**
+     * @return Port number.
+     */
+    public int port() {
+        return port;
+    }
+
+    /**
+     * @return Username.
+     */
+    public String username() {
+        return uname;
+    }
+
+    /**
+     * @return Password.
+     */
+    public String password() {
+        return passwd;
+    }
+
+    /**
+     * @return Private key file path.
+     */
+    public File key() {
+        return key;
+    }
+
+    /**
+     * @return Private key file name.
+     */
+    public String keyName() {
+        return keyName;
+    }
+
+    /**
+     * @return Number of nodes to start.
+     */
+    public int nodes() {
+        return nodes;
+    }
+
+    /**
+     * @return Ignite installation folder.
+     */
+    public String igniteHome() {
+        return igniteHome;
+    }
+
+    /**
+     * @return Configuration full path.
+     */
+    public String configuration() {
+        return cfg;
+    }
+
+    /**
+     * @return Configuration path short version - just file name.
+     */
+    public String configurationName() {
+        return cfgName;
+    }
+
+    /**
+     * @return Script path.
+     */
+    public String script() {
+        return script;
+    }
+
+    /**
+     * @return Custom logger.
+     */
+    public IgniteLogger logger() {
+        return logger;
+    }
+
+    /**
+     * @return Valid flag.
+     */
+    public boolean valid() {
+        return valid;
+    }
+
+    /**
+     * @param valid Valid flag.
+     */
+    public void valid(boolean valid) {
+        this.valid = valid;
+    }
+
+    /**
+     * Sets correct separator in paths.
+     *
+     * @param separator Separator.
+     */
+    public void fixPaths(char separator) {
+        if (igniteHome != null)
+            igniteHome = igniteHome.replace('\\', separator).replace('/', 
separator);
+
+        if (script != null)
+            script = script.replace('\\', separator).replace('/', separator);
+
+        if (cfg != null)
+            cfg = cfg.replace('\\', separator).replace('/', separator);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteSshProcessor.java
----------------------------------------------------------------------
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteSshProcessor.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteSshProcessor.java
index 3b68ee8..9f59fa2 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteSshProcessor.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteSshProcessor.java
@@ -30,5 +30,5 @@ public interface IgniteSshProcessor {
      * @param timeout Connection timeout.
      * @return {@link Callable} starting node using SSH.
      */
-    public GridNodeCallable nodeStartCallable(GridRemoteStartSpecification 
spec, int timeout);
+    public IgniteNodeCallable nodeStartCallable(IgniteRemoteStartSpecification 
spec, int timeout);
 }

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/core/src/test/bin/start-nodes-custom.bat
----------------------------------------------------------------------
diff --git a/modules/core/src/test/bin/start-nodes-custom.bat 
b/modules/core/src/test/bin/start-nodes-custom.bat
index 51fbce0..554e9a6 100644
--- a/modules/core/src/test/bin/start-nodes-custom.bat
+++ b/modules/core/src/test/bin/start-nodes-custom.bat
@@ -18,5 +18,5 @@ set SCRIPT_DIR=%~dp0
 if %SCRIPT_DIR:~-1,1% == \ set SCRIPT_DIR=%SCRIPT_DIR:~0,-1%
 
 :: -np option is mandatory, if it is not provided then we will wait for a user 
input,
-:: as a result ggservice windows service hangs forever
+:: as a result igniteservice windows service hangs forever
 call "%SCRIPT_DIR%\..\..\..\..\..\bin\ignite.bat" -v -np 
modules\core\src\test\config\spring-start-nodes-attr.xml

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/core/src/test/config/start-nodes.ini
----------------------------------------------------------------------
diff --git a/modules/core/src/test/config/start-nodes.ini 
b/modules/core/src/test/config/start-nodes.ini
index 8f75e68..8b5922c 100644
--- a/modules/core/src/test/config/start-nodes.ini
+++ b/modules/core/src/test/config/start-nodes.ini
@@ -5,7 +5,7 @@ uname=uname1
 passwd=passwd1
 key=key1
 nodes=1
-ggHome=ggHome1
+igniteHome=ggHome1
 cfg=cfg1
 script=script1
 
@@ -16,7 +16,7 @@ uname=uname2
 passwd=passwd2
 key=key2
 nodes=2
-ggHome=ggHome2
+igniteHome=ggHome2
 cfg=cfg2
 script=script2
 
@@ -26,6 +26,6 @@ uname=uname3
 passwd=passwd3
 key=key3
 nodes=3
-ggHome=ggHome3
+igniteHome=ggHome3
 cfg=cfg3
 script=script3

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/ssh/src/main/java/org/apache/ignite/internal/util/nodestart/GridNodeCallableImpl.java
----------------------------------------------------------------------
diff --git 
a/modules/ssh/src/main/java/org/apache/ignite/internal/util/nodestart/GridNodeCallableImpl.java
 
b/modules/ssh/src/main/java/org/apache/ignite/internal/util/nodestart/GridNodeCallableImpl.java
deleted file mode 100644
index 044d11e..0000000
--- 
a/modules/ssh/src/main/java/org/apache/ignite/internal/util/nodestart/GridNodeCallableImpl.java
+++ /dev/null
@@ -1,344 +0,0 @@
-/*
- * 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.ignite.internal.util.nodestart;
-
-import com.jcraft.jsch.*;
-import org.apache.ignite.*;
-import org.apache.ignite.internal.*;
-import org.apache.ignite.internal.util.lang.*;
-import org.apache.ignite.internal.util.typedef.*;
-import org.apache.ignite.internal.util.typedef.internal.*;
-import org.apache.ignite.resources.*;
-
-import java.io.*;
-import java.text.*;
-import java.util.*;
-
-import static org.apache.ignite.IgniteSystemProperties.*;
-
-/**
- * SSH-based node starter.
- */
-public class GridNodeCallableImpl implements GridNodeCallable {
-    /** Default GridGain home path for Windows (taken from environment 
variable). */
-    private static final String DFLT_IGNITE_HOME_WIN = "%IGNITE_HOME%";
-
-    /** Default GridGain home path for Linux (taken from environment 
variable). */
-    private static final String DFLT_IGNITE_HOME_LINUX = "$IGNITE_HOME";
-
-    /** Default start script path for Windows. */
-    private static final String DFLT_SCRIPT_WIN = "bin\\ignite.bat -v -np";
-
-    /** Default start script path for Linux. */
-    private static final String DFLT_SCRIPT_LINUX = "bin/ignite.sh -v";
-
-    /**
-     * Logs folder for Windows.
-     * Folder for linux is configured in {@code ignite-log4j.xml}.
-     */
-    private static final String LOG_DIR_WIN = "work\\log";
-
-    /** Windows service executable. */
-    private static final String SVC_EXE = "bin\\include\\ggservice.exe";
-
-    /** Date format for log file name. */
-    private static final SimpleDateFormat FILE_NAME_DATE_FORMAT = new 
SimpleDateFormat("MM-dd-yyyy--HH-mm-ss");
-
-    /** Specification. */
-    private final GridRemoteStartSpecification spec;
-
-    /** Connection timeout. */
-    private final int timeout;
-
-    /** Logger. */
-    @IgniteLoggerResource
-    private IgniteLogger log;
-
-    /**
-     * Required by Externalizable.
-     */
-    public GridNodeCallableImpl() {
-        spec = null;
-        timeout = 0;
-
-        assert false;
-    }
-
-    /**
-     * Constructor.
-     *
-     * @param spec Specification.
-     * @param timeout Connection timeout.
-     */
-    public GridNodeCallableImpl(GridRemoteStartSpecification spec, int 
timeout) {
-        assert spec != null;
-
-        this.spec = spec;
-        this.timeout = timeout;
-    }
-
-    /** {@inheritDoc} */
-    @Override public GridTuple3<String, Boolean, String> call() {
-        JSch ssh = new JSch();
-
-        Session ses = null;
-
-        try {
-            if (spec.key() != null)
-                ssh.addIdentity(spec.key().getAbsolutePath());
-
-            ses = ssh.getSession(spec.username(), spec.host(), spec.port());
-
-            if (spec.password() != null)
-                ses.setPassword(spec.password());
-
-            ses.setConfig("StrictHostKeyChecking", "no");
-
-            ses.connect(timeout);
-
-            boolean win = isWindows(ses);
-
-            char separator = win ? '\\' : '/';
-
-            spec.fixPaths(separator);
-
-            String ggHome = spec.ggHome();
-
-            if (ggHome == null)
-                ggHome = win ? DFLT_IGNITE_HOME_WIN : DFLT_IGNITE_HOME_LINUX;
-
-            String script = spec.script();
-
-            if (script == null)
-                script = win ? DFLT_SCRIPT_WIN : DFLT_SCRIPT_LINUX;
-
-            String cfg = spec.configuration();
-
-            if (cfg == null)
-                cfg = "";
-
-            String startNodeCmd;
-            String scriptOutputFileName = FILE_NAME_DATE_FORMAT.format(new 
Date()) + '-'
-                + UUID.randomUUID().toString().substring(0, 8) + ".log";
-
-            if (win) {
-                String logDir = ggHome + '\\' + LOG_DIR_WIN;
-                String tmpDir = env(ses, "%TMP%", logDir);
-                String scriptOutputDir = tmpDir + "\\gridgain-startNodes";
-
-                shell(ses, "mkdir " + logDir);
-                shell(ses, "mkdir " + scriptOutputDir);
-
-                UUID id = UUID.randomUUID();
-
-                String svcName = "GridGain-" + id;
-                String svcPath = ggHome + '\\' + SVC_EXE;
-
-                startNodeCmd = new SB().
-                    a("cmd /c if exist \"").a(svcPath).a("\"").
-                    a(" sc create ").a(svcName).
-                    a(" binPath= \"").a(svcPath).a("\"").
-                    a(" && ").
-                    a("sc start ").a(svcName).
-                    a(" ").a(svcName).
-                    a(" \"").a(ggHome).a('\\').a(script).
-                    a(" ").a(cfg).a("\"").
-                    a(" \"").a(logDir).a("\\gridgain.").a(id).
-                    a(".log\" > 
").a(scriptOutputDir).a("\\").a(scriptOutputFileName).
-                    toString();
-            }
-            else { // Assume Unix.
-                int spaceIdx = script.indexOf(' ');
-
-                String scriptPath = spaceIdx > -1 ? script.substring(0, 
spaceIdx) : script;
-                String scriptArgs = spaceIdx > -1 ? script.substring(spaceIdx 
+ 1) : "";
-                String rmtLogArgs = buildRemoteLogArguments(spec.username(), 
spec.host());
-                String tmpDir = env(ses, "$TMPDIR", "/tmp/");
-                String scriptOutputDir = tmpDir + "gridgain-startNodes";
-
-                shell(ses, "mkdir " + scriptOutputDir);
-
-                // Mac os don't support ~ in double quotes. Trying get home 
path from remote system.
-                if (ggHome.startsWith("~")) {
-                    String homeDir = env(ses, "$HOME", "~");
-
-                    ggHome = ggHome.replaceFirst("~", homeDir);
-                }
-
-                startNodeCmd = new SB().
-                    // Console output is consumed, started nodes must use Grid 
file appenders for log.
-                        a("nohup ").
-                    a("\"").a(ggHome).a('/').a(scriptPath).a("\"").
-                    a(" ").a(scriptArgs).
-                    a(!cfg.isEmpty() ? " \"" : "").a(cfg).a(!cfg.isEmpty() ? 
"\"" : "").
-                    a(rmtLogArgs).
-                    a(" > 
").a(scriptOutputDir).a("/").a(scriptOutputFileName).a(" 2>& 1 &").
-                    toString();
-            }
-
-            info("Starting remote node with SSH command: " + startNodeCmd, 
spec.logger(), log);
-
-            shell(ses, startNodeCmd);
-
-            return new GridTuple3<>(spec.host(), true, null);
-        }
-        catch (IgniteInterruptedCheckedException e) {
-            return new GridTuple3<>(spec.host(), false, e.getMessage());
-        }
-        catch (Exception e) {
-            return new GridTuple3<>(spec.host(), false, 
X.getFullStackTrace(e));
-        }
-        finally {
-            if (ses != null && ses.isConnected())
-                ses.disconnect();
-        }
-    }
-
-    /**
-     * Executes command using {@code shell} channel.
-     *
-     * @param ses SSH session.
-     * @param cmd Command.
-     * @throws JSchException In case of SSH error.
-     * @throws IOException If IO error occurs.
-     * @throws org.apache.ignite.internal.IgniteInterruptedCheckedException If 
thread was interrupted while waiting.
-     */
-    private void shell(Session ses, String cmd) throws JSchException, 
IOException, IgniteInterruptedCheckedException {
-        ChannelShell ch = null;
-
-        try {
-            ch = (ChannelShell)ses.openChannel("shell");
-
-            ch.connect();
-
-            try (PrintStream out = new PrintStream(ch.getOutputStream(), 
true)) {
-                out.println(cmd);
-
-                U.sleep(1000);
-            }
-        }
-        finally {
-            if (ch != null && ch.isConnected())
-                ch.disconnect();
-        }
-    }
-
-    /**
-     * Checks whether host is running Windows OS.
-     *
-     * @param ses SSH session.
-     * @return Whether host is running Windows OS.
-     * @throws JSchException In case of SSH error.
-     */
-    private boolean isWindows(Session ses) throws JSchException {
-        try {
-            return exec(ses, "cmd.exe") != null;
-        }
-        catch (IOException ignored) {
-            return false;
-        }
-    }
-
-    /**
-     * Gets the value of the specified environment variable.
-     *
-     * @param ses SSH session.
-     * @param name environment variable name.
-     * @param dflt default value.
-     * @return environment variable value.
-     * @throws JSchException In case of SSH error.
-     */
-    private String env(Session ses, String name, String dflt) throws 
JSchException {
-        try {
-            return exec(ses, "echo " + name);
-        }
-        catch (IOException ignored) {
-            return dflt;
-        }
-    }
-
-    /**
-     * Gets the value of the specified environment variable.
-     *
-     * @param ses SSH session.
-     * @param cmd environment variable name.
-     * @return environment variable value.
-     * @throws JSchException In case of SSH error.
-     * @throws IOException If failed.
-     */
-    private String exec(Session ses, String cmd) throws JSchException, 
IOException {
-        ChannelExec ch = null;
-
-        try {
-            ch = (ChannelExec)ses.openChannel("exec");
-
-            ch.setCommand(cmd);
-
-            ch.connect();
-
-            try (BufferedReader reader = new BufferedReader(new 
InputStreamReader(ch.getInputStream()))) {
-                return reader.readLine();
-            }
-        }
-        finally {
-            if (ch != null && ch.isConnected())
-                ch.disconnect();
-        }
-    }
-
-    /**
-     * Builds ignite.sh attributes to set up SSH username and password and log 
directory for started node.
-     *
-     * @param username SSH user name.
-     * @param host Host.
-     * @return {@code ignite.sh} script arguments.
-     */
-    private String buildRemoteLogArguments(String username, String host) {
-        assert username != null;
-        assert host != null;
-
-        SB sb = new SB();
-
-        sb.a(" -J-D").a(IGNITE_SSH_HOST).a("=\"").a(host).a("\"").
-            a(" -J-D").a(IGNITE_SSH_USER_NAME).a("=\"").a(username).a("\"");
-
-        return sb.toString();
-    }
-
-    /**
-     * @param log Logger.
-     * @return This callable for chaining method calls.
-     */
-    public GridNodeCallable setLogger(IgniteLogger log) {
-        this.log = log;
-
-        return this;
-    }
-
-    /**
-     * Log info message to loggers.
-     *
-     * @param msg Message text.
-     * @param loggers Loggers.
-     */
-    private void info(String msg, IgniteLogger... loggers) {
-        for (IgniteLogger logger : loggers)
-            if (logger != null && logger.isInfoEnabled())
-                logger.info(msg);
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/ssh/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteNodeCallableImpl.java
----------------------------------------------------------------------
diff --git 
a/modules/ssh/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteNodeCallableImpl.java
 
b/modules/ssh/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteNodeCallableImpl.java
new file mode 100644
index 0000000..9d7d091
--- /dev/null
+++ 
b/modules/ssh/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteNodeCallableImpl.java
@@ -0,0 +1,344 @@
+/*
+ * 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.ignite.internal.util.nodestart;
+
+import com.jcraft.jsch.*;
+import org.apache.ignite.*;
+import org.apache.ignite.internal.*;
+import org.apache.ignite.internal.util.lang.*;
+import org.apache.ignite.internal.util.typedef.*;
+import org.apache.ignite.internal.util.typedef.internal.*;
+import org.apache.ignite.resources.*;
+
+import java.io.*;
+import java.text.*;
+import java.util.*;
+
+import static org.apache.ignite.IgniteSystemProperties.*;
+
+/**
+ * SSH-based node starter.
+ */
+public class IgniteNodeCallableImpl implements IgniteNodeCallable {
+    /** Default Ignite home path for Windows (taken from environment 
variable). */
+    private static final String DFLT_IGNITE_HOME_WIN = "%IGNITE_HOME%";
+
+    /** Default Ignite home path for Linux (taken from environment variable). 
*/
+    private static final String DFLT_IGNITE_HOME_LINUX = "$IGNITE_HOME";
+
+    /** Default start script path for Windows. */
+    private static final String DFLT_SCRIPT_WIN = "bin\\ignite.bat -v -np";
+
+    /** Default start script path for Linux. */
+    private static final String DFLT_SCRIPT_LINUX = "bin/ignite.sh -v";
+
+    /**
+     * Logs folder for Windows.
+     * Folder for linux is configured in {@code ignite-log4j.xml}.
+     */
+    private static final String LOG_DIR_WIN = "work\\log";
+
+    /** Windows service executable. */
+    private static final String SVC_EXE = "bin\\include\\igniteservice.exe";
+
+    /** Date format for log file name. */
+    private static final SimpleDateFormat FILE_NAME_DATE_FORMAT = new 
SimpleDateFormat("MM-dd-yyyy--HH-mm-ss");
+
+    /** Specification. */
+    private final IgniteRemoteStartSpecification spec;
+
+    /** Connection timeout. */
+    private final int timeout;
+
+    /** Logger. */
+    @IgniteLoggerResource
+    private IgniteLogger log;
+
+    /**
+     * Required by Externalizable.
+     */
+    public IgniteNodeCallableImpl() {
+        spec = null;
+        timeout = 0;
+
+        assert false;
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param spec Specification.
+     * @param timeout Connection timeout.
+     */
+    public IgniteNodeCallableImpl(IgniteRemoteStartSpecification spec, int 
timeout) {
+        assert spec != null;
+
+        this.spec = spec;
+        this.timeout = timeout;
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridTuple3<String, Boolean, String> call() {
+        JSch ssh = new JSch();
+
+        Session ses = null;
+
+        try {
+            if (spec.key() != null)
+                ssh.addIdentity(spec.key().getAbsolutePath());
+
+            ses = ssh.getSession(spec.username(), spec.host(), spec.port());
+
+            if (spec.password() != null)
+                ses.setPassword(spec.password());
+
+            ses.setConfig("StrictHostKeyChecking", "no");
+
+            ses.connect(timeout);
+
+            boolean win = isWindows(ses);
+
+            char separator = win ? '\\' : '/';
+
+            spec.fixPaths(separator);
+
+            String igniteHome = spec.igniteHome();
+
+            if (igniteHome == null)
+                igniteHome = win ? DFLT_IGNITE_HOME_WIN : 
DFLT_IGNITE_HOME_LINUX;
+
+            String script = spec.script();
+
+            if (script == null)
+                script = win ? DFLT_SCRIPT_WIN : DFLT_SCRIPT_LINUX;
+
+            String cfg = spec.configuration();
+
+            if (cfg == null)
+                cfg = "";
+
+            String startNodeCmd;
+            String scriptOutputFileName = FILE_NAME_DATE_FORMAT.format(new 
Date()) + '-'
+                + UUID.randomUUID().toString().substring(0, 8) + ".log";
+
+            if (win) {
+                String logDir = igniteHome + '\\' + LOG_DIR_WIN;
+                String tmpDir = env(ses, "%TMP%", logDir);
+                String scriptOutputDir = tmpDir + "\\ignite-startNodes";
+
+                shell(ses, "mkdir " + logDir);
+                shell(ses, "mkdir " + scriptOutputDir);
+
+                UUID id = UUID.randomUUID();
+
+                String svcName = "Ignite-" + id;
+                String svcPath = igniteHome + '\\' + SVC_EXE;
+
+                startNodeCmd = new SB().
+                    a("cmd /c if exist \"").a(svcPath).a("\"").
+                    a(" sc create ").a(svcName).
+                    a(" binPath= \"").a(svcPath).a("\"").
+                    a(" && ").
+                    a("sc start ").a(svcName).
+                    a(" ").a(svcName).
+                    a(" \"").a(igniteHome).a('\\').a(script).
+                    a(" ").a(cfg).a("\"").
+                    a(" \"").a(logDir).a("\\ignite.").a(id).
+                    a(".log\" > 
").a(scriptOutputDir).a("\\").a(scriptOutputFileName).
+                    toString();
+            }
+            else { // Assume Unix.
+                int spaceIdx = script.indexOf(' ');
+
+                String scriptPath = spaceIdx > -1 ? script.substring(0, 
spaceIdx) : script;
+                String scriptArgs = spaceIdx > -1 ? script.substring(spaceIdx 
+ 1) : "";
+                String rmtLogArgs = buildRemoteLogArguments(spec.username(), 
spec.host());
+                String tmpDir = env(ses, "$TMPDIR", "/tmp/");
+                String scriptOutputDir = tmpDir + "ignite-startNodes";
+
+                shell(ses, "mkdir " + scriptOutputDir);
+
+                // Mac os don't support ~ in double quotes. Trying get home 
path from remote system.
+                if (igniteHome.startsWith("~")) {
+                    String homeDir = env(ses, "$HOME", "~");
+
+                    igniteHome = igniteHome.replaceFirst("~", homeDir);
+                }
+
+                startNodeCmd = new SB().
+                    // Console output is consumed, started nodes must use 
Ignite file appenders for log.
+                        a("nohup ").
+                    a("\"").a(igniteHome).a('/').a(scriptPath).a("\"").
+                    a(" ").a(scriptArgs).
+                    a(!cfg.isEmpty() ? " \"" : "").a(cfg).a(!cfg.isEmpty() ? 
"\"" : "").
+                    a(rmtLogArgs).
+                    a(" > 
").a(scriptOutputDir).a("/").a(scriptOutputFileName).a(" 2>& 1 &").
+                    toString();
+            }
+
+            info("Starting remote node with SSH command: " + startNodeCmd, 
spec.logger(), log);
+
+            shell(ses, startNodeCmd);
+
+            return new GridTuple3<>(spec.host(), true, null);
+        }
+        catch (IgniteInterruptedCheckedException e) {
+            return new GridTuple3<>(spec.host(), false, e.getMessage());
+        }
+        catch (Exception e) {
+            return new GridTuple3<>(spec.host(), false, 
X.getFullStackTrace(e));
+        }
+        finally {
+            if (ses != null && ses.isConnected())
+                ses.disconnect();
+        }
+    }
+
+    /**
+     * Executes command using {@code shell} channel.
+     *
+     * @param ses SSH session.
+     * @param cmd Command.
+     * @throws JSchException In case of SSH error.
+     * @throws IOException If IO error occurs.
+     * @throws org.apache.ignite.internal.IgniteInterruptedCheckedException If 
thread was interrupted while waiting.
+     */
+    private void shell(Session ses, String cmd) throws JSchException, 
IOException, IgniteInterruptedCheckedException {
+        ChannelShell ch = null;
+
+        try {
+            ch = (ChannelShell)ses.openChannel("shell");
+
+            ch.connect();
+
+            try (PrintStream out = new PrintStream(ch.getOutputStream(), 
true)) {
+                out.println(cmd);
+
+                U.sleep(1000);
+            }
+        }
+        finally {
+            if (ch != null && ch.isConnected())
+                ch.disconnect();
+        }
+    }
+
+    /**
+     * Checks whether host is running Windows OS.
+     *
+     * @param ses SSH session.
+     * @return Whether host is running Windows OS.
+     * @throws JSchException In case of SSH error.
+     */
+    private boolean isWindows(Session ses) throws JSchException {
+        try {
+            return exec(ses, "cmd.exe") != null;
+        }
+        catch (IOException ignored) {
+            return false;
+        }
+    }
+
+    /**
+     * Gets the value of the specified environment variable.
+     *
+     * @param ses SSH session.
+     * @param name environment variable name.
+     * @param dflt default value.
+     * @return environment variable value.
+     * @throws JSchException In case of SSH error.
+     */
+    private String env(Session ses, String name, String dflt) throws 
JSchException {
+        try {
+            return exec(ses, "echo " + name);
+        }
+        catch (IOException ignored) {
+            return dflt;
+        }
+    }
+
+    /**
+     * Gets the value of the specified environment variable.
+     *
+     * @param ses SSH session.
+     * @param cmd environment variable name.
+     * @return environment variable value.
+     * @throws JSchException In case of SSH error.
+     * @throws IOException If failed.
+     */
+    private String exec(Session ses, String cmd) throws JSchException, 
IOException {
+        ChannelExec ch = null;
+
+        try {
+            ch = (ChannelExec)ses.openChannel("exec");
+
+            ch.setCommand(cmd);
+
+            ch.connect();
+
+            try (BufferedReader reader = new BufferedReader(new 
InputStreamReader(ch.getInputStream()))) {
+                return reader.readLine();
+            }
+        }
+        finally {
+            if (ch != null && ch.isConnected())
+                ch.disconnect();
+        }
+    }
+
+    /**
+     * Builds ignite.sh attributes to set up SSH username and password and log 
directory for started node.
+     *
+     * @param username SSH user name.
+     * @param host Host.
+     * @return {@code ignite.sh} script arguments.
+     */
+    private String buildRemoteLogArguments(String username, String host) {
+        assert username != null;
+        assert host != null;
+
+        SB sb = new SB();
+
+        sb.a(" -J-D").a(IGNITE_SSH_HOST).a("=\"").a(host).a("\"").
+            a(" -J-D").a(IGNITE_SSH_USER_NAME).a("=\"").a(username).a("\"");
+
+        return sb.toString();
+    }
+
+    /**
+     * @param log Logger.
+     * @return This callable for chaining method calls.
+     */
+    public IgniteNodeCallable setLogger(IgniteLogger log) {
+        this.log = log;
+
+        return this;
+    }
+
+    /**
+     * Log info message to loggers.
+     *
+     * @param msg Message text.
+     * @param loggers Loggers.
+     */
+    private void info(String msg, IgniteLogger... loggers) {
+        for (IgniteLogger logger : loggers)
+            if (logger != null && logger.isInfoEnabled())
+                logger.info(msg);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/67376020/modules/ssh/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteSshProcessorImpl.java
----------------------------------------------------------------------
diff --git 
a/modules/ssh/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteSshProcessorImpl.java
 
b/modules/ssh/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteSshProcessorImpl.java
index 4d867c0..723ce35 100644
--- 
a/modules/ssh/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteSshProcessorImpl.java
+++ 
b/modules/ssh/src/main/java/org/apache/ignite/internal/util/nodestart/IgniteSshProcessorImpl.java
@@ -22,7 +22,7 @@ package org.apache.ignite.internal.util.nodestart;
  */
 public class IgniteSshProcessorImpl implements IgniteSshProcessor {
     /** {@inheritDoc} */
-    @Override public GridNodeCallable 
nodeStartCallable(GridRemoteStartSpecification spec, int timeout) {
-        return new GridNodeCallableImpl(spec, timeout);
+    @Override public IgniteNodeCallable 
nodeStartCallable(IgniteRemoteStartSpecification spec, int timeout) {
+        return new IgniteNodeCallableImpl(spec, timeout);
     }
 }

Reply via email to