001/*
002 * Copyright 2020 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2020 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.unboundidds.tools;
037
038
039
040import java.io.File;
041import java.io.IOException;
042import java.io.OutputStream;
043import java.io.PrintWriter;
044import java.lang.reflect.Method;
045import java.security.SecureRandom;
046import java.text.SimpleDateFormat;
047import java.util.ArrayList;
048import java.util.Arrays;
049import java.util.Collections;
050import java.util.Date;
051import java.util.LinkedHashMap;
052import java.util.List;
053import java.util.concurrent.TimeUnit;
054import java.util.concurrent.atomic.AtomicReference;
055import java.util.logging.Level;
056
057import com.unboundid.asn1.ASN1OctetString;
058import com.unboundid.ldap.sdk.Control;
059import com.unboundid.ldap.sdk.LDAPConnectionPool;
060import com.unboundid.ldap.sdk.LDAPException;
061import com.unboundid.ldap.sdk.ResultCode;
062import com.unboundid.ldap.sdk.Version;
063import com.unboundid.ldap.sdk.unboundidds.controls.NoOpRequestControl;
064import com.unboundid.ldap.sdk.unboundidds.extensions.
065            CollectSupportDataExtendedRequest;
066import com.unboundid.ldap.sdk.unboundidds.extensions.
067            CollectSupportDataExtendedRequestProperties;
068import com.unboundid.ldap.sdk.unboundidds.extensions.
069            CollectSupportDataExtendedResult;
070import com.unboundid.ldap.sdk.unboundidds.extensions.
071            DurationCollectSupportDataLogCaptureWindow;
072import com.unboundid.ldap.sdk.unboundidds.extensions.
073            HeadAndTailSizeCollectSupportDataLogCaptureWindow;
074import com.unboundid.ldap.sdk.unboundidds.extensions.
075            StartAdministrativeSessionExtendedRequest;
076import com.unboundid.ldap.sdk.unboundidds.extensions.
077            StartAdministrativeSessionPostConnectProcessor;
078import com.unboundid.ldap.sdk.unboundidds.extensions.
079            TimeWindowCollectSupportDataLogCaptureWindow;
080import com.unboundid.ldap.sdk.unboundidds.tasks.CollectSupportDataSecurityLevel;
081import com.unboundid.util.Base64;
082import com.unboundid.util.Debug;
083import com.unboundid.util.LDAPCommandLineTool;
084import com.unboundid.util.NotNull;
085import com.unboundid.util.Nullable;
086import com.unboundid.util.ObjectPair;
087import com.unboundid.util.PasswordReader;
088import com.unboundid.util.StaticUtils;
089import com.unboundid.util.ThreadSafety;
090import com.unboundid.util.ThreadSafetyLevel;
091import com.unboundid.util.args.Argument;
092import com.unboundid.util.args.ArgumentException;
093import com.unboundid.util.args.ArgumentParser;
094import com.unboundid.util.args.BooleanArgument;
095import com.unboundid.util.args.DurationArgument;
096import com.unboundid.util.args.FileArgument;
097import com.unboundid.util.args.IntegerArgument;
098import com.unboundid.util.args.StringArgument;
099import com.unboundid.util.args.TimestampArgument;
100
101import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*;
102
103
104
105/**
106 * This class provides a command-line tool that may be used to invoke the
107 * collect-support-data utility in the Ping Identity Directory Server and
108 * related server products.
109 * <BR>
110 * <BLOCKQUOTE>
111 *   <B>NOTE:</B>  This class, and other classes within the
112 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
113 *   supported for use against Ping Identity, UnboundID, and
114 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
115 *   for proprietary functionality or for external specifications that are not
116 *   considered stable or mature enough to be guaranteed to work in an
117 *   interoperable way with other types of LDAP servers.
118 * </BLOCKQUOTE>
119 * <BR>
120 * Note that this is a client-side wrapper for the application.  While it may
121 * be used to invoke the tool against a remote server using the
122 * {@link CollectSupportDataExtendedRequest}, it does not include direct support
123 * for invoking the tool against a local instance.  That will be accomplished
124 * indirectly by invoking the server-side version of the tool via reflection.
125 */
126@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
127public final class CollectSupportData
128       extends LDAPCommandLineTool
129{
130  /**
131   * The column at which to wrap long lines.
132   */
133  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
134
135
136
137  /**
138   * The Ping Identity Directory Server's default access log timestamp format
139   * when configured to use millisecond precision.
140   */
141  @NotNull static final String SERVER_LOG_TIMESTAMP_FORMAT_WITH_MILLIS =
142       "'['dd/MMM/yyyy:HH:mm:ss.SSS Z']'";
143
144
145
146  /**
147   * The Ping Identity Directory Server's default access log timestamp format
148   * when configured to use second precision.
149   */
150  @NotNull static final String SERVER_LOG_TIMESTAMP_FORMAT_WITHOUT_MILLIS =
151       "'['dd/MMM/yyyy:HH:mm:ss Z']'";
152
153
154
155  /**
156   * The fully-qualified name of the class in the server codebase that will be
157   * invoked to perform collect-support-data processing when the
158   * --useRemoteServer argument is not provided.
159   */
160  @NotNull private static final String SERVER_CSD_TOOL_CLASS =
161       "com.unboundid.directory.server.tools.CollectSupportData";
162
163
164
165  // The completion message for this tool, if available.
166  @NotNull private final AtomicReference<String> toolCompletionMessage;
167
168  // The command-line arguments supported by this tool.
169  @Nullable private ArgumentParser parser;
170  @Nullable private BooleanArgument archiveExtensionSourceArg;
171  @Nullable private BooleanArgument collectExpensiveDataArg;
172  @Nullable private BooleanArgument collectReplicationStateDumpArg;
173  @Nullable private BooleanArgument dryRunArg;
174  @Nullable private BooleanArgument encryptArg;
175  @Nullable private BooleanArgument generatePassphraseArg;
176  @Nullable private BooleanArgument includeBinaryFilesArg;
177  @Nullable private BooleanArgument noLDAPArg;
178  @Nullable private BooleanArgument noPromptArg;
179  @Nullable private BooleanArgument scriptFriendlyArg;
180  @Nullable private BooleanArgument sequentialArg;
181  @Nullable private BooleanArgument useAdministrativeSessionArg;
182  @Nullable private BooleanArgument useRemoteServerArg;
183  @Nullable private DurationArgument logDurationArg;
184  @Nullable private FileArgument decryptArg;
185  @Nullable private FileArgument outputPathArg;
186  @Nullable private FileArgument passphraseFileArg;
187  @Nullable private IntegerArgument jstackCountArg;
188  @Nullable private IntegerArgument logFileHeadCollectionSizeKBArg;
189  @Nullable private IntegerArgument logFileTailCollectionSizeKBArg;
190  @Nullable private IntegerArgument reportCountArg;
191  @Nullable private IntegerArgument reportIntervalSecondsArg;
192  @Nullable private IntegerArgument pidArg;
193  @Nullable private IntegerArgument proxyToServerPortArg;
194  @Nullable private StringArgument commentArg;
195  @Nullable private StringArgument proxyToServerAddressArg;
196  @Nullable private StringArgument securityLevelArg;
197  @Nullable private StringArgument logTimeRangeArg;
198
199
200
201  /**
202   * Invokes this tool with the provided set of command-line arguments.  The
203   * JVM's default standard output and standard error streams will be used.
204   *
205   * @param  args  The set of command-line arguments provided to this program.
206   *               It must not be {@code null} but may be empty.
207   */
208  public static void main(@NotNull final String... args)
209  {
210    final ResultCode resultCode = main(System.out, System.err, args);
211    if ((resultCode != ResultCode.SUCCESS) &&
212         (resultCode != ResultCode.NO_OPERATION))
213    {
214      System.exit(resultCode.intValue());
215    }
216  }
217
218
219
220  /**
221   * Invokes this tool with the provided set of command-line arguments, using
222   * the given output and error streams.
223   *
224   * @param  out   The output stream to use for standard output.  It may be
225   *               {@code null} if standard output should be suppressed.
226   * @param  err   The output stream to use for standard error.  It may be
227   *               {@code null} if standard error should be suppressed.
228   * @param  args  The set of command-line arguments provided to this program.
229   *               It must not be {@code null} but may be empty.
230   *
231   * @return  A result code indicating the final status of the processing that
232   *          was performed.  A result code of {@link ResultCode#SUCCESS}
233   *          indicates that all processing was successful.  A result code of
234   *          {@link ResultCode#NO_OPERATION} indicates that it is likely that
235   *          processing would have been successful if the --dryRun argument
236   *          had not been provided.  Any other result code indicates that the
237   *          processing did not complete successfully.
238   */
239  @NotNull()
240  public static ResultCode main(@Nullable final OutputStream out,
241                                @Nullable final OutputStream err,
242                                @NotNull final String... args)
243  {
244    final CollectSupportData tool = new CollectSupportData(out, err);
245    return tool.runTool(args);
246  }
247
248
249
250  /**
251   * Creates a new instance of this tool that will use the provided streams for
252   * standard output and standard error.
253   *
254   * @param  out  The output stream to use for standard output.  It may be
255   *              {@code null} if standard output should be suppressed.
256   * @param  err  The output stream to use for standard error.  It may be
257   *              {@code null} if standard error should be suppressed.
258   */
259  public CollectSupportData(@Nullable final OutputStream out,
260                            @Nullable final OutputStream err)
261  {
262    super(out, err);
263
264    toolCompletionMessage = new AtomicReference<>();
265
266    parser = null;
267    archiveExtensionSourceArg = null;
268    collectExpensiveDataArg = null;
269    collectReplicationStateDumpArg = null;
270    dryRunArg = null;
271    encryptArg = null;
272    generatePassphraseArg = null;
273    includeBinaryFilesArg = null;
274    noLDAPArg = null;
275    noPromptArg = null;
276    scriptFriendlyArg = null;
277    sequentialArg = null;
278    useRemoteServerArg = null;
279    logDurationArg = null;
280    logFileHeadCollectionSizeKBArg = null;
281    logFileTailCollectionSizeKBArg = null;
282    jstackCountArg = null;
283    outputPathArg = null;
284    reportCountArg = null;
285    decryptArg = null;
286    passphraseFileArg = null;
287    reportIntervalSecondsArg = null;
288    pidArg = null;
289    proxyToServerPortArg = null;
290    commentArg = null;
291    logTimeRangeArg = null;
292    proxyToServerAddressArg = null;
293    securityLevelArg = null;
294  }
295
296
297
298  /**
299   * {@inheritDoc}
300   */
301  @Override()
302  @NotNull()
303  public String getToolName()
304  {
305    return "collect-support-data";
306  }
307
308
309
310  /**
311   * {@inheritDoc}
312   */
313  @Override()
314  @NotNull()
315  public String getToolDescription()
316  {
317    return INFO_CSD_TOOL_DESCRIPTION_1.get();
318  }
319
320
321
322  /**
323   * {@inheritDoc}
324   */
325  @Override()
326  @NotNull()
327  public List<String> getAdditionalDescriptionParagraphs()
328  {
329    return Collections.singletonList(INFO_CSD_TOOL_DESCRIPTION_2.get());
330  }
331
332
333
334  /**
335   * {@inheritDoc}
336   */
337  @Override()
338  @NotNull()
339  public String getToolVersion()
340  {
341    return Version.NUMERIC_VERSION_STRING;
342  }
343
344
345
346  /**
347   * {@inheritDoc}
348   */
349  @Override()
350  public boolean supportsInteractiveMode()
351  {
352    return true;
353  }
354
355
356
357  /**
358   * {@inheritDoc}
359   */
360  @Override()
361  public boolean defaultsToInteractiveMode()
362  {
363    return false;
364  }
365
366
367
368  /**
369   * {@inheritDoc}
370   */
371  @Override()
372  public boolean supportsPropertiesFile()
373  {
374    return true;
375  }
376
377
378
379  /**
380   * {@inheritDoc}
381   */
382  @Override()
383  protected boolean defaultToPromptForBindPassword()
384  {
385    if ((noPromptArg != null) && noPromptArg.isPresent())
386    {
387      return false;
388    }
389
390    return true;
391  }
392
393
394
395  /**
396   * {@inheritDoc}
397   */
398  @Override()
399  protected boolean includeAlternateLongIdentifiers()
400  {
401    return true;
402  }
403
404
405
406  /**
407   * {@inheritDoc}
408   */
409  @Override()
410  protected boolean supportsSSLDebugging()
411  {
412    return true;
413  }
414
415
416
417  /**
418   * {@inheritDoc}
419   */
420  @Override()
421  protected boolean logToolInvocationByDefault()
422  {
423    return true;
424  }
425
426
427
428  /**
429   * {@inheritDoc}
430   */
431  @Override()
432  @Nullable()
433  protected String getToolCompletionMessage()
434  {
435    return toolCompletionMessage.get();
436  }
437
438
439
440  /**
441   * {@inheritDoc}
442   */
443  @Override()
444  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
445         throws ArgumentException
446  {
447    this.parser = parser;
448
449    // Output-related arguments.
450    outputPathArg = new FileArgument(null, "outputPath", false, 1, null,
451         INFO_CSD_ARG_DESC_OUTPUT_PATH.get(), false, true, false, false);
452    outputPathArg.addLongIdentifier("output-path", true);
453    outputPathArg.addLongIdentifier("outputFile", true);
454    outputPathArg.addLongIdentifier("output-file", true);
455    outputPathArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
456    parser.addArgument(outputPathArg);
457
458    encryptArg = new BooleanArgument(null, "encrypt", 1,
459         INFO_CSD_ARG_DESC_ENCRYPT.get());
460    encryptArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
461    parser.addArgument(encryptArg);
462
463    passphraseFileArg = new FileArgument(null, "passphraseFile", false, 1,
464         null, INFO_CSD_ARG_DESC_PASSPHRASE_FILE.get(), false, true, true,
465         false);
466    passphraseFileArg.addLongIdentifier("passphrase-file", true);
467    passphraseFileArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
468    parser.addArgument(passphraseFileArg);
469
470    generatePassphraseArg = new BooleanArgument(null, "generatePassphrase", 1,
471         INFO_CSD_ARG_DESC_GENERATE_PASSPHRASE.get());
472    generatePassphraseArg.addLongIdentifier("generate-passphrase", true);
473    generatePassphraseArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
474    parser.addArgument(generatePassphraseArg);
475
476    decryptArg = new FileArgument(null, "decrypt", false, 1, null,
477         INFO_CSD_ARG_DESC_DECRYPT.get(), false, true, true, false);
478    decryptArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
479    parser.addArgument(decryptArg);
480
481
482    // Collection-related arguments.
483    collectExpensiveDataArg = new BooleanArgument(null, "collectExpensiveData",
484         1, INFO_CSD_ARG_DESC_COLLECT_EXPENSIVE_DATA.get());
485    collectExpensiveDataArg.addLongIdentifier("collect-expensive-data", true);
486    collectExpensiveDataArg.addLongIdentifier("includeExpensiveData", true);
487    collectExpensiveDataArg.addLongIdentifier("include-expensive-data", true);
488    collectExpensiveDataArg.setArgumentGroupName(
489         INFO_CSD_ARG_GROUP_COLLECTION.get());
490    parser.addArgument(collectExpensiveDataArg);
491
492    collectReplicationStateDumpArg = new BooleanArgument(null,
493         "collectReplicationStateDump", 1,
494         INFO_CSD_ARG_DESC_COLLECT_REPL_STATE.get());
495    collectReplicationStateDumpArg.addLongIdentifier(
496         "collect-replication-state-dump", true);
497    collectReplicationStateDumpArg.addLongIdentifier(
498         "collectReplicationState", true);
499    collectReplicationStateDumpArg.addLongIdentifier(
500         "collect-replication-state", true);
501    collectReplicationStateDumpArg.addLongIdentifier(
502         "includeReplicationStateDump", true);
503    collectReplicationStateDumpArg.addLongIdentifier(
504         "include-replication-state-dump", true);
505    collectReplicationStateDumpArg.addLongIdentifier(
506         "includeReplicationState", true);
507    collectReplicationStateDumpArg.addLongIdentifier(
508         "include-replication-state", true);
509    collectReplicationStateDumpArg.setArgumentGroupName(
510         INFO_CSD_ARG_GROUP_COLLECTION.get());
511    parser.addArgument(collectReplicationStateDumpArg);
512
513    includeBinaryFilesArg = new BooleanArgument(null, "includeBinaryFiles", 1,
514         INFO_CSD_ARG_DESC_INCLUDE_BINARY_FILES.get());
515    includeBinaryFilesArg.addLongIdentifier("include-binary-files", true);
516    includeBinaryFilesArg.setArgumentGroupName(
517         INFO_CSD_ARG_GROUP_COLLECTION.get());
518    parser.addArgument(includeBinaryFilesArg);
519
520    archiveExtensionSourceArg = new BooleanArgument(null,
521         "archiveExtensionSource", 1,
522         INFO_CSD_ARG_DESC_ARCHIVE_EXTENSION_SOURCE.get());
523    archiveExtensionSourceArg.addLongIdentifier("archive-extension-source",
524         true);
525    archiveExtensionSourceArg.addLongIdentifier("includeExtensionSource", true);
526    archiveExtensionSourceArg.addLongIdentifier("include-extension-source",
527         true);
528    archiveExtensionSourceArg.setArgumentGroupName(
529         INFO_CSD_ARG_GROUP_COLLECTION.get());
530    parser.addArgument(archiveExtensionSourceArg);
531
532    sequentialArg = new BooleanArgument(null, "sequential", 1,
533         INFO_CSD_ARG_DESC_SEQUENTIAL.get());
534    sequentialArg.addLongIdentifier("sequentialMode", true);
535    sequentialArg.addLongIdentifier("sequential-mode", true);
536    sequentialArg.addLongIdentifier("useSequentialMode", true);
537    sequentialArg.addLongIdentifier("use-sequential-mode", true);
538    sequentialArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
539    parser.addArgument(sequentialArg);
540
541    securityLevelArg = new StringArgument(null, "securityLevel", false, 1,
542         INFO_CSD_ARG_PLACEHOLDER_SECURITY_LEVEL.get(),
543         INFO_CSD_ARG_DESC_SECURITY_LEVEL.get(),
544         StaticUtils.setOf(
545              CollectSupportDataSecurityLevel.NONE.getName(),
546              CollectSupportDataSecurityLevel.OBSCURE_SECRETS.getName(),
547              CollectSupportDataSecurityLevel.MAXIMUM.getName()));
548    securityLevelArg.addLongIdentifier("security-level", true);
549    securityLevelArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
550    parser.addArgument(securityLevelArg);
551
552    jstackCountArg = new IntegerArgument(null, "jstackCount", false, 1,
553         INFO_CSD_ARG_PLACEHOLDER_COUNT.get(),
554         INFO_CSD_ARG_DESC_JSTACK_COUNT.get(), 0, Integer.MAX_VALUE);
555    jstackCountArg.addLongIdentifier("jstack-count", true);
556    jstackCountArg.addLongIdentifier("maxJstacks", true);
557    jstackCountArg.addLongIdentifier("max-jstacks", true);
558    jstackCountArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
559    parser.addArgument(jstackCountArg);
560
561    reportCountArg = new IntegerArgument(null, "reportCount", false, 1,
562         INFO_CSD_ARG_PLACEHOLDER_COUNT.get(),
563         INFO_CSD_ARG_DESC_REPORT_COUNT.get(), 0, Integer.MAX_VALUE);
564    reportCountArg.addLongIdentifier("report-count", true);
565    reportCountArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
566    parser.addArgument(reportCountArg);
567
568    reportIntervalSecondsArg = new IntegerArgument(null,
569         "reportIntervalSeconds", false, 1,
570         INFO_CSD_ARG_PLACEHOLDER_SECONDS.get(),
571         INFO_CSD_ARG_DESC_REPORT_INTERVAL_SECONDS.get(), 1,
572         Integer.MAX_VALUE);
573    reportIntervalSecondsArg.addLongIdentifier("report-interval-seconds", true);
574    reportIntervalSecondsArg.addLongIdentifier("reportInterval", true);
575    reportIntervalSecondsArg.addLongIdentifier("report-interval", true);
576    reportIntervalSecondsArg.setArgumentGroupName(
577         INFO_CSD_ARG_GROUP_COLLECTION.get());
578    parser.addArgument(reportIntervalSecondsArg);
579
580    logTimeRangeArg = new StringArgument(null, "logTimeRange", false, 1,
581         INFO_CSD_ARG_PLACEHOLDER_TIME_RANGE.get(),
582         INFO_CSD_ARG_DESC_TIME_RANGE.get());
583    logTimeRangeArg.addLongIdentifier("log-time-range", true);
584    logTimeRangeArg.addLongIdentifier("timeRange", true);
585    logTimeRangeArg.addLongIdentifier("time-range", true);
586    logTimeRangeArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
587    parser.addArgument(logTimeRangeArg);
588
589    logDurationArg = new DurationArgument(null, "logDuration", false, null,
590         INFO_CSD_ARG_DESC_DURATION.get());
591    logDurationArg.addLongIdentifier("log-duration", true);
592    logDurationArg.addLongIdentifier("duration", true);
593    logDurationArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
594    parser.addArgument(logDurationArg);
595
596    logFileHeadCollectionSizeKBArg = new IntegerArgument(null,
597         "logFileHeadCollectionSizeKB", false, 1,
598         INFO_CSD_ARG_PLACEHOLDER_SIZE_KB.get(),
599         INFO_CSD_ARG_DESC_LOG_HEAD_SIZE_KB.get(), 0, Integer.MAX_VALUE);
600    logFileHeadCollectionSizeKBArg.addLongIdentifier(
601         "log-file-head-collection-size-kb", true);
602    logFileHeadCollectionSizeKBArg.addLongIdentifier("logFileHeadSizeKB", true);
603    logFileHeadCollectionSizeKBArg.addLongIdentifier("log-file-head-size-kb",
604         true);
605    logFileHeadCollectionSizeKBArg.addLongIdentifier("logHeadCollectionSizeKB",
606         true);
607    logFileHeadCollectionSizeKBArg.addLongIdentifier(
608         "log-head-collection-size-kb", true);
609    logFileHeadCollectionSizeKBArg.addLongIdentifier("logHeadSizeKB", true);
610    logFileHeadCollectionSizeKBArg.addLongIdentifier("log-head-size-kb", true);
611    logFileHeadCollectionSizeKBArg.setArgumentGroupName(
612         INFO_CSD_ARG_GROUP_COLLECTION.get());
613    parser.addArgument(logFileHeadCollectionSizeKBArg);
614
615    logFileTailCollectionSizeKBArg = new IntegerArgument(null,
616         "logFileTailCollectionSizeKB", false, 1,
617         INFO_CSD_ARG_PLACEHOLDER_SIZE_KB.get(),
618         INFO_CSD_ARG_DESC_LOG_TAIL_SIZE_KB.get(), 0, Integer.MAX_VALUE);
619    logFileTailCollectionSizeKBArg.addLongIdentifier(
620         "log-file-tail-collection-size-kb", true);
621    logFileTailCollectionSizeKBArg.addLongIdentifier("logFileTailSizeKB", true);
622    logFileTailCollectionSizeKBArg.addLongIdentifier("log-file-tail-size-kb",
623         true);
624    logFileTailCollectionSizeKBArg.addLongIdentifier("logTailCollectionSizeKB",
625         true);
626    logFileTailCollectionSizeKBArg.addLongIdentifier(
627         "log-tail-collection-size-kb", true);
628    logFileTailCollectionSizeKBArg.addLongIdentifier("logTailSizeKB", true);
629    logFileTailCollectionSizeKBArg.addLongIdentifier("log-tail-size-kb", true);
630    logFileTailCollectionSizeKBArg.setArgumentGroupName(
631         INFO_CSD_ARG_GROUP_COLLECTION.get());
632    parser.addArgument(logFileTailCollectionSizeKBArg);
633
634    pidArg = new IntegerArgument(null, "pid", false, 0,
635         INFO_CSD_ARG_PLACEHOLDER_PID.get(), INFO_CSD_ARG_DESC_PID.get());
636    pidArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
637    parser.addArgument(pidArg);
638
639    commentArg = new StringArgument(null, "comment", false, 1, null,
640         INFO_CSD_ARG_DESC_COMMENT.get());
641    commentArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
642    parser.addArgument(commentArg);
643
644
645    // Communication-related arguments.
646    useRemoteServerArg = new BooleanArgument(null, "useRemoteServer", 1,
647         INFO_CSD_ARG_DEC_USE_REMOTE_SERVER.get());
648    useRemoteServerArg.addLongIdentifier("use-remote-server", true);
649    useRemoteServerArg.setArgumentGroupName(
650         INFO_CSD_ARG_GROUP_COMMUNICATION.get());
651    parser.addArgument(useRemoteServerArg);
652
653    useAdministrativeSessionArg = new BooleanArgument(null,
654         "useAdministrativeSession", 1,
655         INFO_CSD_ARG_DESC_USE_ADMIN_SESSION.get());
656    useAdministrativeSessionArg.addLongIdentifier("use-administrative-session",
657         true);
658    useAdministrativeSessionArg.addLongIdentifier("useAdminSession", true);
659    useAdministrativeSessionArg.addLongIdentifier("use-admin-session",
660         true);
661    useAdministrativeSessionArg.addLongIdentifier("administrativeSession",
662         true);
663    useAdministrativeSessionArg.addLongIdentifier("administrative-session",
664         true);
665    useAdministrativeSessionArg.addLongIdentifier("adminSession", true);
666    useAdministrativeSessionArg.addLongIdentifier("admin-session", true);
667    useAdministrativeSessionArg.setArgumentGroupName(
668         INFO_CSD_ARG_GROUP_COMMUNICATION.get());
669    parser.addArgument(useAdministrativeSessionArg);
670
671    proxyToServerAddressArg = new StringArgument(null, "proxyToServerAddress",
672         false, 1, INFO_CSD_ARG_PLACEHOLDER_ADDRESS.get(),
673         INFO_CSD_ARG_DESC_PROXY_TO_ADDRESS.get());
674    proxyToServerAddressArg.addLongIdentifier("proxy-to-server-address", true);
675    proxyToServerAddressArg.addLongIdentifier("proxyToAddress", true);
676    proxyToServerAddressArg.addLongIdentifier("proxy-to-address", true);
677    proxyToServerAddressArg.setArgumentGroupName(
678         INFO_CSD_ARG_GROUP_COMMUNICATION.get());
679    parser.addArgument(proxyToServerAddressArg);
680
681    proxyToServerPortArg = new IntegerArgument(null, "proxyToServerPort", false,
682         1, INFO_CSD_ARG_PLACEHOLDER_PORT.get(),
683         INFO_CSD_ARG_DESC_PROXY_TO_PORT.get(), 1, 65535);
684    proxyToServerPortArg.addLongIdentifier("proxy-to-server-port", true);
685    proxyToServerPortArg.addLongIdentifier("proxyToPort", true);
686    proxyToServerPortArg.addLongIdentifier("proxy-to-port", true);
687    proxyToServerPortArg.setArgumentGroupName(
688         INFO_CSD_ARG_GROUP_COMMUNICATION.get());
689    parser.addArgument(proxyToServerPortArg);
690
691    noLDAPArg = new BooleanArgument(null, "noLDAP", 1,
692         INFO_CSD_ARG_DESC_NO_LDAP.get());
693    noLDAPArg.addLongIdentifier("no-ldap", true);
694    noLDAPArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COMMUNICATION.get());
695    parser.addArgument(noLDAPArg);
696
697
698    // Other arguments.
699    noPromptArg = new BooleanArgument('n', "noPrompt",  1,
700         INFO_CSD_ARG_DESC_NO_PROMPT.get());
701    noPromptArg.addLongIdentifier("no-prompt", true);
702    parser.addArgument(noPromptArg);
703
704    dryRunArg = new BooleanArgument(null, "dryRun", 1,
705         INFO_CSD_ARG_DESC_DRY_RUN.get());
706    dryRunArg.addLongIdentifier("dry-run", true);
707    dryRunArg.addLongIdentifier("noOperation", true);
708    dryRunArg.addLongIdentifier("no-operation", true);
709    dryRunArg.addLongIdentifier("noOp", true);
710    dryRunArg.addLongIdentifier("no-op", true);
711    parser.addArgument(dryRunArg);
712
713    scriptFriendlyArg = new BooleanArgument(null, "scriptFriendly", 1,
714         INFO_CSD_ARG_DESC_SCRIPT_FRIENDLY.get());
715    scriptFriendlyArg.addLongIdentifier("script-friendly", true);
716    scriptFriendlyArg.setHidden(true);
717    parser.addArgument(scriptFriendlyArg);
718
719
720    // If the --useRemoteServer argument is provided, then none of the --pid,
721    // --decrypt, --noLDAP, or --scriptFriendly arguments may be given.
722    parser.addExclusiveArgumentSet(useRemoteServerArg, pidArg);
723    parser.addExclusiveArgumentSet(useRemoteServerArg, decryptArg);
724    parser.addExclusiveArgumentSet(useRemoteServerArg, noLDAPArg);
725    parser.addExclusiveArgumentSet(useRemoteServerArg, scriptFriendlyArg);
726
727    // The --useAdministrativeSession argument can only be provided if the
728    // --useRemoteServer argument is also given.
729    parser.addDependentArgumentSet(useAdministrativeSessionArg,
730         useRemoteServerArg);
731
732    // If the --proxyToServerAddress or --proxyToServerPort argument is given,
733    // then the other must be provided as well.
734    parser.addMutuallyDependentArgumentSet(proxyToServerAddressArg,
735         proxyToServerPortArg);
736
737    // The --proxyToServerAddress and --proxyToServerPort arguments can only
738    // be used if the --useRemoteServer argument is given.
739    parser.addDependentArgumentSet(proxyToServerAddressArg, useRemoteServerArg);
740    parser.addDependentArgumentSet(proxyToServerPortArg, useRemoteServerArg);
741
742    // The --logTimeRange and --logDuration arguments cannot be used together.
743    parser.addExclusiveArgumentSet(logTimeRangeArg, logDurationArg);
744
745    // Neither the --logFileHeadCollectionSizeKB argument nor the
746    // --logFileTailCollectionSizeKB argument can be used in conjunction with
747    // either the --logTimeRange or --logDuration argument.
748    parser.addExclusiveArgumentSet(logFileHeadCollectionSizeKBArg,
749         logTimeRangeArg, logDurationArg);
750    parser.addExclusiveArgumentSet(logFileTailCollectionSizeKBArg,
751         logTimeRangeArg, logDurationArg);
752
753    // The --generatePassphrase argument can only be used if both the
754    // --encrypt and --passphraseFile arguments are provided.
755    parser.addDependentArgumentSet(generatePassphraseArg, encryptArg);
756    parser.addDependentArgumentSet(generatePassphraseArg, passphraseFileArg);
757
758    // The --encrypt and --decrypt arguments cannot be used together.
759    parser.addExclusiveArgumentSet(encryptArg, decryptArg);
760
761
762    // There are several arguments that the LDAP SDK's LDAP command-line tool
763    // framework offers that the server-side version of the framework does not
764    // provide.  Those arguments can only be used in conjunction with the
765    // --useRemoteServer argument.
766    for (final String argumentIdentifier :
767         Arrays.asList("promptForBindPassword", "promptForKeyStorePassword",
768              "keyStoreFormat", "promptForTrustStorePassword",
769              "trustStoreFormat", "enableSSLDebugging", "useSASLExternal"))
770    {
771      final Argument arg = parser.getNamedArgument(argumentIdentifier);
772      parser.addDependentArgumentSet(arg, useRemoteServerArg);
773    }
774  }
775
776
777
778  /**
779   * {@inheritDoc}
780   */
781  @Override()
782  public void doExtendedNonLDAPArgumentValidation()
783         throws ArgumentException
784  {
785    // If the --logTimeRange argument was provided, then make sure we can
786    // parse each of the values and that the end time is greater than or equal
787    // to the start time.
788    if (logTimeRangeArg.isPresent())
789    {
790      try
791      {
792        parseTimeRange(logTimeRangeArg.getValue(),
793             useRemoteServerArg.isPresent());
794      }
795      catch (final LDAPException e)
796      {
797        Debug.debugException(e);
798        toolCompletionMessage.set(e.getMessage());
799        throw new ArgumentException(e.getMessage(), e);
800      }
801    }
802
803
804    // If the --passphraseFile argument was provided without the
805    // --generatePassphrase argument, then make sure the file exists.
806    if (passphraseFileArg.isPresent() && (! generatePassphraseArg.isPresent()))
807    {
808      final File passphraseFile = passphraseFileArg.getValue();
809      if (! passphraseFile.exists())
810      {
811        final String message =ERR_CSD_PASSPHRASE_FILE_MISSING.get(
812             passphraseFile.getAbsolutePath());
813        toolCompletionMessage.set(message);
814        throw new ArgumentException(message);
815      }
816    }
817
818
819    // If either the --encrypt or --decrypt argument is provided in conjunction
820    // with the --noPrompt argument, then the --passphraseFile argument must
821    // also have been provided.
822    if (noPromptArg.isPresent() &&
823         (encryptArg.isPresent() || decryptArg.isPresent()) &&
824         (! passphraseFileArg.isPresent()))
825    {
826      final String message = ERR_CSD_NO_PASSPHRASE_WITH_NO_PROMPT.get();
827      toolCompletionMessage.set(message);
828      throw new ArgumentException(message);
829    }
830  }
831
832
833
834  /**
835   * Parses the provided string as a time range.  If both start and end time
836   * values are provided, then they must be separated by a comma; otherwise,
837   * there must only be a start time value.  Each timestamp must be in either
838   * the generalized time format or the Ping Identity Directory Server's default
839   * access log format (with or without millisecond precision).
840   *
841   * @param  timeRangeStr  The string to be parsed as a time range.  It must not
842   *                       be {@code null}.
843   * @param  strict        Indicates whether to require strict compliance with
844   *                       the timestamp format.  This should be {@code true}
845   *                       when the useRemoteServer argument was provided, and
846   *                       {@code false} otherwise.
847   *
848   * @return  An object pair in which the first value is the start time for
849   *          the range and the second value is the end time for the range.  The
850   *          first element will always be non-{@code null}, but the second
851   *          element may be {@code null} if the time range did not specify an
852   *          end time.  The entire return value may be {@code null} if the
853   *          time range string could not be parsed and {@code strict} is
854   *          {@code false}.
855   *
856   * @throws  LDAPException  If a problem is encountered while parsing the
857   *                         provided string as a time range, or if the start
858   *                         time is greater than the end time.
859   */
860  @Nullable()
861  static ObjectPair<Date,Date> parseTimeRange(
862              @NotNull final String timeRangeStr,
863              final boolean strict)
864         throws LDAPException
865  {
866    final Date startTime;
867    final Date endTime;
868
869    try
870    {
871      // See if there is a comma to separate the before and after times.  If so,
872      // then parse each value separately.  Otherwise, the value will be just
873      // the start time and the current time will be used as the end time.
874      final int commaPos = timeRangeStr.indexOf(',');
875      if (commaPos > 0)
876      {
877        startTime = parseTimestamp(timeRangeStr.substring(0, commaPos).trim());
878        endTime = parseTimestamp(timeRangeStr.substring(commaPos+1).trim());
879      }
880      else
881      {
882        startTime = parseTimestamp(timeRangeStr);
883        endTime = null;
884      }
885    }
886    catch (final LDAPException e)
887    {
888      Debug.debugException(e);
889
890      // NOTE:  The server-side version of the collect-support-data tool has a
891      // not-so-documented feature in which you can provide rotated file names
892      // as an alternative to an actual time range.  We can't handle that
893      // when operating against a remote server, so we'll require strict
894      // timestamp compliance when --useRemoteServer is provided, but we'll just
895      // return null and let the argument value be passed through to the
896      // server-side code otherwise.
897      if (strict)
898      {
899        throw e;
900      }
901      else
902      {
903        return null;
904      }
905    }
906
907    if ((endTime != null) && (startTime.getTime() > endTime.getTime()))
908    {
909      throw new LDAPException(ResultCode.PARAM_ERROR,
910           ERR_CSD_TIME_RANGE_START_GREATER_THAN_END.get());
911    }
912
913    return new ObjectPair<>(startTime, endTime);
914  }
915
916
917
918  /**
919   * Parses the provided string as a timestamp value in either the generalized
920   * time format or the Ping Identity Directory Server's default access log
921   * format (with or without millisecond precision).
922   *
923   * @param  timestampStr  The timestamp to be parsed.  It must not be
924   *                       {@code null}.
925   *
926   * @return  The {@code Date} created by parsing the timestamp.
927   *
928   * @throws  LDAPException  If the provided string cannot be parsed as a
929   *                         valid timestamp.
930   */
931  @NotNull()
932  static Date parseTimestamp(@NotNull final String timestampStr)
933         throws LDAPException
934  {
935    // First, try using the timestamp argument to parse the timestamp.
936    try
937    {
938      return TimestampArgument.parseTimestamp(timestampStr);
939    }
940    catch (final Exception e)
941    {
942      Debug.debugException(Level.FINEST, e);
943    }
944
945
946    // Next, try the server's default access log format with millisecond
947    // precision.
948    try
949    {
950      final SimpleDateFormat timestampFormatter =
951           new SimpleDateFormat(SERVER_LOG_TIMESTAMP_FORMAT_WITH_MILLIS);
952      timestampFormatter.setLenient(false);
953      return timestampFormatter.parse(timestampStr);
954    }
955    catch (final Exception e)
956    {
957      Debug.debugException(Level.FINEST, e);
958    }
959
960
961    // Next, try the server's default access log format with second precision.
962    try
963    {
964      final SimpleDateFormat timestampFormatter =
965           new SimpleDateFormat(SERVER_LOG_TIMESTAMP_FORMAT_WITHOUT_MILLIS);
966      timestampFormatter.setLenient(false);
967      return timestampFormatter.parse(timestampStr);
968    }
969    catch (final Exception e)
970    {
971      Debug.debugException(Level.FINEST, e);
972    }
973
974
975    // If we've gotten here, then we could not parse the timestamp.
976    throw new LDAPException(ResultCode.PARAM_ERROR,
977         ERR_CSD_TIME_RANGE_CANNOT_PARSE_TIMESTAMP.get(timestampStr));
978  }
979
980
981
982  /**
983   * {@inheritDoc}
984   */
985  @Override()
986  @NotNull()
987  public ResultCode doToolProcessing()
988  {
989    // If the --useRemoteServer argument was provided, then use the extended
990    // operation to perform the processing.  Otherwise, use reflection to invoke
991    // the server's version of the collect-support-data tool.
992    if (useRemoteServerArg.isPresent())
993    {
994      return doExtendedOperationProcessing();
995    }
996    else
997    {
998      return doLocalProcessing();
999    }
1000  }
1001
1002
1003
1004  /**
1005   * Performs the collect-support-data processing against a remote server using
1006   * the extended operation.
1007   *
1008   * @return  A result code that indicates the result of the processing.
1009   */
1010  @NotNull()
1011  private ResultCode doExtendedOperationProcessing()
1012  {
1013    // Create a connection pool that will be used to communicate with the
1014    // server.  Use an administrative session if appropriate.
1015    final StartAdministrativeSessionPostConnectProcessor p;
1016    if (useAdministrativeSessionArg.isPresent())
1017    {
1018      p = new StartAdministrativeSessionPostConnectProcessor(
1019           new StartAdministrativeSessionExtendedRequest(getToolName(),
1020                true));
1021    }
1022    else
1023    {
1024      p = null;
1025    }
1026
1027    final LDAPConnectionPool pool;
1028    try
1029    {
1030      pool = getConnectionPool(1, 1, 0, p, null, true,
1031           new ReportBindResultLDAPConnectionPoolHealthCheck(this, true,
1032                false));
1033    }
1034    catch (final LDAPException e)
1035    {
1036      Debug.debugException(e);
1037      wrapErr(0, WRAP_COLUMN, e.getMessage());
1038      toolCompletionMessage.set(e.getMessage());
1039      return e.getResultCode();
1040    }
1041
1042
1043    try
1044    {
1045      // Create the properties to use for the extended request.
1046      final CollectSupportDataExtendedRequestProperties properties =
1047           new CollectSupportDataExtendedRequestProperties();
1048
1049      final File outputPath = outputPathArg.getValue();
1050      if (outputPath != null)
1051      {
1052        if (! (outputPath.exists() && outputPath.isDirectory()))
1053        {
1054          properties.setArchiveFileName(outputPath.getName());
1055        }
1056      }
1057
1058      try
1059      {
1060        properties.setEncryptionPassphrase(
1061             getEncryptionPassphraseForExtOpProcessing());
1062      }
1063      catch (final LDAPException e)
1064      {
1065        Debug.debugException(e);
1066        wrapErr(0, WRAP_COLUMN, e.getMessage());
1067        toolCompletionMessage.set(e.getMessage());
1068        return e.getResultCode();
1069      }
1070
1071
1072      properties.setIncludeExpensiveData(collectExpensiveDataArg.isPresent());
1073      properties.setIncludeReplicationStateDump(
1074           collectReplicationStateDumpArg.isPresent());
1075      properties.setIncludeBinaryFiles(includeBinaryFilesArg.isPresent());
1076      properties.setIncludeExtensionSource(
1077           archiveExtensionSourceArg.isPresent());
1078      properties.setUseSequentialMode(sequentialArg.isPresent());
1079
1080      if (securityLevelArg.isPresent())
1081      {
1082        properties.setSecurityLevel(CollectSupportDataSecurityLevel.forName(
1083             securityLevelArg.getValue()));
1084      }
1085
1086      if (jstackCountArg.isPresent())
1087      {
1088        properties.setJStackCount(jstackCountArg.getValue());
1089      }
1090
1091      if (reportCountArg.isPresent())
1092      {
1093        properties.setReportCount(reportCountArg.getValue());
1094      }
1095
1096      if (reportIntervalSecondsArg.isPresent())
1097      {
1098        properties.setReportIntervalSeconds(
1099             reportIntervalSecondsArg.getValue());
1100      }
1101
1102      if (logTimeRangeArg.isPresent())
1103      {
1104        try
1105        {
1106          final ObjectPair<Date,Date> timeRange =
1107               parseTimeRange(logTimeRangeArg.getValue(), true);
1108          properties.setLogCaptureWindow(
1109               new TimeWindowCollectSupportDataLogCaptureWindow(
1110                    timeRange.getFirst(), timeRange.getSecond()));
1111        }
1112        catch (final LDAPException e)
1113        {
1114          // This should never happen because we should have pre-validated the
1115          // value.  But handle it just in case.
1116          Debug.debugException(e);
1117          wrapErr(0, WRAP_COLUMN, e.getMessage());
1118          toolCompletionMessage.set(e.getMessage());
1119          return e.getResultCode();
1120        }
1121      }
1122      else if (logDurationArg.isPresent())
1123      {
1124        properties.setLogCaptureWindow(
1125             new DurationCollectSupportDataLogCaptureWindow(
1126                  logDurationArg.getValue(TimeUnit.MILLISECONDS)));
1127      }
1128      else if (logFileHeadCollectionSizeKBArg.isPresent() ||
1129           logFileTailCollectionSizeKBArg.isPresent())
1130      {
1131        properties.setLogCaptureWindow(
1132             new HeadAndTailSizeCollectSupportDataLogCaptureWindow(
1133                  logFileHeadCollectionSizeKBArg.getValue(),
1134                  logFileTailCollectionSizeKBArg.getValue()));
1135      }
1136
1137      if (commentArg.isPresent())
1138      {
1139        properties.setComment(commentArg.getValue());
1140      }
1141
1142      if (proxyToServerAddressArg.isPresent())
1143      {
1144        properties.setProxyToServer(proxyToServerAddressArg.getValue(),
1145             proxyToServerPortArg.getValue());
1146      }
1147
1148
1149      // Create the intermediate response listener that will be used to handle
1150      // output and archive fragment messages.
1151      ResultCode resultCode = null;
1152      try (CollectSupportDataIRListener listener =
1153           new CollectSupportDataIRListener(this, outputPathArg.getValue()))
1154      {
1155        // Construct the extended request to send to the server.
1156        final Control[] controls;
1157        if (dryRunArg.isPresent())
1158        {
1159          controls = new Control[]
1160          {
1161            new NoOpRequestControl()
1162          };
1163        }
1164        else
1165        {
1166          controls = StaticUtils.NO_CONTROLS;
1167        }
1168
1169        final CollectSupportDataExtendedRequest request =
1170             new CollectSupportDataExtendedRequest(properties, listener,
1171                  controls);
1172        request.setResponseTimeoutMillis(0L);
1173
1174
1175        // Send the request and read the result.
1176        final CollectSupportDataExtendedResult result;
1177        try
1178        {
1179          result = (CollectSupportDataExtendedResult)
1180               pool.processExtendedOperation(request);
1181        }
1182        catch (final LDAPException e)
1183        {
1184          Debug.debugException(e);
1185          final String message = ERR_CSD_ERROR_SENDING_REQUEST.get(
1186               StaticUtils.getExceptionMessage(e));
1187          wrapErr(0, WRAP_COLUMN, message);
1188          toolCompletionMessage.set(message);
1189          return e.getResultCode();
1190        }
1191
1192
1193        resultCode = result.getResultCode();
1194        final String diagnosticMessage = result.getDiagnosticMessage();
1195        if (diagnosticMessage != null)
1196        {
1197          if ((resultCode == ResultCode.SUCCESS) ||
1198               (resultCode == ResultCode.NO_OPERATION))
1199          {
1200            wrapOut(0, WRAP_COLUMN, diagnosticMessage);
1201          }
1202          else
1203          {
1204            wrapErr(0, WRAP_COLUMN, diagnosticMessage);
1205          }
1206
1207          toolCompletionMessage.set(diagnosticMessage);
1208        }
1209        else
1210        {
1211          toolCompletionMessage.set(INFO_CSD_COMPLETED_WITH_RESULT_CODE.get(
1212               String.valueOf(resultCode)));
1213        }
1214      }
1215      catch (final IOException e)
1216      {
1217        Debug.debugException(e);
1218
1219        if (resultCode == ResultCode.SUCCESS)
1220        {
1221          resultCode = ResultCode.LOCAL_ERROR;
1222          toolCompletionMessage.set(e.getMessage());
1223        }
1224      }
1225
1226      return resultCode;
1227    }
1228    finally
1229    {
1230      pool.close();
1231    }
1232  }
1233
1234
1235
1236  /**
1237   * Retrieves the passphrase to use to generate the key for encrypting the
1238   * support data archive.  This method should only be used when the tool
1239   * processing will be performed using an extended operation.
1240   *
1241   * @return  The passphrase to use to generate the key for encrypting the
1242   *          support data archive.
1243   *
1244   * @throws  LDAPException  If a problem is encountered while attempting to
1245   *                         obtain the passphrase.
1246   */
1247  @Nullable()
1248  private ASN1OctetString getEncryptionPassphraseForExtOpProcessing()
1249          throws LDAPException
1250  {
1251    if (! encryptArg.isPresent())
1252    {
1253      return null;
1254    }
1255
1256    if (passphraseFileArg.isPresent())
1257    {
1258      final File passphraseFile = passphraseFileArg.getValue();
1259      if (generatePassphraseArg.isPresent())
1260      {
1261        // Generate a passphrase as a base64url-encoded representation of some
1262        // randomly generated data.
1263        final SecureRandom random = new SecureRandom();
1264        final byte[] randomBytes = new byte[64];
1265        random.nextBytes(randomBytes);
1266        final String passphrase = Base64.urlEncode(randomBytes, false);
1267
1268        try (PrintWriter writer = new PrintWriter(passphraseFile))
1269        {
1270          writer.println(passphrase);
1271        }
1272        catch (final Exception e)
1273        {
1274          Debug.debugException(e);
1275          throw new LDAPException(ResultCode.LOCAL_ERROR,
1276               ERR_CSD_CANNOT_WRITE_GENERATED_PASSPHRASE.get(
1277                    passphraseFile.getAbsolutePath(),
1278                    StaticUtils.getExceptionMessage(e)),
1279               e);
1280        }
1281
1282        return new ASN1OctetString(passphrase);
1283      }
1284      else
1285      {
1286        try
1287        {
1288          final char[] passphrase =
1289               getPasswordFileReader().readPassword(passphraseFile);
1290          return new ASN1OctetString(new String(passphrase));
1291        }
1292        catch (final Exception e)
1293        {
1294          Debug.debugException(e);
1295
1296          ResultCode resultCode = ResultCode.LOCAL_ERROR;
1297          if (e instanceof LDAPException)
1298          {
1299            resultCode = ((LDAPException) e).getResultCode();
1300          }
1301
1302          throw new LDAPException(resultCode,
1303               ERR_CSD_CANNOT_READ_PASSPHRASE.get(
1304                    passphraseFile.getAbsolutePath(),
1305                    StaticUtils.getExceptionMessage(e)),
1306               e);
1307        }
1308      }
1309    }
1310
1311
1312    // Prompt for the encryption passphrase.
1313    while (true)
1314    {
1315      try
1316      {
1317        getOut().print(INFO_CSD_PASSPHRASE_INITIAL_PROMPT.get());
1318        final byte[] passphraseBytes = PasswordReader.readPassword();
1319
1320        getOut().print(INFO_CSD_PASSPHRASE_CONFIRM_PROMPT.get());
1321        final byte[] confirmBytes = PasswordReader.readPassword();
1322        if (Arrays.equals(passphraseBytes, confirmBytes))
1323        {
1324          return new ASN1OctetString(passphraseBytes);
1325        }
1326        else
1327        {
1328          wrapErr(0, WRAP_COLUMN, ERR_CSD_PASSPHRASE_MISMATCH.get());
1329          err();
1330        }
1331      }
1332      catch (final Exception e)
1333      {
1334        throw new LDAPException(ResultCode.LOCAL_ERROR,
1335             ERR_CSD_PASSPHRASE_PROMPT_READ_ERROR.get(
1336                  StaticUtils.getExceptionMessage(e)),
1337             e);
1338      }
1339    }
1340  }
1341
1342
1343
1344  /**
1345   * Performs the collect-support-data tool using reflection to invoke the
1346   * server-side version of the tool.
1347 *
1348   * @return  A result code that indicates the result of the processing.
1349     */
1350  @NotNull()
1351  private ResultCode doLocalProcessing()
1352  {
1353    // Construct the argument list to use when invoking the server-side code.
1354    // Although this tool supports all of the arguments that the server-side
1355    // tool provides, the server-side tool does not support all of the arguments
1356    // that this version offers, nor does it support all of the variants (e.g.,
1357    // alternate names) for the arguments that do overlap.
1358    final List<String> argList = new ArrayList<>(20);
1359
1360    if (outputPathArg.isPresent())
1361    {
1362      argList.add("--outputPath");
1363      argList.add(outputPathArg.getValue().getAbsolutePath());
1364    }
1365
1366    if (noLDAPArg.isPresent())
1367    {
1368      argList.add("--noLdap");
1369    }
1370
1371    if (pidArg.isPresent())
1372    {
1373      for (final Integer pid : pidArg.getValues())
1374      {
1375        argList.add("--pid");
1376        argList.add(pid.toString());
1377      }
1378    }
1379
1380    if (sequentialArg.isPresent())
1381    {
1382      argList.add("--sequential");
1383    }
1384
1385    if (reportCountArg.isPresent())
1386    {
1387      argList.add("--reportCount");
1388      argList.add(reportCountArg.getValue().toString());
1389    }
1390
1391    if (reportIntervalSecondsArg.isPresent())
1392    {
1393      argList.add("--reportInterval");
1394      argList.add(reportIntervalSecondsArg.getValue().toString());
1395    }
1396
1397    if (jstackCountArg.isPresent())
1398    {
1399      argList.add("--maxJstacks");
1400      argList.add(jstackCountArg.getValue().toString());
1401    }
1402
1403    if (collectExpensiveDataArg.isPresent())
1404    {
1405      argList.add("--collectExpensiveData");
1406    }
1407
1408    if (collectReplicationStateDumpArg.isPresent())
1409    {
1410      argList.add("--collectReplicationStateDump");
1411    }
1412
1413    if (commentArg.isPresent())
1414    {
1415      argList.add("--comment");
1416      argList.add(commentArg.getValue());
1417    }
1418
1419    if (includeBinaryFilesArg.isPresent())
1420    {
1421      argList.add("--includeBinaryFiles");
1422    }
1423
1424    if (securityLevelArg.isPresent())
1425    {
1426      argList.add("--securityLevel");
1427      argList.add(securityLevelArg.getValue());
1428    }
1429
1430    if (encryptArg.isPresent())
1431    {
1432      argList.add("--encrypt");
1433    }
1434
1435    if (passphraseFileArg.isPresent())
1436    {
1437      argList.add("--passphraseFile");
1438      argList.add(passphraseFileArg.getValue().getAbsolutePath());
1439    }
1440
1441    if (generatePassphraseArg.isPresent())
1442    {
1443      argList.add("--generatePassphrase");
1444    }
1445
1446    if (decryptArg.isPresent())
1447    {
1448      argList.add("--decrypt");
1449      argList.add(decryptArg.getValue().getAbsolutePath());
1450    }
1451
1452    if (logTimeRangeArg.isPresent())
1453    {
1454      final ObjectPair<Date,Date> timeRange;
1455      try
1456      {
1457        timeRange = parseTimeRange(logTimeRangeArg.getValue(), false);
1458      }
1459      catch (final LDAPException e)
1460      {
1461        // This should never happen because we should have pre-validated the
1462        // value.  But handle it just in case.
1463        Debug.debugException(e);
1464        wrapErr(0, WRAP_COLUMN, e.getMessage());
1465        return e.getResultCode();
1466      }
1467
1468      if (timeRange == null)
1469      {
1470        // We'll assume that this means the time range was specified using
1471        // rotated log filenames, which we can't handle in the LDAP SDK code so
1472        // we'll just pass the argument value through to the server code.
1473        argList.add("--timeRange");
1474        argList.add(logTimeRangeArg.getValue());
1475      }
1476      else
1477      {
1478        final Date startTime = timeRange.getFirst();
1479        Date endTime = timeRange.getSecond();
1480        if (endTime == null)
1481        {
1482          endTime = new Date(Math.max(System.currentTimeMillis(),
1483               startTime.getTime()));
1484        }
1485
1486        final SimpleDateFormat timestampFormatter =
1487             new SimpleDateFormat(SERVER_LOG_TIMESTAMP_FORMAT_WITH_MILLIS);
1488        argList.add("--timeRange");
1489        argList.add(timestampFormatter.format(startTime) + ',' +
1490             timestampFormatter.format(endTime));
1491      }
1492    }
1493
1494    if (logDurationArg.isPresent())
1495    {
1496      argList.add("--duration");
1497      argList.add(DurationArgument.nanosToDuration(
1498           logDurationArg.getValue(TimeUnit.NANOSECONDS)));
1499    }
1500
1501    if (logFileHeadCollectionSizeKBArg.isPresent())
1502    {
1503      argList.add("--logFileHeadCollectionSizeKB");
1504      argList.add(String.valueOf(logFileHeadCollectionSizeKBArg.getValue()));
1505    }
1506
1507    if (logFileTailCollectionSizeKBArg.isPresent())
1508    {
1509      argList.add("--logFileTailCollectionSizeKB");
1510      argList.add(String.valueOf(logFileTailCollectionSizeKBArg.getValue()));
1511    }
1512
1513    if (archiveExtensionSourceArg.isPresent())
1514    {
1515      argList.add("--archiveExtensionSource");
1516    }
1517
1518    if (noPromptArg.isPresent())
1519    {
1520      argList.add("--no-prompt");
1521    }
1522
1523    if (scriptFriendlyArg.isPresent())
1524    {
1525      argList.add("--script-friendly");
1526    }
1527
1528
1529    // We also need to include values for arguments provided by the LDAP
1530    // command-line tool framework.
1531    for (final String argumentIdentifier :
1532         Arrays.asList("hostname", "port", "bindDN", "bindPassword",
1533              "bindPasswordFile", "useSSL", "useStartTLS", "trustAll",
1534              "keyStorePath", "keyStorePassword", "keyStorePasswordFile",
1535              "trustStorePath", "trustStorePassword", "trustStorePasswordFile",
1536              "certNickname", "saslOption", "propertiesFilePath",
1537              "noPropertiesFile"))
1538    {
1539      final Argument arg = parser.getNamedArgument(argumentIdentifier);
1540      if (arg.getNumOccurrences() > 0)
1541      {
1542        for (final String value : arg.getValueStringRepresentations(false))
1543        {
1544          argList.add("--" + argumentIdentifier);
1545          if (arg.takesValue())
1546          {
1547            argList.add(value);
1548          }
1549        }
1550      }
1551    }
1552
1553
1554    // If the --dryRun argument was provided, then return without actually
1555    // invoking the tool.
1556    if (dryRunArg.isPresent())
1557    {
1558      final String message = INFO_CSD_LOCAL_MODE_DRY_RUN.get();
1559      wrapOut(0, WRAP_COLUMN, message);
1560      toolCompletionMessage.set(message);
1561      return ResultCode.NO_OPERATION;
1562    }
1563
1564
1565    // Make sure that we have access to the method in the server codebase that
1566    // we need to invoke local collect-support-data processing.
1567    final Method doMainMethod;
1568    try
1569    {
1570      final Class<?> csdToolClass = Class.forName(SERVER_CSD_TOOL_CLASS);
1571      doMainMethod = csdToolClass.getMethod("doMain", Boolean.TYPE,
1572           OutputStream.class, OutputStream.class, String[].class);
1573    }
1574    catch (final Throwable t)
1575    {
1576      Debug.debugException(t);
1577      final String message = ERR_CSD_SERVER_CODE_NOT_AVAILABLE.get();
1578      wrapErr(0, WRAP_COLUMN, message);
1579      toolCompletionMessage.set(message);
1580      return ResultCode.NOT_SUPPORTED;
1581    }
1582
1583
1584    // Invoke the doMain method via reflection
1585    final String[] argArray = new String[argList.size()];
1586    argList.toArray(argArray);
1587
1588    try
1589    {
1590      final Object doMainMethodReturnValue = doMainMethod.invoke(null,
1591           true, getOut(), getErr(), argArray);
1592      final int exitCode = ((Integer) doMainMethodReturnValue).intValue();
1593      return ResultCode.valueOf(exitCode);
1594    }
1595    catch (final Throwable t)
1596    {
1597      Debug.debugException(t);
1598      final String message =
1599           ERR_CSD_INVOKE_ERROR.get(StaticUtils.getExceptionMessage(t));
1600      wrapErr(0, WRAP_COLUMN, message);
1601      toolCompletionMessage.set(message);
1602      return ResultCode.LOCAL_ERROR;
1603    }
1604  }
1605
1606
1607
1608  /**
1609   * {@inheritDoc}
1610   */
1611  @Override()
1612  @NotNull()
1613  public LinkedHashMap<String[],String> getExampleUsages()
1614  {
1615    final LinkedHashMap<String[],String> examples =
1616         new LinkedHashMap<>(StaticUtils.computeMapCapacity(3));
1617
1618    examples.put(
1619         new String[]
1620         {
1621           "--bindDN", "uid=admin,dc=example,dc=com",
1622           "--bindPasswordFile", "admin-pw.txt"
1623         },
1624         INFO_CSD_EXAMPLE_1.get());
1625
1626    examples.put(
1627         new String[]
1628         {
1629           "--useRemoteServer",
1630           "--hostname", "ds.example.com",
1631           "--port", "636",
1632           "--useSSL",
1633           "--trustStorePath", "config/truststore",
1634           "--bindDN", "uid=admin,dc=example,dc=com",
1635           "--bindPasswordFile", "admin-pw.txt",
1636           "--collectExpensiveData",
1637           "--collectReplicationStateDump",
1638           "--securityLevel", "maximum",
1639           "--logDuration", "10 minutes",
1640           "--encrypt",
1641           "--passphraseFile", "encryption-passphrase.txt",
1642           "--generatePassphrase",
1643           "--outputPath", "csd.zip"
1644         },
1645         INFO_CSD_EXAMPLE_2.get());
1646
1647    examples.put(
1648         new String[]
1649         {
1650           "--decrypt", "support-data-ds-inst1-" +
1651                StaticUtils.encodeGeneralizedTime(new Date()) +
1652              "-zip-encrypted",
1653           "--passphraseFile", "encryption-passphrase.txt"
1654         },
1655         INFO_CSD_EXAMPLE_3.get());
1656
1657    return examples;
1658  }
1659}