/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2011-2015 ForgeRock AS.
*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at
* http://forgerock.org/license/CDDLv1.0.html
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at http://forgerock.org/license/CDDLv1.0.html
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*/
package org.forgerock.openidm.shell.impl;
import java.io.File;
import java.io.FileFilter;
import java.io.InputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.felix.service.command.CommandSession;
import org.apache.felix.service.command.Descriptor;
import org.apache.felix.service.command.Parameter;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.Requests;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourcePath;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.openidm.config.persistence.ConfigBootstrapHelper;
import org.forgerock.openidm.core.IdentityServer;
import org.forgerock.openidm.shell.CustomCommandScope;
import org.forgerock.openidm.shell.felixgogo.MetaVar;
import org.forgerock.services.context.RootContext;
/**
* Command scope for remote operations.
*/
public class RemoteCommandScope extends CustomCommandScope {
private static final String DOTTED_PLACEHOLDER = "............................................";
private static final String IDM_PORT_DEFAULT = "8080";
private static final String IDM_PORT_DESC =
"Port of OpenIDM REST service. This will override any port in --url. Default " + IDM_PORT_DEFAULT;
private static final String IDM_PORT_METAVAR = "PORT";
private static final String IDM_URL_DEFAULT = "http://localhost:8080/openidm/";
private static final String IDM_URL_DESC = "URL of OpenIDM REST service. Default " + IDM_URL_DEFAULT;
private static final String IDM_URL_METAVAR = "URL";
private static final String USER_PASS_DESC = "Server user and password";
private static final String USER_PASS_METAVAR = "USER[:PASSWORD]";
private static final String USER_PASS_DEFAULT = "";
private static final String REPLACE_ALL_DESC =
"Replace the entire config set by deleting the additional configuration";
private final HttpRemoteJsonResource resource = new HttpRemoteJsonResource();
private final ObjectMapper mapper = new ObjectMapper();
/**
* {@inheritDoc}
*/
public Map<String, String> getFunctionMap() {
Map<String, String> help = new HashMap<String, String>();
help.put("configimport", getLongHeader("configimport"));
help.put("configexport", getLongHeader("configexport"));
help.put("configureconnector", getLongHeader("configureconnector"));
help.put("update", getLongHeader("update"));
return help;
}
/**
* {@inheritDoc}
*/
public String getScope() {
return "remote";
}
private void processOptions(final String userPass, final String idmUrl, final String idmPort) {
String username = "";
String password = "";
if (StringUtils.isNotBlank(userPass)) {
int passwordIdx = userPass.indexOf(":") + 1;
if (passwordIdx > 0) {
username = userPass.substring(0, passwordIdx - 1);
password = userPass.substring(passwordIdx);
} else {
username = userPass;
password = new String(System.console().readPassword("Password:"));
}
resource.setUsername(username);
resource.setPassword(password);
}
if (StringUtils.isNotBlank(idmUrl)) {
resource.setBaseUri(idmUrl.endsWith("/") ? idmUrl : idmUrl + "/");
}
if (StringUtils.isNotBlank(idmPort)) {
if (NumberUtils.isDigits(idmPort)) {
resource.setPort(Integer.decode(idmPort));
} else {
throw new IllegalArgumentException("Port must be a number");
}
}
}
/**
* Handles the Update (upgrade/patch) process.
* @param session session that invoked the command.
* @param userPass user:passwd to be used on rest calls.
* @param idmUrl url to the idm instance.
* @param idmPort port that idm is running on.
* @param acceptLicense if true, the accept license step is skipped.
* @param archive The simple file name of the archive that is already in bin/update.
*/
@Descriptor("Update the system with the provided update file.")
public void update(CommandSession session,
@Descriptor(USER_PASS_DESC)
@MetaVar(USER_PASS_METAVAR)
@Parameter(names = {"-u", "--user"}, absentValue = USER_PASS_DEFAULT)
final String userPass,
@Descriptor(IDM_URL_DESC)
@MetaVar(IDM_URL_METAVAR)
@Parameter(names = {"--url"}, absentValue = IDM_URL_DEFAULT)
final String idmUrl,
@Descriptor(IDM_PORT_DESC)
@MetaVar(IDM_PORT_METAVAR)
@Parameter(names = {"-P", "--port"}, absentValue = IDM_PORT_DEFAULT)
final String idmPort,
@Descriptor("Automatically accepts the product license (if present). " +
"Defaults to 'false' to ask for acceptance.")
@Parameter(names = {"--acceptLicense"}, presentValue = "true", absentValue = "false")
final boolean acceptLicense,
@Descriptor("Timeout value to wait for jobs to finish. " +
"Defaults to -1 to exit immediately if jobs are running.")
@MetaVar("TIME")
@Parameter(names = {"--maxJobsFinishWaitTimeMs"}, absentValue = "-1")
final long maxJobsFinishWaitTimeMs,
@Descriptor("Timeout value to wait for update process to complete. Defaults to 30000 ms.")
@MetaVar("TIME")
@Parameter(names = {"--maxUpdateWaitTimeMs"}, absentValue = "30000")
final long maxUpdateWaitTimeMs,
@Descriptor("Log file path. (optional) Defaults to logs/update.log")
@MetaVar("LOG_FILE")
@Parameter(names = {"-l", "--log"}, absentValue = "logs/update.log")
final String logFilePath,
@Descriptor("Log only to the log file.")
@Parameter(names = {"-Q", "--Quiet"}, presentValue = "true", absentValue = "false")
final boolean quietMode,
@Descriptor("Filename of the Update archive within bin/update.")
String archive) {
processOptions(userPass, idmUrl, idmPort);
UpdateCommandConfig config = new UpdateCommandConfig()
.setUpdateArchive(archive)
.setLogFilePath(logFilePath)
.setQuietMode(quietMode)
.setAcceptedLicense(acceptLicense)
.setMaxJobsFinishWaitTimeMs(maxJobsFinishWaitTimeMs)
.setMaxUpdateWaitTimeMs(maxUpdateWaitTimeMs);
new UpdateCommand(session, resource, config)
.execute(new RootContext());
}
/**
* Import the configuration set from a local "conf" directory.
*
* @param session the command session
* @param userPass the username/password
* @param idmUrl the url of the OpenIDM instance
* @param idmPort the OpenIDM instance's port
* @param replaceall whether or not to replace the config
*/
@Descriptor("Imports the configuration set from local 'conf' directory.")
public void configimport(
final CommandSession session,
@Descriptor(USER_PASS_DESC)
@MetaVar(USER_PASS_METAVAR)
@Parameter(names = { "-u", "--user" }, absentValue = USER_PASS_DEFAULT)
final String userPass,
@Descriptor(IDM_URL_DESC)
@MetaVar(IDM_URL_METAVAR)
@Parameter(names = { "--url" }, absentValue = IDM_URL_DEFAULT)
final String idmUrl,
@Descriptor(IDM_PORT_DESC)
@MetaVar(IDM_PORT_METAVAR)
@Parameter(names = { "-P", "--port" }, absentValue = IDM_PORT_DEFAULT)
final String idmPort,
@Descriptor(REPLACE_ALL_DESC)
@Parameter(names = { "-r", "--replaceall", "--replaceAll" }, presentValue = "true", absentValue = "false")
final boolean replaceall) {
configimport(session, userPass, idmUrl, idmPort, replaceall, "conf");
}
/**
* Imports the configuration set from a local file or directory.
*
* @param session the command session
* @param userPass the username/password
* @param idmUrl the url of the OpenIDM instance
* @param idmPort the OpenIDM instance's port
* @param replaceall whether or not to replace the config
* @param source the source directory
*/
@Descriptor("Imports the configuration set from local file/directory.")
public void configimport(
final CommandSession session,
@Descriptor(USER_PASS_DESC)
@MetaVar(USER_PASS_METAVAR)
@Parameter(names = { "-u", "--user" }, absentValue = USER_PASS_DEFAULT)
final String userPass,
@Descriptor(IDM_URL_DESC)
@MetaVar(IDM_URL_METAVAR)
@Parameter(names = { "--url" }, absentValue = IDM_URL_DEFAULT)
final String idmUrl,
@Descriptor(IDM_PORT_DESC)
@MetaVar(IDM_PORT_METAVAR)
@Parameter(names = { "-P", "--port" }, absentValue = IDM_PORT_DEFAULT)
final String idmPort,
@Descriptor(REPLACE_ALL_DESC)
@Parameter(names = { "-r", "--replaceall", "--replaceAll" }, presentValue = "true", absentValue = "false")
final boolean replaceall,
@Descriptor("source directory")
final String source) {
processOptions(userPass, idmUrl, idmPort);
PrintStream console = session.getConsole();
File file = IdentityServer.getFileForPath(source);
console.println("...................................................................");
if (file.isDirectory()) {
console.println("[ConfigImport] Load JSON configuration files from:");
console.append("[ConfigImport] \t").println(file.getAbsolutePath());
FileFilter filter = new FileFilter() {
public boolean accept(File f) {
return f.getName().endsWith(".json");
}
};
// Read the files from the provided source directory.
File[] files = file.listFiles(filter);
Map<String, File> localConfigSet = new HashMap<String, File>(files.length);
for (File subFile : files) {
if (subFile.isDirectory()) {
continue;
}
String configName = subFile.getName().replaceFirst("-", "/");
configName = ConfigBootstrapHelper.unqualifyPid(configName.substring(0, configName.length() - 5));
if (configName.indexOf("-") > -1) {
console.append(
"[WARN] Invalid file name found with multiple '-' character. The normalized config id: ");
console.println(configName);
}
localConfigSet.put(configName, subFile);
}
// Read the remote configs that are currently active.
Map<String, JsonValue> remoteConfigSet = new HashMap<String, JsonValue>();
try {
ResourceResponse responseValue = resource.read(null, Requests.newReadRequest("config"));
Iterator<JsonValue> iterator = responseValue.getContent().get("configurations").iterator();
while (iterator.hasNext()) {
JsonValue configValue = iterator.next();
// TODO catch JsonValueExceptions
String id = ConfigBootstrapHelper.unqualifyPid(configValue.get("_id").required().asString());
// Remove apache configurations
if (!id.startsWith("org.apache")) {
remoteConfigSet.put(id, configValue);
}
}
} catch (ResourceException e) {
console.append("Remote operation failed: ").println(e.getMessage());
return;
} catch (Exception e) {
console.append("Operation failed: ").println(e.getMessage());
return;
}
final ResourcePath configResource = ResourcePath.valueOf("config");
for (Map.Entry<String, File> entry : localConfigSet.entrySet()) {
String sourceConfigId = entry.getKey();
try {
if (remoteConfigSet.containsKey(sourceConfigId)) {
// Update
UpdateRequest updateRequest = Requests.newUpdateRequest(configResource.concat(sourceConfigId),
new JsonValue(mapper.readValue(entry.getValue(), Map.class)));
resource.update(null, updateRequest);
// If the update succeeded, remove the entry from 'remoteConfigSet' - this prevents it
// from being deleted below. If this update fails, the entry will remain in remoteConfigSet
// and will be deleted from the remote IDM instance.
remoteConfigSet.remove(sourceConfigId);
} else {
// Create
CreateRequest createRequest = Requests.newCreateRequest(configResource.concat(sourceConfigId),
new JsonValue(mapper.readValue(entry.getValue(), Map.class)));
resource.create(null, createRequest);
}
prettyPrint(console, "ConfigImport", sourceConfigId, null);
} catch (Exception e) {
prettyPrint(console, "ConfigImport", sourceConfigId, e.getMessage());
}
}
// Delete all additional config objects
if (replaceall) {
for (String configId : remoteConfigSet.keySet()) {
if (isProtectedConfigId(configId)) {
prettyPrint(console, "ConfigDelete", configId, "Protected configuration can not be deleted");
continue;
}
try {
// configId is concatenated to avoid file paths from getting url encoded -> '/'-> '%2f'
resource.delete(null, Requests.newDeleteRequest(configResource.concat(configId)));
prettyPrint(console, "ConfigDelete", configId, null);
} catch (Exception e) {
prettyPrint(console, "ConfigDelete", configId, e.getMessage());
}
}
}
} else if (file.exists()) {
// TODO import archive file
console.println("Input path must be a directory not a file.");
} else {
console.append("[ConfigImport] Configuration directory not found at: ");
console.println(file.getAbsolutePath());
}
}
private boolean isProtectedConfigId(String configId) {
return "authentication".equals(configId)
|| "router".equals(configId)
|| "audit".equals(configId)
|| configId.startsWith("repo");
}
/**
* Exports all configuration to the "conf" folder.
*
* @param session the command session
* @param userPass the username/password
* @param idmUrl the url of the OpenIDM instance
* @param idmPort the OpenIDM instance's port
*/
@Descriptor("Exports all configurations to 'conf' folder.")
public void configexport(
CommandSession session,
@Descriptor(USER_PASS_DESC)
@MetaVar(USER_PASS_METAVAR)
@Parameter(names = { "-u", "--user" }, absentValue = USER_PASS_DEFAULT)
final String userPass,
@Descriptor(IDM_URL_DESC)
@MetaVar(IDM_URL_METAVAR)
@Parameter(names = { "--url" }, absentValue = IDM_URL_DEFAULT)
final String idmUrl,
@Descriptor(IDM_PORT_DESC)
@MetaVar(IDM_PORT_METAVAR)
@Parameter(names = { "-P", "--port" }, absentValue = IDM_PORT_DEFAULT)
final String idmPort) {
configexport(session, userPass, idmUrl, idmPort, "conf");
}
/**
* Exports all configurations.
*
* @param session the command session
* @param userPass the username/password
* @param idmUrl the url of the OpenIDM instance
* @param idmPort the OpenIDM instance's port
* @param target the target directory
*/
@Descriptor("Exports all configurations.")
public void configexport(
CommandSession session,
@Descriptor(USER_PASS_DESC)
@MetaVar(USER_PASS_METAVAR)
@Parameter(names = { "-u", "--user" }, absentValue = USER_PASS_DEFAULT)
final String userPass,
@Descriptor(IDM_URL_DESC)
@MetaVar(IDM_URL_METAVAR)
@Parameter(names = { "--url" }, absentValue = IDM_URL_DEFAULT)
final String idmUrl,
@Descriptor(IDM_PORT_DESC)
@MetaVar(IDM_PORT_METAVAR)
@Parameter(names = { "-P", "--port" }, absentValue = IDM_PORT_DEFAULT)
final String idmPort,
@Descriptor("target directory")
String target) {
processOptions(userPass, idmUrl, idmPort);
File targetDir = IdentityServer.getFileForPath(target);
if (!targetDir.exists()) {
targetDir.mkdirs();
}
session.getConsole().println("[ConfigExport] Export JSON configurations to:");
session.getConsole().append("[ConfigExport] \t").println(targetDir.getAbsolutePath());
try {
ResourceResponse responseValue = resource.read(null, Requests.newReadRequest("config"));
Iterator<JsonValue> iterator = responseValue.getContent().get("configurations").iterator();
String bkpPostfix = "." + (new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss")).format(new Date()) + ".bkp";
while (iterator.hasNext()) {
String id = iterator.next().get("_id").required().asString();
if (!id.startsWith("org.apache")) {
try {
responseValue = resource.read(null, Requests.newReadRequest("config/" + id));
if (null != responseValue.getContent()
&& !responseValue.getContent().isNull()) {
File configFile = new File(targetDir, id.replace("/", "-") + ".json");
if (configFile.exists()) {
configFile.renameTo(
new File(configFile.getParentFile(), configFile.getName() + bkpPostfix));
}
mapper.writerWithDefaultPrettyPrinter().writeValue(configFile,
responseValue.getContent().getObject());
prettyPrint(session.getConsole(), "ConfigExport", id, null);
}
} catch (Exception e) {
prettyPrint(session.getConsole(), "ConfigExport", id, e.getMessage());
}
}
}
} catch (ResourceException e) {
session.getConsole().append("Remote operation failed: ").println(e.getMessage());
} catch (Exception e) {
session.getConsole().append("Operation failed: ").println(e.getMessage());
}
}
private void export(InputStream console, PrintStream out, String[] args) {
out.println("Exported");
}
/**
* Generate connector configuration.
*
* @param session the command session
* @param userPass the username/password
* @param idmUrl the url of the OpenIDM instance
* @param idmPort the OpenIDM instance's port
* @param name the connector name
*/
@Descriptor("Generate connector configuration.")
public void configureconnector(
CommandSession session,
@Descriptor(USER_PASS_DESC)
@MetaVar(USER_PASS_METAVAR)
@Parameter(names = { "-u", "--user" }, absentValue = USER_PASS_DEFAULT)
final String userPass,
@Descriptor(IDM_URL_DESC)
@MetaVar(IDM_URL_METAVAR)
@Parameter(names = { "--url" }, absentValue = IDM_URL_DEFAULT)
final String idmUrl,
@Descriptor(IDM_PORT_DESC)
@MetaVar(IDM_PORT_METAVAR)
@Parameter(names = { "-P", "--port" }, absentValue = IDM_PORT_DEFAULT)
final String idmPort,
@Descriptor("Name of the new connector configuration.")
@MetaVar("CONNECTOR")
@Parameter(names = { "-n", "--name" }, absentValue = "test")
String name) {
processOptions(userPass, idmUrl, idmPort);
try {
// Prepare temp folder and file
File temp = IdentityServer.getFileForPath("temp");
if (!temp.exists()) {
temp.mkdir();
}
// TODO Make safe file name
if (StringUtils.isBlank(name) || !name.matches("\\w++")) {
session.getConsole().append("The given name \"").append(name).println(
"\" must match [a-zA-Z_0-9] pattern");
return;
}
File finalConfig = new File(temp, "provisioner.openicf-" + name + ".json");
// Common request attributes
ActionRequest request = Requests.newActionRequest("system", "CREATECONFIGURATION");
JsonValue responseValue;
Map<String, Object> configuration = null;
// Phase#1 - Get available connectors
if (!finalConfig.exists()) {
responseValue = resource.action(null, request).getJsonContent();
JsonValue connectorRef = responseValue.get("connectorRef");
if (!connectorRef.isNull() && connectorRef.isList()) {
int i = 0;
for (JsonValue connector : connectorRef) {
String displayName = connector.get("displayName").asString();
if (null == displayName) {
displayName = connector.get("connectorName").asString();
}
String version = connector.get("bundleVersion").asString();
String connectorHostRef = connector.get("connectorHostRef").asString();
session.getConsole().append(Integer.toString(i)).append(". ").append(displayName);
if (null != connectorHostRef) {
session.getConsole().append(" Remote (").append(connectorHostRef).append(")");
}
session.getConsole().append(" version ").println(version);
i++;
}
session.getConsole().append(Integer.toString(connectorRef.size())).println(". Exit");
Scanner input = new Scanner(session.getKeyboard());
int index = -1;
do {
session.getConsole()
.append("Select [0..")
.append(Integer.toString(connectorRef.size()))
.append("]: ");
index = input.nextInt();
} while (index < 0 || index > connectorRef.size());
if (index == connectorRef.size()) {
return;
}
configuration = responseValue.asMap();
// If we don't getObject() JsonValue will wrap the object resulting in a JSON payload of
// { "connectorRef": { ..., "wrappedObject": ... }
configuration.put("connectorRef", connectorRef.get(index).getObject());
} else {
session.getConsole().println("There are no available connector!");
}
} else {
configuration = new JsonValue(mapper.readValue(finalConfig, Map.class)).asMap();
session.getConsole().append("Configuration was found and read from: ")
.println(finalConfig.getAbsolutePath());
}
if (null == configuration) {
return;
}
// Repeatable phase #2 and #3
request.setContent(new JsonValue(configuration));
responseValue = resource.action(null, request).getJsonContent();
configuration = responseValue.asMap();
configuration.put("name", name);
mapper.writerWithDefaultPrettyPrinter().writeValue(finalConfig, configuration);
session.getConsole()
.append("Edit the configuration file and run the command again. The configuration was saved to ")
.println(finalConfig.getAbsolutePath());
} catch (ResourceException e) {
session.getConsole().append("Remote operation failed: ").println(e.getMessage());
} catch (Exception e) {
session.getConsole().append("Operation failed: ").println(e.getMessage());
}
}
private void prettyPrint(PrintStream out, String cmd, String name, String reason) {
out.append("[").append(cmd).append("] ").append(name).append(" ").append(
DOTTED_PLACEHOLDER.substring(Math.min(name.length(), DOTTED_PLACEHOLDER.length())));
if (null == reason) {
out.println(" SUCCESS");
} else {
out.println(" FAILED");
out.append("\t[").append(reason).println("]");
}
}
}