/*
* 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 java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgniteBiTuple;
import org.jetbrains.annotations.Nullable;
/**
* 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;
}
}