001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2007-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2012-2016 ForgeRock AS.
016 */
017package org.opends.server.tools.tasks;
018
019import static org.opends.messages.TaskMessages.*;
020import static org.opends.messages.ToolMessages.*;
021import static org.opends.server.util.StaticUtils.*;
022
023import static com.forgerock.opendj.cli.Utils.*;
024import static com.forgerock.opendj.cli.CommonArguments.*;
025
026import java.io.IOException;
027import java.io.PrintStream;
028import java.util.Date;
029import java.util.HashSet;
030import java.util.List;
031import java.util.Set;
032
033import org.forgerock.i18n.LocalizableMessage;
034import org.forgerock.opendj.ldap.DecodeException;
035import org.opends.server.admin.client.cli.TaskScheduleArgs;
036import org.opends.server.backends.task.FailedDependencyAction;
037import org.opends.server.backends.task.TaskState;
038import org.opends.server.core.DirectoryServer;
039import org.opends.server.loggers.JDKLogging;
040import org.opends.server.tools.LDAPConnection;
041import org.opends.server.tools.LDAPConnectionException;
042import org.opends.server.types.InitializationException;
043import org.opends.server.types.LDAPException;
044import org.opends.server.types.OpenDsException;
045import org.opends.server.util.BuildVersion;
046import org.opends.server.util.cli.LDAPConnectionArgumentParser;
047
048import com.forgerock.opendj.cli.Argument;
049import com.forgerock.opendj.cli.ArgumentException;
050import com.forgerock.opendj.cli.ArgumentGroup;
051import com.forgerock.opendj.cli.BooleanArgument;
052import com.forgerock.opendj.cli.ClientException;
053import com.forgerock.opendj.cli.StringArgument;
054
055/**
056 * Base class for tools that are capable of operating either by running
057 * local within this JVM or by scheduling a task to perform the same
058 * action running within the directory server through the tasks interface.
059 */
060public abstract class TaskTool implements TaskScheduleInformation {
061
062  /**
063   * Magic value used to indicate that the user would like to schedule
064   * this operation to run immediately as a task as opposed to running
065   * the operation in the local VM.
066   */
067  public static final String NOW = TaskScheduleArgs.NOW;
068
069  /**
070   * The error code used by the mixed-script to know if the java
071   * arguments for the off-line mode must be used.
072   */
073  private static final int RUN_OFFLINE = 51;
074  /**
075   * The error code used by the mixed-script to know if the java
076   * arguments for the on-line mode must be used.
077   */
078  private static final int RUN_ONLINE = 52;
079
080  /**
081   * Number of milliseconds this utility will wait before reloading
082   * this task's entry in the directory while it is polling for status.
083   */
084  private static final int SYNCHRONOUS_TASK_POLL_INTERVAL = 1000;
085
086  private LDAPConnectionArgumentParser argParser;
087
088  private TaskScheduleArgs taskScheduleArgs;
089
090  /**
091   * Argument used to know whether we must test if we must run in off-line mode.
092   */
093  private BooleanArgument testIfOfflineArg;
094
095  /** This CLI is always using the administration connector with SSL. */
096  private static final boolean alwaysSSL = true;
097
098  /**
099   * Called when this utility should perform its actions locally in this
100   * JVM.
101   *
102   * @param initializeServer indicates whether to initialize the
103   *        directory server in the case of a local action
104   * @param out stream to write messages; may be null
105   * @param err stream to write messages; may be null
106   * @return int indicating the result of this action
107   */
108  protected abstract int processLocal(boolean initializeServer,
109                                      PrintStream out,
110                                      PrintStream err);
111
112  /**
113   * Cleanup task environment after offline run.
114   * Default implementation does nothing.
115   * Tasks which initialize some static part of the DirectoryServer instance (e.g admin data local backends) must
116   * override this method to shutdown all needed component to prevent JVM environment alteration.
117   */
118  protected void cleanup()
119  {
120    // Do nothing by default.
121  }
122
123  /**
124   * Creates an argument parser prepopulated with arguments for processing
125   * input for scheduling tasks with the task backend.
126   *
127   * @param className of this tool
128   * @param toolDescription of this tool
129   * @return LDAPConnectionArgumentParser for processing CLI input
130   */
131  protected LDAPConnectionArgumentParser createArgParser(String className,
132    LocalizableMessage toolDescription)
133  {
134    ArgumentGroup ldapGroup = new ArgumentGroup(
135      INFO_DESCRIPTION_TASK_LDAP_ARGS.get(), 1001);
136
137    argParser = new LDAPConnectionArgumentParser(className,
138      toolDescription, false, ldapGroup, alwaysSSL);
139
140    ArgumentGroup taskGroup = new ArgumentGroup(
141      INFO_DESCRIPTION_TASK_TASK_ARGS.get(), 1000);
142
143    try {
144      StringArgument propertiesFileArgument =
145          propertiesFileArgument();
146      argParser.addArgument(propertiesFileArgument);
147      argParser.setFilePropertiesArgument(propertiesFileArgument);
148
149      BooleanArgument noPropertiesFileArgument =
150          noPropertiesFileArgument();
151      argParser.addArgument(noPropertiesFileArgument);
152      argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);
153
154      taskScheduleArgs = new TaskScheduleArgs();
155
156      for (Argument arg : taskScheduleArgs.getArguments())
157      {
158        argParser.addArgument(arg, taskGroup);
159      }
160
161      testIfOfflineArg =
162              BooleanArgument.builder("testIfOffline")
163                      .description(INFO_DESCRIPTION_TEST_IF_OFFLINE.get())
164                      .hidden()
165                      .buildAndAddToParser(argParser);
166    } catch (ArgumentException e) {
167      // should never happen
168    }
169
170    return argParser;
171  }
172
173  /**
174   * Validates arguments related to task scheduling.  This should be
175   * called after the <code>ArgumentParser.parseArguments</code> has
176   * been called.
177   *
178   * @throws ArgumentException if there is a problem with the arguments.
179   * @throws ClientException if there is a problem with one of the values provided
180   * by the user.
181   */
182  protected void validateTaskArgs() throws ArgumentException, ClientException
183  {
184    if (isRemoteTask())
185    {
186      taskScheduleArgs.validateArgs();
187    }
188    else
189    {
190      // server is offline => output logs to the console
191      JDKLogging.enableConsoleLoggingForOpenDJTool();
192      taskScheduleArgs.validateArgsIfOffline();
193    }
194  }
195
196  /** {@inheritDoc} */
197  @Override
198  public Date getStartDateTime() {
199    return taskScheduleArgs.getStartDateTime();
200  }
201
202  /** {@inheritDoc} */
203  @Override
204  public String getRecurringDateTime() {
205    return taskScheduleArgs.getRecurringDateTime();
206  }
207
208  /** {@inheritDoc} */
209  @Override
210  public List<String> getDependencyIds() {
211    return taskScheduleArgs.getDependencyIds();
212  }
213
214  /** {@inheritDoc} */
215  @Override
216  public FailedDependencyAction getFailedDependencyAction() {
217    return taskScheduleArgs.getFailedDependencyAction();
218  }
219
220  /** {@inheritDoc} */
221  @Override
222  public List<String> getNotifyUponCompletionEmailAddresses() {
223    return taskScheduleArgs.getNotifyUponCompletionEmailAddresses();
224  }
225
226  /** {@inheritDoc} */
227  @Override
228  public List<String> getNotifyUponErrorEmailAddresses() {
229    return taskScheduleArgs.getNotifyUponErrorEmailAddresses();
230  }
231
232  /**
233   * Either invokes initiates this tool's local action or schedule this
234   * tool using the tasks interface based on user input.
235   *
236   * @param argParser used to parse user arguments
237   * @param initializeServer indicates whether to initialize the
238   *        directory server in the case of a local action
239   * @param out stream to write messages; may be null
240   * @param err stream to write messages; may be null
241   * @return int indicating the result of this action
242   */
243  protected int process(LDAPConnectionArgumentParser argParser,
244                        boolean initializeServer,
245                        PrintStream out, PrintStream err) {
246    if (testIfOffline())
247    {
248      if (!isRemoteTask())
249      {
250        return RUN_OFFLINE;
251      }
252      else
253      {
254        return RUN_ONLINE;
255      }
256    }
257
258    if (!isRemoteTask())
259    {
260      try
261      {
262        return processLocal(initializeServer, out, err);
263      }
264      finally
265      {
266        if (initializeServer)
267        {
268          cleanup();
269        }
270      }
271    }
272
273    if (initializeServer)
274    {
275      try
276      {
277        DirectoryServer.bootstrapClient();
278        DirectoryServer.initializeJMX();
279      }
280      catch (Exception e)
281      {
282        printWrappedText(err, ERR_SERVER_BOOTSTRAP_ERROR.get(getExceptionMessage(e)));
283        return 1;
284      }
285    }
286
287    LDAPConnection conn = null;
288    try {
289      conn = argParser.connect(out, err);
290      TaskClient tc = new TaskClient(conn);
291      TaskEntry taskEntry = tc.schedule(this);
292      LocalizableMessage startTime = taskEntry.getScheduledStartTime();
293      if (taskEntry.getTaskState() == TaskState.RECURRING) {
294        printWrappedText(out, INFO_TASK_TOOL_RECURRING_TASK_SCHEDULED.get(taskEntry.getType(), taskEntry.getId()));
295      } else if (startTime == null || startTime.length() == 0) {
296        printWrappedText(out, INFO_TASK_TOOL_TASK_SCHEDULED_NOW.get(taskEntry.getType(), taskEntry.getId()));
297      } else {
298        printWrappedText(out, INFO_TASK_TOOL_TASK_SCHEDULED_FUTURE.get(
299            taskEntry.getType(), taskEntry.getId(), taskEntry.getScheduledStartTime()));
300      }
301      if (!taskScheduleArgs.startArg.isPresent()) {
302
303        // Poll the task printing log messages until finished
304        String taskId = taskEntry.getId();
305        Set<LocalizableMessage> printedLogMessages = new HashSet<>();
306        do {
307          taskEntry = tc.getTaskEntry(taskId);
308          List<LocalizableMessage> logs = taskEntry.getLogMessages();
309          for (LocalizableMessage log : logs) {
310            if (printedLogMessages.add(log)) {
311              out.println(log);
312            }
313          }
314
315          try {
316            Thread.sleep(SYNCHRONOUS_TASK_POLL_INTERVAL);
317          } catch (InterruptedException e) {
318            // ignore
319          }
320
321        } while (!taskEntry.isDone());
322        if (TaskState.isSuccessful(taskEntry.getTaskState())) {
323          if (taskEntry.getTaskState() != TaskState.RECURRING) {
324            printWrappedText(out, INFO_TASK_TOOL_TASK_SUCESSFULL.get(taskEntry.getType(), taskEntry.getId()));
325          }
326          return 0;
327        } else {
328          printWrappedText(out, INFO_TASK_TOOL_TASK_NOT_SUCESSFULL.get(taskEntry.getType(), taskEntry.getId()));
329          return 1;
330        }
331      }
332      return 0;
333    } catch (LDAPConnectionException e) {
334      if (isWrongPortException(e,
335          Integer.valueOf(argParser.getArguments().getPort())))
336      {
337        printWrappedText(err, ERR_TASK_LDAP_FAILED_TO_CONNECT_WRONG_PORT.get(
338            argParser.getArguments().getHostName(), argParser.getArguments().getPort()));
339      } else {
340        printWrappedText(err, ERR_TASK_TOOL_START_TIME_NO_LDAP.get(e.getMessage()));
341      }
342      return 1;
343    } catch (DecodeException ae) {
344      printWrappedText(err, ERR_TASK_TOOL_DECODE_ERROR.get(ae.getMessage()));
345      return 1;
346    } catch (IOException ioe) {
347      printWrappedText(err, ERR_TASK_TOOL_IO_ERROR.get(ioe));
348      return 1;
349    } catch (LDAPException le) {
350      printWrappedText(err, ERR_TASK_TOOL_LDAP_ERROR.get(le.getMessage()));
351      return 1;
352    } catch (OpenDsException e) {
353      printWrappedText(err, e.getMessageObject());
354      return 1;
355    } catch (ArgumentException e) {
356      argParser.displayMessageAndUsageReference(err, e.getMessageObject());
357      return 1;
358    }
359    finally
360    {
361      if (conn != null)
362      {
363        try
364        {
365          conn.close(null);
366        }
367        catch (Throwable t)
368        {
369          // Ignore.
370        }
371      }
372    }
373  }
374
375  private boolean isRemoteTask() {
376    return argParser.connectionArgumentsPresent();
377  }
378
379  /**
380   * Returns {@code true} if the provided exception was caused by trying to
381   * connect to the wrong port and {@code false} otherwise.
382   * @param t the exception to be analyzed.
383   * @param port the port to which we tried to connect.
384   * @return {@code true} if the provided exception was caused by trying to
385   * connect to the wrong port and {@code false} otherwise.
386   */
387  private boolean isWrongPortException(Throwable t, int port)
388  {
389    boolean isWrongPortException = false;
390    boolean isDefaultClearPort = (port - 389) % 1000 == 0;
391    while (t != null && isDefaultClearPort)
392    {
393      isWrongPortException = t instanceof java.net.SocketTimeoutException;
394      if (!isWrongPortException)
395      {
396        t = t.getCause();
397      }
398      else
399      {
400        break;
401      }
402    }
403    return isWrongPortException;
404  }
405
406
407  /**
408   * Indicates whether we must return if the command must be run in off-line
409   * mode.
410   * @return <CODE>true</CODE> if we must return if the command must be run in
411   * off-line mode and <CODE>false</CODE> otherwise.
412   */
413  public boolean testIfOffline()
414  {
415    boolean returnValue = false;
416    if (testIfOfflineArg != null)
417    {
418      returnValue = testIfOfflineArg.isPresent();
419    }
420    return returnValue;
421  }
422
423  /**
424   * Checks that binary version and instance version are the same.
425   *
426   * @throws InitializationException
427   *           If versions mismatch
428   */
429  protected void checkVersion() throws InitializationException
430  {
431    // FIXME Do not perform this check if the tool is use in remote mode (see OPENDJ-1166)
432    BuildVersion.checkVersionMismatch();
433  }
434}