/* * 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 legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2008-2009 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. */ package org.opends.guitools.controlpanel.task; import static com.forgerock.opendj.cli.Utils.*; import static com.forgerock.opendj.util.OperatingSystem.*; import static org.opends.messages.AdminToolMessages.*; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.DirContext; import javax.naming.directory.ModificationItem; import javax.naming.ldap.InitialLdapContext; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.DN; import org.opends.admin.ads.util.ConnectionUtils; import org.opends.guitools.controlpanel.datamodel.ControlPanelInfo; import org.opends.guitools.controlpanel.datamodel.ServerDescriptor; import org.opends.guitools.controlpanel.event.ConfigurationElementCreatedEvent; import org.opends.guitools.controlpanel.event.ConfigurationElementCreatedListener; import org.opends.guitools.controlpanel.event.PrintStreamListener; import org.opends.guitools.controlpanel.ui.ColorAndFontConstants; import org.opends.guitools.controlpanel.ui.ProgressDialog; import org.opends.guitools.controlpanel.util.ApplicationPrintStream; import org.opends.guitools.controlpanel.util.ConfigReader; import org.opends.guitools.controlpanel.util.ProcessReader; import org.opends.guitools.controlpanel.util.Utilities; import org.opends.quicksetup.Installation; import org.opends.quicksetup.UserData; import org.opends.server.core.DirectoryServer; import org.opends.server.types.DirectoryException; import org.opends.server.types.HostPort; import org.opends.server.types.InitializationException; import org.opends.server.types.Schema; import org.opends.server.util.Base64; import org.opends.server.util.SetupUtils; import com.forgerock.opendj.cli.CommandBuilder; /** * The class used to define a number of common methods and mechanisms for the * tasks that are run in the Control Panel. */ public abstract class Task { private static String localHostName = UserData.getDefaultHostName(); private String binDir; /** The different task types. */ public enum Type { /** New Base DN creation. */ NEW_BASEDN, /** New index creation. */ NEW_INDEX, /** Modification of indexes. */ MODIFY_INDEX, /** Deletion of indexes. */ DELETE_INDEX, /** Creation of VLV indexes. */ NEW_VLV_INDEX, /** Modification of VLV indexes. */ MODIFY_VLV_INDEX, /** Deletion of VLV indexes. */ DELETE_VLV_INDEX, /** Import of an LDIF file. */ IMPORT_LDIF, /** Export of an LDIF file. */ EXPORT_LDIF, /** Backup. */ BACKUP, /** Restore. */ RESTORE, /** Verification of indexes. */ VERIFY_INDEXES, /** Rebuild of indexes. */ REBUILD_INDEXES, /** Enabling of Windows Service. */ ENABLE_WINDOWS_SERVICE, /** Disabling of Windows Service. */ DISABLE_WINDOWS_SERVICE, /** Starting the server. */ START_SERVER, /** Stopping the server. */ STOP_SERVER, /** Updating the java settings for the different command-lines. */ JAVA_SETTINGS_UPDATE, /** Creating a new element in the schema. */ NEW_SCHEMA_ELEMENT, /** Deleting an schema element. */ DELETE_SCHEMA_ELEMENT, /** Modify an schema element. */ MODIFY_SCHEMA_ELEMENT, /** Modifying an entry. */ MODIFY_ENTRY, /** Creating an entry. */ NEW_ENTRY, /** Deleting an entry. */ DELETE_ENTRY, /** Deleting a base DN. */ DELETE_BASEDN, /** Deleting a backend. */ DELETE_BACKEND, /** Other task. */ OTHER } /** The state on which the task can be. */ public enum State { /** The task is not started. */ NOT_STARTED, /** The task is running. */ RUNNING, /** The task finished successfully. */ FINISHED_SUCCESSFULLY, /** The task finished with error. */ FINISHED_WITH_ERROR } /** * Returns the names of the backends that are affected by the task. * @return the names of the backends that are affected by the task. */ public abstract Set getBackends(); /** The current state of the task. */ protected State state = State.NOT_STARTED; /** The return code of the task. */ protected Integer returnCode; /** The last exception encountered during the task execution. */ protected Throwable lastException; /** * The progress logs of the task. Note that the user of StringBuffer is not * a bug, because of the way the contents of logs is updated, using * StringBuffer instead of StringBuilder is required. */ private StringBuffer logs = new StringBuffer(); /** The error logs of the task. */ private StringBuilder errorLogs = new StringBuilder(); /** The standard output logs of the task. */ private StringBuilder outputLogs = new StringBuilder(); /** The print stream for the error logs. */ protected ApplicationPrintStream errorPrintStream = new ApplicationPrintStream(); /** The print stream for the standard output logs. */ protected ApplicationPrintStream outPrintStream = new ApplicationPrintStream(); /** * The process (if any) that the task launched. For instance if this is a * start server task, the process generated executing the start-ds * command-line. */ private Process process; private ControlPanelInfo info; private ServerDescriptor server; private ProgressDialog progressDialog; private ArrayList confListeners = new ArrayList<>(); private static int MAX_BINARY_LENGTH_TO_DISPLAY = 1024; /** * Constructor of the task. * @param info the control panel information. * @param progressDialog the progress dialog where the task progress will be * displayed. */ protected Task(ControlPanelInfo info, ProgressDialog progressDialog) { this.info = info; this.progressDialog = progressDialog; outPrintStream.addListener(new PrintStreamListener() { /** * Add a new line to the logs. * @param msg the new line. */ @Override public void newLine(String msg) { outputLogs.append(msg).append("\n"); logs.append(msg).append("\n"); } }); errorPrintStream.addListener(new PrintStreamListener() { /** * Add a new line to the error logs. * @param msg the new line. */ @Override public void newLine(String msg) { errorLogs.append(msg).append("\n"); logs.append(msg).append("\n"); } }); server = info.getServerDescriptor(); } /** * Returns the ControlPanelInfo object. * @return the ControlPanelInfo object. */ public ControlPanelInfo getInfo() { return info; } /** * Stops the pooling and initializes the configuration. * * @throws DirectoryException * if the configuration cannot be deregistered * @throws InitializationException * if a problem occurs during configuration initialization */ protected void stopPoolingAndInitializeConfiguration() throws DirectoryException, InitializationException { getInfo().stopPooling(); if (getInfo().mustDeregisterConfig()) { DirectoryServer.deregisterBaseDN(DN.valueOf("cn=config")); } DirectoryServer.getInstance().initializeConfiguration(ConfigReader.configFile); getInfo().setMustDeregisterConfig(true); } /** * Initializes the configuration and starts the pooling. * * @throws InitializationException * if a problem occurs during configuration initialization */ protected void startPoolingAndInitializeConfiguration() throws InitializationException { DirectoryServer.getInstance().initializeConfiguration(ConfigReader.configFile); getInfo().startPooling(); } /** * Returns the logs of the task. * @return the logs of the task. */ public String getLogs() { return logs.toString(); } /** * Returns the error logs of the task. * @return the error logs of the task. */ public String getErrorLogs() { return errorLogs.toString(); } /** * Returns the output logs of the task. * @return the output logs of the task. */ public String getOutputLogs() { return outputLogs.toString(); } /** * Returns the state of the task. * @return the state of the task. */ public State getState() { return state; } /** * Returns last exception encountered during the task execution. * Returns null if no exception was found. * @return last exception encountered during the task execution. */ public Throwable getLastException() { return lastException; } /** * Returns the return code (this makes sense when the task launches a * command-line, it will return the error code returned by the command-line). * @return the return code. */ public Integer getReturnCode() { return returnCode; } /** * Returns the process that the task launched. * Returns null if not process was launched. * @return the process that the task launched. */ public Process getProcess() { return process; } /** * Returns the progress dialog. * @return the progress dialog. */ protected ProgressDialog getProgressDialog() { return progressDialog; } /** * Tells whether a new server descriptor should be regenerated when the task * is over. If the task has an influence in the configuration or state of * the server (for instance the creation of a base DN) this method should * return true so that the configuration will be re-read and * all the ConfigChangeListeners will receive a notification with the new * configuration. * @return true if a new server descriptor must be regenerated * when the task is over and false otherwise. */ public boolean regenerateDescriptor() { return true; } /** * Method that is called when everything is finished after updating the * progress dialog. It is called from the event thread. */ public void postOperation() { // no-op } /** * The description of the task. It is used in both the incompatibility * messages and in the warning message displayed when the user wants to * quit and there are tasks running. * @return the description of the task. */ public abstract LocalizableMessage getTaskDescription(); /** * Adds a configuration element created listener. * @param listener the listener. */ public void addConfigurationElementCreatedListener( ConfigurationElementCreatedListener listener) { confListeners.add(listener); } /** * Removes a configuration element created listener. * @param listener the listener. */ public void removeConfigurationElementCreatedListener( ConfigurationElementCreatedListener listener) { confListeners.remove(listener); } /** * Notifies the configuration element created listener that a new object has * been created. * @param configObject the created object. */ protected void notifyConfigurationElementCreated(Object configObject) { for (ConfigurationElementCreatedListener listener : confListeners) { listener.elementCreated( new ConfigurationElementCreatedEvent(this, configObject)); } } /** * Returns a String representation of a value. In general this is called * to display the command-line equivalent when we do a modification in an * entry. But since some attributes must be obfuscated (like the user * password) we pass through this method. * @param attrName the attribute name. * @param o the attribute value. * @return the obfuscated String representing the attribute value to be * displayed in the logs of the user. */ protected String obfuscateAttributeStringValue(String attrName, Object o) { if (Utilities.mustObfuscate(attrName, getInfo().getServerDescriptor().getSchema())) { return OBFUSCATED_VALUE; } else if (o instanceof byte[]) { byte[] bytes = (byte[])o; if (displayBase64(attrName)) { if (bytes.length > MAX_BINARY_LENGTH_TO_DISPLAY) { return INFO_CTRL_PANEL_VALUE_IN_BASE64.get().toString(); } else { return Base64.encode(bytes); } } else { if (bytes.length > MAX_BINARY_LENGTH_TO_DISPLAY) { return INFO_CTRL_PANEL_BINARY_VALUE.get().toString(); } else { // Get the String value ByteString v = ByteString.wrap(bytes); return v.toString(); } } } else { return String.valueOf(o); } } /** * Obfuscates (if required) the attribute value in an LDIF line. * @param line the line of the LDIF file that must be treated. * @return the line obfuscated. */ protected String obfuscateLDIFLine(String line) { int index = line.indexOf(":"); if (index != -1) { String attrName = line.substring(0, index).trim(); if (Utilities.mustObfuscate(attrName, getInfo().getServerDescriptor().getSchema())) { return attrName + ": " + OBFUSCATED_VALUE; } } return line; } /** * Executes a command-line synchronously. * @param commandLineName the command line full path. * @param args the arguments for the command-line. * @return the error code returned by the command-line. */ protected int executeCommandLine(String commandLineName, String[] args) { returnCode = -1; String[] cmd = new String[args.length + 1]; cmd[0] = commandLineName; System.arraycopy(args, 0, cmd, 1, args.length); ProcessBuilder pb = new ProcessBuilder(cmd); // Use the java args in the script. Map env = pb.environment(); //env.put(SetupUtils.OPENDJ_JAVA_ARGS, ""); env.remove(SetupUtils.OPENDJ_JAVA_ARGS); env.remove("CLASSPATH"); ProcessReader outReader = null; ProcessReader errReader = null; try { process = pb.start(); outReader = new ProcessReader(process, outPrintStream, false); errReader = new ProcessReader(process, errorPrintStream, true); outReader.startReading(); errReader.startReading(); returnCode = process.waitFor(); } catch (Throwable t) { lastException = t; } finally { if (outReader != null) { outReader.interrupt(); } if (errReader != null) { errReader.interrupt(); } } return returnCode; } /** * Informs of whether the task to be launched can be launched or not. Every * task must implement this method so that we avoid launching in paralel two * tasks that are not compatible. Note that in general if the current task * is not running this method will return true. * * @param taskToBeLaunched the Task that we are trying to launch. * @param incompatibilityReasons the list of incompatibility reasons that * must be updated. * @return true if the task that we are trying to launch can be * launched in parallel with this task and false otherwise. */ public abstract boolean canLaunch(Task taskToBeLaunched, Collection incompatibilityReasons); /** Execute the task. This method is synchronous. */ public abstract void runTask(); /** * Returns the type of the task. * @return the type of the task. */ public abstract Type getType(); /** * Returns the binary/script directory. * @return the binary/script directory. */ protected String getBinaryDir() { if (binDir == null) { File f = Installation.getLocal().getBinariesDirectory(); try { binDir = f.getCanonicalPath(); } catch (Throwable t) { binDir = f.getAbsolutePath(); } if (binDir.lastIndexOf(File.separatorChar) != binDir.length() - 1) { binDir += File.separatorChar; } } return binDir; } /** * Check whether the provided task and this task run on the same server. * @param task the task the task to be analyzed. * @return true if both tasks run on the same server and * false otherwise. */ protected boolean runningOnSameServer(Task task) { if (getServer().isLocal() && task.getServer().isLocal()) { return true; } // Compare the host name and the instance path. This is safer than // comparing ports: we might be running locally on a stopped instance with // the same configuration as a "remote" (though located on the same machine) server. String host1 = getServer().getHostname(); String host2 = task.getServer().getHostname(); boolean runningOnSameServer = host1 == null ? host2 == null : host1.equalsIgnoreCase(host2); if (runningOnSameServer) { String f1 = getServer().getInstancePath(); String f2 = task.getServer().getInstancePath(); return Objects.equals(f1, f2); } return runningOnSameServer; } /** * Returns the server descriptor on which the task was launched. * @return the server descriptor on which the task was launched. */ public ServerDescriptor getServer() { return server; } /** * Returns the full path of the command-line associated with this task or * null if there is not a command-line (or a single command-line) * associated with the task. * @return the full path of the command-line associated with this task. */ protected abstract String getCommandLinePath(); /** * Returns the full path of the command-line for a given script name. * @param scriptBasicName the script basic name (with no extension). * @return the full path of the command-line for a given script name. */ protected String getCommandLinePath(String scriptBasicName) { if (isWindows()) { return getBinaryDir() + scriptBasicName + ".bat"; } return getBinaryDir() + scriptBasicName; } /** * Returns the list of command-line arguments. * @return the list of command-line arguments. */ protected abstract List getCommandLineArguments(); /** * Returns the list of obfuscated command-line arguments. This is called * basically to display the equivalent command-line to the user. * @param clearArgs the arguments in clear. * @return the list of obfuscated command-line arguments. */ protected List getObfuscatedCommandLineArguments(List clearArgs) { String[] toObfuscate = { "--bindPassword", "--currentPassword", "--newPassword" }; ArrayList args = new ArrayList<>(clearArgs); for (int i=1; i getConfigCommandLineArguments() { return Arrays.asList("--configFile", ConfigReader.configFile); } /** * Returns the list of arguments related to the connection (host, port, bind * DN, etc.). * @return the list of arguments related to the connection. */ protected List getConnectionCommandLineArguments() { return getConnectionCommandLineArguments(true, false); } /** * Returns the list of arguments related to the connection (host, port, bind * DN, etc.). * @param useAdminConnector use the administration connector to generate * the command line. * @param addConnectionTypeParameters add the connection type parameters * (--useSSL or --useStartTLS parameters: for ldapadd, ldapdelete, etc.). * @return the list of arguments related to the connection. */ protected List getConnectionCommandLineArguments( boolean useAdminConnector, boolean addConnectionTypeParameters) { ArrayList args = new ArrayList<>(); InitialLdapContext ctx; if (useAdminConnector) { ctx = getInfo().getConnection().getLdapContext(); } else { ctx = getInfo().getUserDataDirContext(); } if (isServerRunning() && ctx != null) { HostPort hostPort = ConnectionUtils.getHostPort(ctx); String hostName = localHostName; if (hostName == null || !getInfo().getServerDescriptor().isLocal()) { hostName = hostPort.getHost(); } boolean isSSL = ConnectionUtils.isSSL(ctx); boolean isStartTLS = ConnectionUtils.isStartTLS(ctx); String bindDN = ConnectionUtils.getBindDN(ctx); String bindPwd = ConnectionUtils.getBindPassword(ctx); args.add("--hostName"); args.add(hostName); args.add("--port"); args.add(String.valueOf(hostPort.getPort())); args.add("--bindDN"); args.add(bindDN); args.add("--bindPassword"); args.add(bindPwd); if (isSSL || isStartTLS) { args.add("--trustAll"); } if (isSSL && addConnectionTypeParameters) { args.add("--useSSL"); } else if (isStartTLS && addConnectionTypeParameters) { args.add("--useStartTLS"); } } return args; } /** * Returns the noPropertiesFile argument. * @return the noPropertiesFile argument. */ protected String getNoPropertiesFileArgument() { return "--noPropertiesFile"; } /** * Returns the command-line to be displayed (when we display the equivalent * command-line). * @return the command-line to be displayed. */ public String getCommandLineToDisplay() { String cmdLineName = getCommandLinePath(); if (cmdLineName != null) { List args = getObfuscatedCommandLineArguments(getCommandLineArguments()); args.removeAll(getConfigCommandLineArguments()); return getEquivalentCommandLine(cmdLineName, args); } return null; } /** * Commodity method to know if the server is running or not. * @return true if the server is running and false * otherwise. */ protected boolean isServerRunning() { return getInfo().getServerDescriptor().getStatus() == ServerDescriptor.ServerStatus.STARTED; } /** * Returns the print stream for the error logs. * @return the print stream for the error logs. */ public ApplicationPrintStream getErrorPrintStream() { return errorPrintStream; } /** * Returns the print stream for the output logs. * @return the print stream for the output logs. */ public ApplicationPrintStream getOutPrintStream() { return outPrintStream; } /** * Prints the equivalent modify command line in the progress dialog. * @param dn the dn of the modified entry. * @param mods the modifications. * @param useAdminCtx use the administration connector. */ protected void printEquivalentCommandToModify(DN dn, Collection mods, boolean useAdminCtx) { printEquivalentCommandToModify(dn.toString(), mods, useAdminCtx); } /** * Prints the equivalent modify command line in the progress dialog. * @param dn the dn of the modified entry. * @param mods the modifications. * @param useAdminCtx use the administration connector. */ protected void printEquivalentCommandToModify(String dn, Collection mods, boolean useAdminCtx) { ArrayList args = new ArrayList<>(getObfuscatedCommandLineArguments( getConnectionCommandLineArguments(useAdminCtx, true))); args.add(getNoPropertiesFileArgument()); String equiv = getEquivalentCommandLine(getCommandLinePath("ldapmodify"), args); StringBuilder sb = new StringBuilder(); sb.append(INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_MODIFY.get()).append("
"); sb.append(equiv); sb.append("
"); sb.append("dn: ").append(dn); boolean firstChangeType = true; for (ModificationItem mod : mods) { if (firstChangeType) { sb.append("
changetype: modify
"); } else { sb.append("-
"); } firstChangeType = false; Attribute attr = mod.getAttribute(); String attrName = attr.getID(); if (mod.getModificationOp() == DirContext.ADD_ATTRIBUTE) { sb.append("add: ").append(attrName).append("
"); } else if (mod.getModificationOp() == DirContext.REPLACE_ATTRIBUTE) { sb.append("replace: ").append(attrName).append("
"); } else { sb.append("delete: ").append(attrName).append("
"); } for (int i=0; i"); } catch (NamingException ne) { // Bug throw new RuntimeException( "Unexpected error parsing modifications: "+ne, ne); } } } sb.append("


"); getProgressDialog().appendProgressHtml(Utilities.applyFont( sb.toString(), ColorAndFontConstants.progressFont)); } /** The separator used to link the lines of the resulting command-lines. */ private static final String LINE_SEPARATOR = CommandBuilder.HTML_LINE_SEPARATOR; /** * Returns the equivalent command line in HTML without font properties. * @param cmdName the command name. * @param args the arguments for the command line. * @return the equivalent command-line in HTML. */ public static String getEquivalentCommandLine(String cmdName, List args) { StringBuilder sb = new StringBuilder(cmdName); for (String arg : args) { if (arg.charAt(0) == '-') { sb.append(LINE_SEPARATOR); } else { sb.append(" "); } sb.append(CommandBuilder.escapeValue(arg)); } return sb.toString(); } /** * Prints the equivalent command line. * @param cmdName the command name. * @param args the arguments for the command line. * @param msg the message associated with the command line. */ protected void printEquivalentCommandLine(String cmdName, List args, LocalizableMessage msg) { getProgressDialog().appendProgressHtml(Utilities.applyFont(msg+"
"+ getEquivalentCommandLine(cmdName, args)+"

", ColorAndFontConstants.progressFont)); } /** * Tells whether the provided attribute's values must be displayed using * base 64 when displaying the equivalent command-line or not. * @param attrName the attribute name. * @return true if the attribute must be displayed using base 64 * and false otherwise. */ protected boolean displayBase64(String attrName) { Schema schema = null; if (getInfo() != null) { schema = getInfo().getServerDescriptor().getSchema(); } return Utilities.hasBinarySyntax(attrName, schema); } /** * Prints the equivalent rename command line in the progress dialog. * @param oldDN the old DN of the entry. * @param newDN the new DN of the entry. * @param useAdminCtx use the administration connector. */ protected void printEquivalentRenameCommand(DN oldDN, DN newDN, boolean useAdminCtx) { ArrayList args = new ArrayList<>(getObfuscatedCommandLineArguments( getConnectionCommandLineArguments(useAdminCtx, true))); args.add(getNoPropertiesFileArgument()); String equiv = getEquivalentCommandLine(getCommandLinePath("ldapmodify"), args); StringBuilder sb = new StringBuilder(); sb.append(INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_RENAME.get()).append("
"); sb.append(equiv); sb.append("
"); sb.append("dn: ").append(oldDN); sb.append("
"); sb.append("changetype: moddn
"); sb.append("newrdn: ").append(newDN.rdn()).append("
"); sb.append("deleteoldrdn: 1"); sb.append("


"); getProgressDialog().appendProgressHtml( Utilities.applyFont(sb.toString(), ColorAndFontConstants.progressFont)); } /** * Returns the incompatible message between two tasks. * @param taskRunning the task that is running. * @param taskToBeLaunched the task that we are trying to launch. * @return the incompatible message between two tasks. */ protected LocalizableMessage getIncompatibilityMessage(Task taskRunning, Task taskToBeLaunched) { return INFO_CTRL_PANEL_INCOMPATIBLE_TASKS.get( taskRunning.getTaskDescription(), taskToBeLaunched.getTaskDescription()); } }