package org.ovirt.engine.core.config; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.net.ConnectException; import java.security.InvalidParameterException; import java.sql.SQLException; import java.text.MessageFormat; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.HierarchicalConfiguration; import org.apache.commons.configuration.SubnodeConfiguration; import org.apache.commons.configuration.tree.ConfigurationNode; import org.apache.commons.lang.StringUtils; import org.ovirt.engine.core.config.db.ConfigDao; import org.ovirt.engine.core.config.db.ConfigDaoImpl; import org.ovirt.engine.core.config.entity.ConfigKey; import org.ovirt.engine.core.config.entity.ConfigKeyFactory; import org.ovirt.engine.core.config.validation.ConfigActionType; import org.ovirt.engine.core.tools.ToolConsole; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The <code>EngineConfigLogic</code> class is responsible for the logic of the EngineConfig tool. */ public class EngineConfigLogic { // The log: private static final Logger log = LoggerFactory.getLogger(EngineConfigLogic.class); // The console: private static final ToolConsole console = ToolConsole.getInstance(); private static final String ALTERNATE_KEY = "alternateKey"; private static final String MERGABLE_TOKEN = "mergable"; private static final String DELIMITER_TOKEN = "delimiter"; private static final String MERGE_NOT_SUPPORTED_MSG = "%s does not support merge of values."; private static final String MERGE_SAME_VALUE_MSG = "Merge operation cancelled as value is unchanged."; private static final String MERGE_PERSIST_ERR_MSG = "setValue: error merging %s value. No such entry%s."; private static final String KEY_NOT_FOUND_ERR_MSG = "Cannot display help for key %1$s. The key does not exist at the configuration file of engine-config."; private Configuration appConfig; private HierarchicalConfiguration keysConfig; private Map<String, String> alternateKeysMap; private ConfigKeyFactory configKeyFactory; private ConfigDao configDao; private EngineConfigCLIParser parser; public EngineConfigLogic(EngineConfigCLIParser parser) throws Exception { this.parser = parser; init(); } /** * Initiates the members of the class. */ private void init() throws Exception { log.debug("init: beginning initiation of EngineConfigLogic"); appConfig = new AppConfig(parser.getAlternateConfigFile()).getFile(); keysConfig = new KeysConfig<>(parser.getAlternatePropertiesFile()).getFile(); populateAlternateKeyMap(keysConfig); ConfigKeyFactory.init(keysConfig, alternateKeysMap, parser); configKeyFactory = ConfigKeyFactory.getInstance(); try { this.configDao = new ConfigDaoImpl(appConfig); } catch (SQLException se) { log.debug("init: caught connection error. Error details: ", se); throw new ConnectException("Connection to the Database failed. Please check that the hostname and port number are correct and that the Database service is up and running."); } } private void populateAlternateKeyMap(HierarchicalConfiguration config) { List<SubnodeConfiguration> configurationsAt = config.configurationsAt("/*/" + ALTERNATE_KEY); alternateKeysMap = new HashMap<>(configurationsAt.size()); for (SubnodeConfiguration node : configurationsAt) { String rootKey = node.getRootNode() .getParentNode().getName(); String[] alternateKeys = config.getStringArray("/" + rootKey + "/" + ALTERNATE_KEY); for (String token : alternateKeys) { alternateKeysMap.put(token, rootKey); } } } /** * Executes desired action. Assumes the parser is now holding valid arguments. */ public void execute() throws Exception { ConfigActionType actionType = parser.getConfigAction(); log.debug("execute: beginning execution of action {}.", actionType); switch (actionType) { case ACTION_ALL: printAllValues(); break; case ACTION_LIST: listKeys(); break; case ACTION_GET: printKey(); break; case ACTION_SET: persistValue(); break; case ACTION_MERGE: mergeValue(); break; case ACTION_HELP: printHelpForKey(); break; case ACTION_RELOAD: reloadConfigurations(); break; default: // Should have already been discovered before execute log.debug("execute: unable to recognize action: {}.", actionType); throw new UnsupportedOperationException("Please tell me what to do: list? get? set? get-all? reload?"); } } /** * Is the actual execution of the 'list' action ('-l', '--list') */ private void listKeys() { if (parser.isOnlyReloadable()) { printReloadableKeys(); } else { printAvailableKeys(); } } private void reloadConfigurations() throws IOException { String user = parser.getUser(); String adminPassFile = parser.getAdminPassFile(); String pass = null; // Both user and password file were not given if (user == null) { user = startUserDialog(); pass = startPasswordDialog(user); // only user was given } else if (adminPassFile == null) { pass = startPasswordDialog(user); // Both user and password file were given } else { pass = getPassFromFile(adminPassFile); } loginAndReload(user, pass); } public static String getPassFromFile(String passFile) throws IOException { File f = new File(passFile); if (!f.exists()) { return StringUtils.EMPTY; } String pass; try (BufferedReader br = new BufferedReader(new FileReader(f))) { pass = br.readLine(); } if (pass == null) { return StringUtils.EMPTY; } return pass; } private void loginAndReload(String user, String pass) { // For now the reload action is undocumented in the help, // will be implemented in the next phase throw new UnsupportedOperationException(); } /** * Is called when user has not been given * * @return The user */ private String startUserDialog() throws IOException { log.debug("starting user dialog."); String user = null; while (StringUtils.isBlank(user)) { console.writeLine("Please enter user: "); user = console.readLine(); } return user; } /** * Is called when user has been given, and all is needed from the user is password * * @return The user's password */ public static String startPasswordDialog(String user) throws IOException { return startPasswordDialog(user, "Please enter a password"); } public static String startPasswordDialog(String user, String msg) throws IOException { log.debug("starting password dialog."); String prompt = null; if (user != null) { prompt = msg + " for " + user + ": "; } else { prompt = msg + ": "; } return console.readPassword(prompt); } /** * Prints the values of the given key from the DB. */ private void printAllValuesForKey(String key) throws Exception { List<ConfigKey> keysForName = getConfigDao().getKeysForName(key); if (keysForName.size() == 0) { log.debug("Failed to fetch value for key \"{}\", no such entry with default version.", key); throw new RuntimeException("Error fetching " + key + " value: no such entry with default version."); } for (ConfigKey configKey : keysForName) { console.write(key); console.write(": "); if (!configKey.isPasswordKey()) { console.write(configKey.getDisplayValue()); } else { char[] value = configKey.getDisplayValue().toCharArray(); console.writePassword(value); Arrays.fill(value, '\0'); } console.write(" "); console.write("version: "); console.write(configKey.getVersion()); console.writeLine(); } } /** * Prints all configuration values. Is the actual execution of the 'get-all' action ('-a', '--all') */ private void printAllValues() { List<ConfigurationNode> configNodes = keysConfig.getRootNode().getChildren(); for (ConfigurationNode node : configNodes) { ConfigKey key = configKeyFactory.generateByPropertiesKey(node.getName()); // TODO - move to one statement for all - time permitting; try { printAllValuesForKey(key.getKey()); } catch (Exception exception) { log.error("Error while retrieving value for key \"{}\".", key.getKey(), exception); } } } /** * Prints all available configuration keys. */ public void printAvailableKeys() { iterateAllKeys(configKeyFactory, keysConfig, key -> { printKeyInFormat(key); return true; }); } public static boolean iterateAllKeys(ConfigKeyFactory factory, HierarchicalConfiguration config, ConfigKeyHandler handler) { List<ConfigurationNode> configNodes = config.getRootNode().getChildren(); for (ConfigurationNode node : configNodes) { ConfigKey key = factory.generateByPropertiesKey(node.getName()); if (!handler.handle(key)) { return true; } } return false; } /** * Prints all reloadable configuration keys. Is the actual execution of the 'list' action ('-l', '--list') with the * --only-reloadable flag */ public void printReloadableKeys() { List<ConfigurationNode> configNodes = keysConfig.getRootNode().getChildren(); for (ConfigurationNode node : configNodes) { ConfigKey key = configKeyFactory.generateByPropertiesKey(node.getName()); if (key.isReloadable()) { printKeyInFormat(key); } } } private void printKeyInFormat(ConfigKey key) { console.writeFormat( "%s: %s (Value Type: %s)\n", key.getKey(), key.getDescription(), key.getType() ); } /** * If a version has been given, prints the specific value for the key and version, otherwise prints all the values * for the key. Is the actual execution of the 'get' action ('-g', '--get') */ private void printKey() throws Exception { String key = parser.getKey(); String version = parser.getVersion(); if (StringUtils.isBlank(version)) { ConfigKey configKey = getConfigKey(key); if (configKey == null) { throw new RuntimeException("Error fetching " + key + " value: no such entry. Please verify key name and property file support."); } testIfConfigKeyCanBeFetchedOrPrinted(configKey); printAllValuesForKey(configKey.getKey()); } else { printKeyWithSpecifiedVersion(key, version); } } /** * Fetches the given key with the given version from the DB and prints it. */ private void printKeyWithSpecifiedVersion(String key, String version) throws Exception { ConfigKey configKey = fetchConfigKey(key, version); if (configKey == null || configKey.getKey() == null) { log.debug("getValue: error fetching {} value: no such entry with version '{}'.", key, version); throw new RuntimeException("Error fetching " + key + " value: no such entry with version '" + version + "'."); } if (configKey.isPasswordKey()) { console.writePassword(configKey.getDisplayValue().toCharArray()); } else { console.write(configKey.getDisplayValue()); } console.writeLine(); } /** * Sets the value of the given key for the given version. Is the actual execution of the 'set' action ('-s', * '--set') */ private void persistValue() throws Exception { String key = parser.getKey(); String value = parser.getValue(); String version = parser.getVersion(); if (version == null) { version = startVersionDialog(key); } boolean sucessUpdate = persist(key, value, version); if (!sucessUpdate) { log.debug("setValue: error setting {}'s value. No such entry{}{}.", key, version == null ? "" : " with version ", version); throw new IllegalArgumentException("Error setting " + key + "'s value. No such entry" + (version == null ? "" : " with version " + version) + "."); } } /** * Concatenates the value of the given key for the given version. Is the actual execution of the * 'merge' action ('-m', '--merge') */ private void mergeValue() throws Exception { String key = parser.getKey(); String value = parser.getValue(); if(!keysConfig.getBoolean(key + "/" + MERGABLE_TOKEN, false)) { console.writeFormat(MERGE_NOT_SUPPORTED_MSG, key); console.writeLine(); return; } String version = parser.getVersion(); if (version == null) { version = startVersionDialog(key); } ConfigKey configKey = fetchConfigKey(key, version); if (configKey != null && configKey.getKey() != null && configKey.getDisplayValue().trim().length() > 0) { String valueInDb = configKey.getDisplayValue().trim(); String delimiter = keysConfig.getString(key + "/" + DELIMITER_TOKEN, ";"); value = mergedValues(value, valueInDb, delimiter); if(valueInDb.equals(value)) { console.writeFormat(MERGE_SAME_VALUE_MSG); console.writeLine(); return; } } if (!persist(key, value, version)) { String msg = MessageFormat.format(MERGE_PERSIST_ERR_MSG, key, version == null ? "" : " with version " + version); log.debug(msg); throw new IllegalArgumentException(msg); } } private String mergedValues(String valueToAppend, String currentValue, String delimiter) { String retValue = currentValue; for (String val : valueToAppend.split(delimiter)) { retValue = mergedValue(val, retValue, delimiter); } return retValue; } private String mergedValue(String valueToAppend, String currentValue, String delimiter) { StringBuilder retValue = new StringBuilder(currentValue); if (!Arrays.asList(currentValue.split(delimiter)).contains(valueToAppend)) { if (!currentValue.endsWith(delimiter)) { retValue.append(delimiter); } retValue.append(valueToAppend); } return retValue.toString(); } private void printHelpForKey() throws Exception { final String keyName = parser.getKey(); boolean foundKey = iterateAllKeys(this.configKeyFactory, keysConfig, key -> { if (key.getKey().equals(keyName)) { console.writeLine(key.getValueHelper().getHelpNote(key)); return false; } return true; }); if (!foundKey) { console.writeFormat(KEY_NOT_FOUND_ERR_MSG, keyName); } } /** * Is called when it is unclear which version is desired. If only one version exists for the given key, assumes that * is the desired version, if more than one exist, prompts the user to choose one. * * @param key * The version needs to be found for this key * @return A version for the given key */ private String startVersionDialog(String key) throws IOException, SQLException { log.debug("starting version dialog."); String version = null; List<ConfigKey> keys = configDao.getKeysForName(key); if (keys.size() == 1) { version = keys.get(0).getVersion(); } else if (keys.size() > 1) { while (true) { console.writeLine("Please select a version:"); for (int i = 0; i < keys.size(); i++) { console.writeFormat("%d. %s\n", i + 1, keys.get(i).getVersion()); } int index = 0; try { index = Integer.parseInt(console.readLine()); } catch (NumberFormatException e) { continue; } if (index >= 1 && index <= keys.size()) { version = keys.get(index - 1).getVersion(); break; } } } return version; } /** * Sets the given key with the given version to the given value. Is protected for test purposes. */ protected boolean persist(String key, String value, String version) throws IllegalAccessException { ConfigKey configKey = configKeyFactory.generateByPropertiesKey(key); configKey.setVersion(version); String message = null; boolean res = true; if (configKey.isDeprecated()) { throw new IllegalAccessError("Configuration key " + key + " is deprecated, thus it cannot be set."); } try { configKey.safeSetValue(value); res = getConfigDao().updateKey(configKey) == 1; } catch (InvalidParameterException ipe) { message = ipe.getMessage(); if (message == null) { message = "'" + value + "' is not a valid value for type " + configKey.getType() + ". " + (configKey.getValidValues().isEmpty() ? "" : "Valid values are " + configKey.getValidValues()); } } catch (Exception e) { message = "Error setting " + key + "'s value. " + e.getMessage(); log.debug("Error details: ", e); } if (message != null) { log.debug(message); throw new IllegalAccessException(message); } return res; } public boolean persist(String key, String value) throws Exception { return persist(key, value, ""); } private ConfigKey getConfigKey(String key) { ConfigKey ckReturn = null; ckReturn = configKeyFactory.generateByPropertiesKey(key); if (ckReturn == null || ckReturn.getKey() == null) { ckReturn = null; log.debug("getConfigKey: Unable to fetch the value of {}.", key); } return ckReturn; } public ConfigKey fetchConfigKey(String key, String version) { ConfigKey configKey = getConfigKey(key); if (configKey == null || configKey.getKey() == null) { log.debug("Unable to fetch the value of {} in version {}", key, version); return null; } testIfConfigKeyCanBeFetchedOrPrinted(configKey); configKey.setVersion(version); log.debug("Fetching key={} ver={}", configKey.getKey(), version); try { return getConfigDao().getKey(configKey); } catch (SQLException e) { return null; } } private void testIfConfigKeyCanBeFetchedOrPrinted(ConfigKey configKey) { if (configKey.isDeprecated()) { throw new IllegalAccessError("Configuration key " + configKey.getKey() + " is deprecated, thus cannot get its value."); } } public ConfigDao getConfigDao() { return configDao; } }