001/*
002 * Copyright 2020-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020-2024 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-2024 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.text.SimpleDateFormat;
046import java.util.ArrayList;
047import java.util.Arrays;
048import java.util.Collections;
049import java.util.Date;
050import java.util.LinkedHashMap;
051import java.util.List;
052import java.util.concurrent.TimeUnit;
053import java.util.concurrent.atomic.AtomicReference;
054import java.util.logging.Level;
055
056import com.unboundid.asn1.ASN1OctetString;
057import com.unboundid.ldap.sdk.Control;
058import com.unboundid.ldap.sdk.LDAPConnectionPool;
059import com.unboundid.ldap.sdk.LDAPException;
060import com.unboundid.ldap.sdk.ResultCode;
061import com.unboundid.ldap.sdk.Version;
062import com.unboundid.ldap.sdk.unboundidds.controls.NoOpRequestControl;
063import com.unboundid.ldap.sdk.unboundidds.extensions.
064            CollectSupportDataExtendedRequest;
065import com.unboundid.ldap.sdk.unboundidds.extensions.
066            CollectSupportDataExtendedRequestProperties;
067import com.unboundid.ldap.sdk.unboundidds.extensions.
068            CollectSupportDataExtendedResult;
069import com.unboundid.ldap.sdk.unboundidds.extensions.
070            DurationCollectSupportDataLogCaptureWindow;
071import com.unboundid.ldap.sdk.unboundidds.extensions.
072            HeadAndTailSizeCollectSupportDataLogCaptureWindow;
073import com.unboundid.ldap.sdk.unboundidds.extensions.
074            StartAdministrativeSessionExtendedRequest;
075import com.unboundid.ldap.sdk.unboundidds.extensions.
076            StartAdministrativeSessionPostConnectProcessor;
077import com.unboundid.ldap.sdk.unboundidds.extensions.
078            TimeWindowCollectSupportDataLogCaptureWindow;
079import com.unboundid.ldap.sdk.unboundidds.tasks.CollectSupportDataSecurityLevel;
080import com.unboundid.util.Base64;
081import com.unboundid.util.Debug;
082import com.unboundid.util.LDAPCommandLineTool;
083import com.unboundid.util.NotNull;
084import com.unboundid.util.Nullable;
085import com.unboundid.util.ObjectPair;
086import com.unboundid.util.PasswordReader;
087import com.unboundid.util.StaticUtils;
088import com.unboundid.util.ThreadLocalSecureRandom;
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              "promptForTrustStorePassword", "enableSSLDebugging",
769              "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 byte[] randomBytes = new byte[64];
1264        ThreadLocalSecureRandom.get().nextBytes(randomBytes);
1265        final String passphrase = Base64.urlEncode(randomBytes, false);
1266
1267        try (PrintWriter writer = new PrintWriter(passphraseFile))
1268        {
1269          writer.println(passphrase);
1270        }
1271        catch (final Exception e)
1272        {
1273          Debug.debugException(e);
1274          throw new LDAPException(ResultCode.LOCAL_ERROR,
1275               ERR_CSD_CANNOT_WRITE_GENERATED_PASSPHRASE.get(
1276                    passphraseFile.getAbsolutePath(),
1277                    StaticUtils.getExceptionMessage(e)),
1278               e);
1279        }
1280
1281        return new ASN1OctetString(passphrase);
1282      }
1283      else
1284      {
1285        try
1286        {
1287          final char[] passphrase =
1288               getPasswordFileReader().readPassword(passphraseFile);
1289          return new ASN1OctetString(new String(passphrase));
1290        }
1291        catch (final Exception e)
1292        {
1293          Debug.debugException(e);
1294
1295          ResultCode resultCode = ResultCode.LOCAL_ERROR;
1296          if (e instanceof LDAPException)
1297          {
1298            resultCode = ((LDAPException) e).getResultCode();
1299          }
1300
1301          throw new LDAPException(resultCode,
1302               ERR_CSD_CANNOT_READ_PASSPHRASE.get(
1303                    passphraseFile.getAbsolutePath(),
1304                    StaticUtils.getExceptionMessage(e)),
1305               e);
1306        }
1307      }
1308    }
1309
1310
1311    // Prompt for the encryption passphrase.
1312    while (true)
1313    {
1314      try
1315      {
1316        getOut().print(INFO_CSD_PASSPHRASE_INITIAL_PROMPT.get());
1317        final byte[] passphraseBytes = PasswordReader.readPassword();
1318
1319        getOut().print(INFO_CSD_PASSPHRASE_CONFIRM_PROMPT.get());
1320        final byte[] confirmBytes = PasswordReader.readPassword();
1321        if (Arrays.equals(passphraseBytes, confirmBytes))
1322        {
1323          return new ASN1OctetString(passphraseBytes);
1324        }
1325        else
1326        {
1327          wrapErr(0, WRAP_COLUMN, ERR_CSD_PASSPHRASE_MISMATCH.get());
1328          err();
1329        }
1330      }
1331      catch (final Exception e)
1332      {
1333        throw new LDAPException(ResultCode.LOCAL_ERROR,
1334             ERR_CSD_PASSPHRASE_PROMPT_READ_ERROR.get(
1335                  StaticUtils.getExceptionMessage(e)),
1336             e);
1337      }
1338    }
1339  }
1340
1341
1342
1343  /**
1344   * Performs the collect-support-data tool using reflection to invoke the
1345   * server-side version of the tool.
1346 *
1347   * @return  A result code that indicates the result of the processing.
1348     */
1349  @NotNull()
1350  private ResultCode doLocalProcessing()
1351  {
1352    // Construct the argument list to use when invoking the server-side code.
1353    // Although this tool supports all of the arguments that the server-side
1354    // tool provides, the server-side tool does not support all of the arguments
1355    // that this version offers, nor does it support all of the variants (e.g.,
1356    // alternate names) for the arguments that do overlap.
1357    final List<String> argList = new ArrayList<>(20);
1358
1359    if (outputPathArg.isPresent())
1360    {
1361      argList.add("--outputPath");
1362      argList.add(outputPathArg.getValue().getAbsolutePath());
1363    }
1364
1365    if (noLDAPArg.isPresent())
1366    {
1367      argList.add("--noLdap");
1368    }
1369
1370    if (pidArg.isPresent())
1371    {
1372      for (final Integer pid : pidArg.getValues())
1373      {
1374        argList.add("--pid");
1375        argList.add(pid.toString());
1376      }
1377    }
1378
1379    if (sequentialArg.isPresent())
1380    {
1381      argList.add("--sequential");
1382    }
1383
1384    if (reportCountArg.isPresent())
1385    {
1386      argList.add("--reportCount");
1387      argList.add(reportCountArg.getValue().toString());
1388    }
1389
1390    if (reportIntervalSecondsArg.isPresent())
1391    {
1392      argList.add("--reportInterval");
1393      argList.add(reportIntervalSecondsArg.getValue().toString());
1394    }
1395
1396    if (jstackCountArg.isPresent())
1397    {
1398      argList.add("--maxJstacks");
1399      argList.add(jstackCountArg.getValue().toString());
1400    }
1401
1402    if (collectExpensiveDataArg.isPresent())
1403    {
1404      argList.add("--collectExpensiveData");
1405    }
1406
1407    if (collectReplicationStateDumpArg.isPresent())
1408    {
1409      argList.add("--collectReplicationStateDump");
1410    }
1411
1412    if (commentArg.isPresent())
1413    {
1414      argList.add("--comment");
1415      argList.add(commentArg.getValue());
1416    }
1417
1418    if (includeBinaryFilesArg.isPresent())
1419    {
1420      argList.add("--includeBinaryFiles");
1421    }
1422
1423    if (securityLevelArg.isPresent())
1424    {
1425      argList.add("--securityLevel");
1426      argList.add(securityLevelArg.getValue());
1427    }
1428
1429    if (encryptArg.isPresent())
1430    {
1431      argList.add("--encrypt");
1432    }
1433
1434    if (passphraseFileArg.isPresent())
1435    {
1436      argList.add("--passphraseFile");
1437      argList.add(passphraseFileArg.getValue().getAbsolutePath());
1438    }
1439
1440    if (generatePassphraseArg.isPresent())
1441    {
1442      argList.add("--generatePassphrase");
1443    }
1444
1445    if (decryptArg.isPresent())
1446    {
1447      argList.add("--decrypt");
1448      argList.add(decryptArg.getValue().getAbsolutePath());
1449    }
1450
1451    if (logTimeRangeArg.isPresent())
1452    {
1453      final ObjectPair<Date,Date> timeRange;
1454      try
1455      {
1456        timeRange = parseTimeRange(logTimeRangeArg.getValue(), false);
1457      }
1458      catch (final LDAPException e)
1459      {
1460        // This should never happen because we should have pre-validated the
1461        // value.  But handle it just in case.
1462        Debug.debugException(e);
1463        wrapErr(0, WRAP_COLUMN, e.getMessage());
1464        return e.getResultCode();
1465      }
1466
1467      if (timeRange == null)
1468      {
1469        // We'll assume that this means the time range was specified using
1470        // rotated log filenames, which we can't handle in the LDAP SDK code so
1471        // we'll just pass the argument value through to the server code.
1472        argList.add("--timeRange");
1473        argList.add(logTimeRangeArg.getValue());
1474      }
1475      else
1476      {
1477        final Date startTime = timeRange.getFirst();
1478        Date endTime = timeRange.getSecond();
1479        if (endTime == null)
1480        {
1481          endTime = new Date(Math.max(System.currentTimeMillis(),
1482               startTime.getTime()));
1483        }
1484
1485        final SimpleDateFormat timestampFormatter =
1486             new SimpleDateFormat(SERVER_LOG_TIMESTAMP_FORMAT_WITH_MILLIS);
1487        argList.add("--timeRange");
1488        argList.add(timestampFormatter.format(startTime) + ',' +
1489             timestampFormatter.format(endTime));
1490      }
1491    }
1492
1493    if (logDurationArg.isPresent())
1494    {
1495      argList.add("--duration");
1496      argList.add(DurationArgument.nanosToDuration(
1497           logDurationArg.getValue(TimeUnit.NANOSECONDS)));
1498    }
1499
1500    if (logFileHeadCollectionSizeKBArg.isPresent())
1501    {
1502      argList.add("--logFileHeadCollectionSizeKB");
1503      argList.add(String.valueOf(logFileHeadCollectionSizeKBArg.getValue()));
1504    }
1505
1506    if (logFileTailCollectionSizeKBArg.isPresent())
1507    {
1508      argList.add("--logFileTailCollectionSizeKB");
1509      argList.add(String.valueOf(logFileTailCollectionSizeKBArg.getValue()));
1510    }
1511
1512    if (archiveExtensionSourceArg.isPresent())
1513    {
1514      argList.add("--archiveExtensionSource");
1515    }
1516
1517    if (noPromptArg.isPresent())
1518    {
1519      argList.add("--no-prompt");
1520    }
1521
1522    if (scriptFriendlyArg.isPresent())
1523    {
1524      argList.add("--script-friendly");
1525    }
1526
1527
1528    // We also need to include values for arguments provided by the LDAP
1529    // command-line tool framework.
1530    for (final String argumentIdentifier :
1531         Arrays.asList("hostname", "port", "bindDN", "bindPassword",
1532              "bindPasswordFile", "useSSL", "useStartTLS", "trustAll",
1533              "keyStorePath", "keyStorePassword", "keyStorePasswordFile",
1534              "keyStoreFormat", "trustStorePath", "trustStorePassword",
1535              "trustStorePasswordFile", "trustStoreFormat", "certNickname",
1536              "saslOption", "propertiesFilePath", "noPropertiesFile"))
1537    {
1538      final Argument arg = parser.getNamedArgument(argumentIdentifier);
1539      if (arg.getNumOccurrences() > 0)
1540      {
1541        for (final String value : arg.getValueStringRepresentations(false))
1542        {
1543          argList.add("--" + argumentIdentifier);
1544          if (arg.takesValue())
1545          {
1546            argList.add(value);
1547          }
1548        }
1549      }
1550    }
1551
1552
1553    // If the --dryRun argument was provided, then return without actually
1554    // invoking the tool.
1555    if (dryRunArg.isPresent())
1556    {
1557      final String message = INFO_CSD_LOCAL_MODE_DRY_RUN.get();
1558      wrapOut(0, WRAP_COLUMN, message);
1559      toolCompletionMessage.set(message);
1560      return ResultCode.NO_OPERATION;
1561    }
1562
1563
1564    // Make sure that we have access to the method in the server codebase that
1565    // we need to invoke local collect-support-data processing.
1566    final Method doMainMethod;
1567    try
1568    {
1569      final Class<?> csdToolClass = Class.forName(SERVER_CSD_TOOL_CLASS);
1570      doMainMethod = csdToolClass.getMethod("doMain", Boolean.TYPE,
1571           OutputStream.class, OutputStream.class, String[].class);
1572    }
1573    catch (final Throwable t)
1574    {
1575      Debug.debugException(t);
1576      final String message = ERR_CSD_SERVER_CODE_NOT_AVAILABLE.get();
1577      wrapErr(0, WRAP_COLUMN, message);
1578      toolCompletionMessage.set(message);
1579      return ResultCode.NOT_SUPPORTED;
1580    }
1581
1582
1583    // Invoke the doMain method via reflection
1584    final String[] argArray = new String[argList.size()];
1585    argList.toArray(argArray);
1586
1587    try
1588    {
1589      final Object doMainMethodReturnValue = doMainMethod.invoke(null,
1590           true, getOut(), getErr(), argArray);
1591      final int exitCode = ((Integer) doMainMethodReturnValue).intValue();
1592      return ResultCode.valueOf(exitCode);
1593    }
1594    catch (final Throwable t)
1595    {
1596      Debug.debugException(t);
1597      final String message =
1598           ERR_CSD_INVOKE_ERROR.get(StaticUtils.getExceptionMessage(t));
1599      wrapErr(0, WRAP_COLUMN, message);
1600      toolCompletionMessage.set(message);
1601      return ResultCode.LOCAL_ERROR;
1602    }
1603  }
1604
1605
1606
1607  /**
1608   * {@inheritDoc}
1609   */
1610  @Override()
1611  @NotNull()
1612  public LinkedHashMap<String[],String> getExampleUsages()
1613  {
1614    final LinkedHashMap<String[],String> examples =
1615         new LinkedHashMap<>(StaticUtils.computeMapCapacity(3));
1616
1617    examples.put(
1618         new String[]
1619         {
1620           "--bindDN", "uid=admin,dc=example,dc=com",
1621           "--bindPasswordFile", "admin-pw.txt"
1622         },
1623         INFO_CSD_EXAMPLE_1.get());
1624
1625    examples.put(
1626         new String[]
1627         {
1628           "--useRemoteServer",
1629           "--hostname", "ds.example.com",
1630           "--port", "636",
1631           "--useSSL",
1632           "--trustStorePath", "config/truststore",
1633           "--bindDN", "uid=admin,dc=example,dc=com",
1634           "--bindPasswordFile", "admin-pw.txt",
1635           "--collectExpensiveData",
1636           "--collectReplicationStateDump",
1637           "--securityLevel", "maximum",
1638           "--logDuration", "10 minutes",
1639           "--encrypt",
1640           "--passphraseFile", "encryption-passphrase.txt",
1641           "--generatePassphrase",
1642           "--outputPath", "csd.zip"
1643         },
1644         INFO_CSD_EXAMPLE_2.get());
1645
1646    examples.put(
1647         new String[]
1648         {
1649           "--decrypt", "support-data-ds-inst1-" +
1650                StaticUtils.encodeGeneralizedTime(new Date()) +
1651              "-zip-encrypted",
1652           "--passphraseFile", "encryption-passphrase.txt"
1653         },
1654         INFO_CSD_EXAMPLE_3.get());
1655
1656    return examples;
1657  }
1658}