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 supportsDebugLogging()
384  {
385    return true;
386  }
387
388
389
390  /**
391   * {@inheritDoc}
392   */
393  @Override()
394  protected boolean defaultToPromptForBindPassword()
395  {
396    if ((noPromptArg != null) && noPromptArg.isPresent())
397    {
398      return false;
399    }
400
401    return true;
402  }
403
404
405
406  /**
407   * {@inheritDoc}
408   */
409  @Override()
410  protected boolean includeAlternateLongIdentifiers()
411  {
412    return true;
413  }
414
415
416
417  /**
418   * {@inheritDoc}
419   */
420  @Override()
421  protected boolean supportsSSLDebugging()
422  {
423    return true;
424  }
425
426
427
428  /**
429   * {@inheritDoc}
430   */
431  @Override()
432  protected boolean logToolInvocationByDefault()
433  {
434    return true;
435  }
436
437
438
439  /**
440   * {@inheritDoc}
441   */
442  @Override()
443  @Nullable()
444  protected String getToolCompletionMessage()
445  {
446    return toolCompletionMessage.get();
447  }
448
449
450
451  /**
452   * {@inheritDoc}
453   */
454  @Override()
455  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
456         throws ArgumentException
457  {
458    this.parser = parser;
459
460    // Output-related arguments.
461    outputPathArg = new FileArgument(null, "outputPath", false, 1, null,
462         INFO_CSD_ARG_DESC_OUTPUT_PATH.get(), false, true, false, false);
463    outputPathArg.addLongIdentifier("output-path", true);
464    outputPathArg.addLongIdentifier("outputFile", true);
465    outputPathArg.addLongIdentifier("output-file", true);
466    outputPathArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
467    parser.addArgument(outputPathArg);
468
469    encryptArg = new BooleanArgument(null, "encrypt", 1,
470         INFO_CSD_ARG_DESC_ENCRYPT.get());
471    encryptArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
472    parser.addArgument(encryptArg);
473
474    passphraseFileArg = new FileArgument(null, "passphraseFile", false, 1,
475         null, INFO_CSD_ARG_DESC_PASSPHRASE_FILE.get(), false, true, true,
476         false);
477    passphraseFileArg.addLongIdentifier("passphrase-file", true);
478    passphraseFileArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
479    parser.addArgument(passphraseFileArg);
480
481    generatePassphraseArg = new BooleanArgument(null, "generatePassphrase", 1,
482         INFO_CSD_ARG_DESC_GENERATE_PASSPHRASE.get());
483    generatePassphraseArg.addLongIdentifier("generate-passphrase", true);
484    generatePassphraseArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
485    parser.addArgument(generatePassphraseArg);
486
487    decryptArg = new FileArgument(null, "decrypt", false, 1, null,
488         INFO_CSD_ARG_DESC_DECRYPT.get(), false, true, true, false);
489    decryptArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_OUTPUT.get());
490    parser.addArgument(decryptArg);
491
492
493    // Collection-related arguments.
494    collectExpensiveDataArg = new BooleanArgument(null, "collectExpensiveData",
495         1, INFO_CSD_ARG_DESC_COLLECT_EXPENSIVE_DATA.get());
496    collectExpensiveDataArg.addLongIdentifier("collect-expensive-data", true);
497    collectExpensiveDataArg.addLongIdentifier("includeExpensiveData", true);
498    collectExpensiveDataArg.addLongIdentifier("include-expensive-data", true);
499    collectExpensiveDataArg.setArgumentGroupName(
500         INFO_CSD_ARG_GROUP_COLLECTION.get());
501    parser.addArgument(collectExpensiveDataArg);
502
503    collectReplicationStateDumpArg = new BooleanArgument(null,
504         "collectReplicationStateDump", 1,
505         INFO_CSD_ARG_DESC_COLLECT_REPL_STATE.get());
506    collectReplicationStateDumpArg.addLongIdentifier(
507         "collect-replication-state-dump", true);
508    collectReplicationStateDumpArg.addLongIdentifier(
509         "collectReplicationState", true);
510    collectReplicationStateDumpArg.addLongIdentifier(
511         "collect-replication-state", true);
512    collectReplicationStateDumpArg.addLongIdentifier(
513         "includeReplicationStateDump", true);
514    collectReplicationStateDumpArg.addLongIdentifier(
515         "include-replication-state-dump", true);
516    collectReplicationStateDumpArg.addLongIdentifier(
517         "includeReplicationState", true);
518    collectReplicationStateDumpArg.addLongIdentifier(
519         "include-replication-state", true);
520    collectReplicationStateDumpArg.setArgumentGroupName(
521         INFO_CSD_ARG_GROUP_COLLECTION.get());
522    parser.addArgument(collectReplicationStateDumpArg);
523
524    includeBinaryFilesArg = new BooleanArgument(null, "includeBinaryFiles", 1,
525         INFO_CSD_ARG_DESC_INCLUDE_BINARY_FILES.get());
526    includeBinaryFilesArg.addLongIdentifier("include-binary-files", true);
527    includeBinaryFilesArg.setArgumentGroupName(
528         INFO_CSD_ARG_GROUP_COLLECTION.get());
529    parser.addArgument(includeBinaryFilesArg);
530
531    archiveExtensionSourceArg = new BooleanArgument(null,
532         "archiveExtensionSource", 1,
533         INFO_CSD_ARG_DESC_ARCHIVE_EXTENSION_SOURCE.get());
534    archiveExtensionSourceArg.addLongIdentifier("archive-extension-source",
535         true);
536    archiveExtensionSourceArg.addLongIdentifier("includeExtensionSource", true);
537    archiveExtensionSourceArg.addLongIdentifier("include-extension-source",
538         true);
539    archiveExtensionSourceArg.setArgumentGroupName(
540         INFO_CSD_ARG_GROUP_COLLECTION.get());
541    parser.addArgument(archiveExtensionSourceArg);
542
543    sequentialArg = new BooleanArgument(null, "sequential", 1,
544         INFO_CSD_ARG_DESC_SEQUENTIAL.get());
545    sequentialArg.addLongIdentifier("sequentialMode", true);
546    sequentialArg.addLongIdentifier("sequential-mode", true);
547    sequentialArg.addLongIdentifier("useSequentialMode", true);
548    sequentialArg.addLongIdentifier("use-sequential-mode", true);
549    sequentialArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
550    parser.addArgument(sequentialArg);
551
552    securityLevelArg = new StringArgument(null, "securityLevel", false, 1,
553         INFO_CSD_ARG_PLACEHOLDER_SECURITY_LEVEL.get(),
554         INFO_CSD_ARG_DESC_SECURITY_LEVEL.get(),
555         StaticUtils.setOf(
556              CollectSupportDataSecurityLevel.NONE.getName(),
557              CollectSupportDataSecurityLevel.OBSCURE_SECRETS.getName(),
558              CollectSupportDataSecurityLevel.MAXIMUM.getName()));
559    securityLevelArg.addLongIdentifier("security-level", true);
560    securityLevelArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
561    parser.addArgument(securityLevelArg);
562
563    jstackCountArg = new IntegerArgument(null, "jstackCount", false, 1,
564         INFO_CSD_ARG_PLACEHOLDER_COUNT.get(),
565         INFO_CSD_ARG_DESC_JSTACK_COUNT.get(), 0, Integer.MAX_VALUE);
566    jstackCountArg.addLongIdentifier("jstack-count", true);
567    jstackCountArg.addLongIdentifier("maxJstacks", true);
568    jstackCountArg.addLongIdentifier("max-jstacks", true);
569    jstackCountArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
570    parser.addArgument(jstackCountArg);
571
572    reportCountArg = new IntegerArgument(null, "reportCount", false, 1,
573         INFO_CSD_ARG_PLACEHOLDER_COUNT.get(),
574         INFO_CSD_ARG_DESC_REPORT_COUNT.get(), 0, Integer.MAX_VALUE);
575    reportCountArg.addLongIdentifier("report-count", true);
576    reportCountArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
577    parser.addArgument(reportCountArg);
578
579    reportIntervalSecondsArg = new IntegerArgument(null,
580         "reportIntervalSeconds", false, 1,
581         INFO_CSD_ARG_PLACEHOLDER_SECONDS.get(),
582         INFO_CSD_ARG_DESC_REPORT_INTERVAL_SECONDS.get(), 1,
583         Integer.MAX_VALUE);
584    reportIntervalSecondsArg.addLongIdentifier("report-interval-seconds", true);
585    reportIntervalSecondsArg.addLongIdentifier("reportInterval", true);
586    reportIntervalSecondsArg.addLongIdentifier("report-interval", true);
587    reportIntervalSecondsArg.setArgumentGroupName(
588         INFO_CSD_ARG_GROUP_COLLECTION.get());
589    parser.addArgument(reportIntervalSecondsArg);
590
591    logTimeRangeArg = new StringArgument(null, "logTimeRange", false, 1,
592         INFO_CSD_ARG_PLACEHOLDER_TIME_RANGE.get(),
593         INFO_CSD_ARG_DESC_TIME_RANGE.get());
594    logTimeRangeArg.addLongIdentifier("log-time-range", true);
595    logTimeRangeArg.addLongIdentifier("timeRange", true);
596    logTimeRangeArg.addLongIdentifier("time-range", true);
597    logTimeRangeArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
598    parser.addArgument(logTimeRangeArg);
599
600    logDurationArg = new DurationArgument(null, "logDuration", false, null,
601         INFO_CSD_ARG_DESC_DURATION.get());
602    logDurationArg.addLongIdentifier("log-duration", true);
603    logDurationArg.addLongIdentifier("duration", true);
604    logDurationArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
605    parser.addArgument(logDurationArg);
606
607    logFileHeadCollectionSizeKBArg = new IntegerArgument(null,
608         "logFileHeadCollectionSizeKB", false, 1,
609         INFO_CSD_ARG_PLACEHOLDER_SIZE_KB.get(),
610         INFO_CSD_ARG_DESC_LOG_HEAD_SIZE_KB.get(), 0, Integer.MAX_VALUE);
611    logFileHeadCollectionSizeKBArg.addLongIdentifier(
612         "log-file-head-collection-size-kb", true);
613    logFileHeadCollectionSizeKBArg.addLongIdentifier("logFileHeadSizeKB", true);
614    logFileHeadCollectionSizeKBArg.addLongIdentifier("log-file-head-size-kb",
615         true);
616    logFileHeadCollectionSizeKBArg.addLongIdentifier("logHeadCollectionSizeKB",
617         true);
618    logFileHeadCollectionSizeKBArg.addLongIdentifier(
619         "log-head-collection-size-kb", true);
620    logFileHeadCollectionSizeKBArg.addLongIdentifier("logHeadSizeKB", true);
621    logFileHeadCollectionSizeKBArg.addLongIdentifier("log-head-size-kb", true);
622    logFileHeadCollectionSizeKBArg.setArgumentGroupName(
623         INFO_CSD_ARG_GROUP_COLLECTION.get());
624    parser.addArgument(logFileHeadCollectionSizeKBArg);
625
626    logFileTailCollectionSizeKBArg = new IntegerArgument(null,
627         "logFileTailCollectionSizeKB", false, 1,
628         INFO_CSD_ARG_PLACEHOLDER_SIZE_KB.get(),
629         INFO_CSD_ARG_DESC_LOG_TAIL_SIZE_KB.get(), 0, Integer.MAX_VALUE);
630    logFileTailCollectionSizeKBArg.addLongIdentifier(
631         "log-file-tail-collection-size-kb", true);
632    logFileTailCollectionSizeKBArg.addLongIdentifier("logFileTailSizeKB", true);
633    logFileTailCollectionSizeKBArg.addLongIdentifier("log-file-tail-size-kb",
634         true);
635    logFileTailCollectionSizeKBArg.addLongIdentifier("logTailCollectionSizeKB",
636         true);
637    logFileTailCollectionSizeKBArg.addLongIdentifier(
638         "log-tail-collection-size-kb", true);
639    logFileTailCollectionSizeKBArg.addLongIdentifier("logTailSizeKB", true);
640    logFileTailCollectionSizeKBArg.addLongIdentifier("log-tail-size-kb", true);
641    logFileTailCollectionSizeKBArg.setArgumentGroupName(
642         INFO_CSD_ARG_GROUP_COLLECTION.get());
643    parser.addArgument(logFileTailCollectionSizeKBArg);
644
645    pidArg = new IntegerArgument(null, "pid", false, 0,
646         INFO_CSD_ARG_PLACEHOLDER_PID.get(), INFO_CSD_ARG_DESC_PID.get());
647    pidArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
648    parser.addArgument(pidArg);
649
650    commentArg = new StringArgument(null, "comment", false, 1, null,
651         INFO_CSD_ARG_DESC_COMMENT.get());
652    commentArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COLLECTION.get());
653    parser.addArgument(commentArg);
654
655
656    // Communication-related arguments.
657    useRemoteServerArg = new BooleanArgument(null, "useRemoteServer", 1,
658         INFO_CSD_ARG_DEC_USE_REMOTE_SERVER.get());
659    useRemoteServerArg.addLongIdentifier("use-remote-server", true);
660    useRemoteServerArg.setArgumentGroupName(
661         INFO_CSD_ARG_GROUP_COMMUNICATION.get());
662    parser.addArgument(useRemoteServerArg);
663
664    useAdministrativeSessionArg = new BooleanArgument(null,
665         "useAdministrativeSession", 1,
666         INFO_CSD_ARG_DESC_USE_ADMIN_SESSION.get());
667    useAdministrativeSessionArg.addLongIdentifier("use-administrative-session",
668         true);
669    useAdministrativeSessionArg.addLongIdentifier("useAdminSession", true);
670    useAdministrativeSessionArg.addLongIdentifier("use-admin-session",
671         true);
672    useAdministrativeSessionArg.addLongIdentifier("administrativeSession",
673         true);
674    useAdministrativeSessionArg.addLongIdentifier("administrative-session",
675         true);
676    useAdministrativeSessionArg.addLongIdentifier("adminSession", true);
677    useAdministrativeSessionArg.addLongIdentifier("admin-session", true);
678    useAdministrativeSessionArg.setArgumentGroupName(
679         INFO_CSD_ARG_GROUP_COMMUNICATION.get());
680    parser.addArgument(useAdministrativeSessionArg);
681
682    proxyToServerAddressArg = new StringArgument(null, "proxyToServerAddress",
683         false, 1, INFO_CSD_ARG_PLACEHOLDER_ADDRESS.get(),
684         INFO_CSD_ARG_DESC_PROXY_TO_ADDRESS.get());
685    proxyToServerAddressArg.addLongIdentifier("proxy-to-server-address", true);
686    proxyToServerAddressArg.addLongIdentifier("proxyToAddress", true);
687    proxyToServerAddressArg.addLongIdentifier("proxy-to-address", true);
688    proxyToServerAddressArg.setArgumentGroupName(
689         INFO_CSD_ARG_GROUP_COMMUNICATION.get());
690    parser.addArgument(proxyToServerAddressArg);
691
692    proxyToServerPortArg = new IntegerArgument(null, "proxyToServerPort", false,
693         1, INFO_CSD_ARG_PLACEHOLDER_PORT.get(),
694         INFO_CSD_ARG_DESC_PROXY_TO_PORT.get(), 1, 65535);
695    proxyToServerPortArg.addLongIdentifier("proxy-to-server-port", true);
696    proxyToServerPortArg.addLongIdentifier("proxyToPort", true);
697    proxyToServerPortArg.addLongIdentifier("proxy-to-port", true);
698    proxyToServerPortArg.setArgumentGroupName(
699         INFO_CSD_ARG_GROUP_COMMUNICATION.get());
700    parser.addArgument(proxyToServerPortArg);
701
702    noLDAPArg = new BooleanArgument(null, "noLDAP", 1,
703         INFO_CSD_ARG_DESC_NO_LDAP.get());
704    noLDAPArg.addLongIdentifier("no-ldap", true);
705    noLDAPArg.setArgumentGroupName(INFO_CSD_ARG_GROUP_COMMUNICATION.get());
706    parser.addArgument(noLDAPArg);
707
708
709    // Other arguments.
710    noPromptArg = new BooleanArgument('n', "noPrompt",  1,
711         INFO_CSD_ARG_DESC_NO_PROMPT.get());
712    noPromptArg.addLongIdentifier("no-prompt", true);
713    parser.addArgument(noPromptArg);
714
715    dryRunArg = new BooleanArgument(null, "dryRun", 1,
716         INFO_CSD_ARG_DESC_DRY_RUN.get());
717    dryRunArg.addLongIdentifier("dry-run", true);
718    dryRunArg.addLongIdentifier("noOperation", true);
719    dryRunArg.addLongIdentifier("no-operation", true);
720    dryRunArg.addLongIdentifier("noOp", true);
721    dryRunArg.addLongIdentifier("no-op", true);
722    parser.addArgument(dryRunArg);
723
724    scriptFriendlyArg = new BooleanArgument(null, "scriptFriendly", 1,
725         INFO_CSD_ARG_DESC_SCRIPT_FRIENDLY.get());
726    scriptFriendlyArg.addLongIdentifier("script-friendly", true);
727    scriptFriendlyArg.setHidden(true);
728    parser.addArgument(scriptFriendlyArg);
729
730
731    // If the --useRemoteServer argument is provided, then none of the --pid,
732    // --decrypt, --noLDAP, or --scriptFriendly arguments may be given.
733    parser.addExclusiveArgumentSet(useRemoteServerArg, pidArg);
734    parser.addExclusiveArgumentSet(useRemoteServerArg, decryptArg);
735    parser.addExclusiveArgumentSet(useRemoteServerArg, noLDAPArg);
736    parser.addExclusiveArgumentSet(useRemoteServerArg, scriptFriendlyArg);
737
738    // The --useAdministrativeSession argument can only be provided if the
739    // --useRemoteServer argument is also given.
740    parser.addDependentArgumentSet(useAdministrativeSessionArg,
741         useRemoteServerArg);
742
743    // If the --proxyToServerAddress or --proxyToServerPort argument is given,
744    // then the other must be provided as well.
745    parser.addMutuallyDependentArgumentSet(proxyToServerAddressArg,
746         proxyToServerPortArg);
747
748    // The --proxyToServerAddress and --proxyToServerPort arguments can only
749    // be used if the --useRemoteServer argument is given.
750    parser.addDependentArgumentSet(proxyToServerAddressArg, useRemoteServerArg);
751    parser.addDependentArgumentSet(proxyToServerPortArg, useRemoteServerArg);
752
753    // The --logTimeRange and --logDuration arguments cannot be used together.
754    parser.addExclusiveArgumentSet(logTimeRangeArg, logDurationArg);
755
756    // Neither the --logFileHeadCollectionSizeKB argument nor the
757    // --logFileTailCollectionSizeKB argument can be used in conjunction with
758    // either the --logTimeRange or --logDuration argument.
759    parser.addExclusiveArgumentSet(logFileHeadCollectionSizeKBArg,
760         logTimeRangeArg, logDurationArg);
761    parser.addExclusiveArgumentSet(logFileTailCollectionSizeKBArg,
762         logTimeRangeArg, logDurationArg);
763
764    // The --generatePassphrase argument can only be used if both the
765    // --encrypt and --passphraseFile arguments are provided.
766    parser.addDependentArgumentSet(generatePassphraseArg, encryptArg);
767    parser.addDependentArgumentSet(generatePassphraseArg, passphraseFileArg);
768
769    // The --encrypt and --decrypt arguments cannot be used together.
770    parser.addExclusiveArgumentSet(encryptArg, decryptArg);
771
772
773    // There are several arguments that the LDAP SDK's LDAP command-line tool
774    // framework offers that the server-side version of the framework does not
775    // provide.  Those arguments can only be used in conjunction with the
776    // --useRemoteServer argument.
777    for (final String argumentIdentifier :
778         Arrays.asList("promptForBindPassword", "promptForKeyStorePassword",
779              "promptForTrustStorePassword", "enableSSLDebugging",
780              "useSASLExternal"))
781    {
782      final Argument arg = parser.getNamedArgument(argumentIdentifier);
783      parser.addDependentArgumentSet(arg, useRemoteServerArg);
784    }
785  }
786
787
788
789  /**
790   * {@inheritDoc}
791   */
792  @Override()
793  public void doExtendedNonLDAPArgumentValidation()
794         throws ArgumentException
795  {
796    // If the --logTimeRange argument was provided, then make sure we can
797    // parse each of the values and that the end time is greater than or equal
798    // to the start time.
799    if (logTimeRangeArg.isPresent())
800    {
801      try
802      {
803        parseTimeRange(logTimeRangeArg.getValue(),
804             useRemoteServerArg.isPresent());
805      }
806      catch (final LDAPException e)
807      {
808        Debug.debugException(e);
809        toolCompletionMessage.set(e.getMessage());
810        throw new ArgumentException(e.getMessage(), e);
811      }
812    }
813
814
815    // If the --passphraseFile argument was provided without the
816    // --generatePassphrase argument, then make sure the file exists.
817    if (passphraseFileArg.isPresent() && (! generatePassphraseArg.isPresent()))
818    {
819      final File passphraseFile = passphraseFileArg.getValue();
820      if (! passphraseFile.exists())
821      {
822        final String message =ERR_CSD_PASSPHRASE_FILE_MISSING.get(
823             passphraseFile.getAbsolutePath());
824        toolCompletionMessage.set(message);
825        throw new ArgumentException(message);
826      }
827    }
828
829
830    // If either the --encrypt or --decrypt argument is provided in conjunction
831    // with the --noPrompt argument, then the --passphraseFile argument must
832    // also have been provided.
833    if (noPromptArg.isPresent() &&
834         (encryptArg.isPresent() || decryptArg.isPresent()) &&
835         (! passphraseFileArg.isPresent()))
836    {
837      final String message = ERR_CSD_NO_PASSPHRASE_WITH_NO_PROMPT.get();
838      toolCompletionMessage.set(message);
839      throw new ArgumentException(message);
840    }
841  }
842
843
844
845  /**
846   * Parses the provided string as a time range.  If both start and end time
847   * values are provided, then they must be separated by a comma; otherwise,
848   * there must only be a start time value.  Each timestamp must be in either
849   * the generalized time format or the Ping Identity Directory Server's default
850   * access log format (with or without millisecond precision).
851   *
852   * @param  timeRangeStr  The string to be parsed as a time range.  It must not
853   *                       be {@code null}.
854   * @param  strict        Indicates whether to require strict compliance with
855   *                       the timestamp format.  This should be {@code true}
856   *                       when the useRemoteServer argument was provided, and
857   *                       {@code false} otherwise.
858   *
859   * @return  An object pair in which the first value is the start time for
860   *          the range and the second value is the end time for the range.  The
861   *          first element will always be non-{@code null}, but the second
862   *          element may be {@code null} if the time range did not specify an
863   *          end time.  The entire return value may be {@code null} if the
864   *          time range string could not be parsed and {@code strict} is
865   *          {@code false}.
866   *
867   * @throws  LDAPException  If a problem is encountered while parsing the
868   *                         provided string as a time range, or if the start
869   *                         time is greater than the end time.
870   */
871  @Nullable()
872  static ObjectPair<Date,Date> parseTimeRange(
873              @NotNull final String timeRangeStr,
874              final boolean strict)
875         throws LDAPException
876  {
877    final Date startTime;
878    final Date endTime;
879
880    try
881    {
882      // See if there is a comma to separate the before and after times.  If so,
883      // then parse each value separately.  Otherwise, the value will be just
884      // the start time and the current time will be used as the end time.
885      final int commaPos = timeRangeStr.indexOf(',');
886      if (commaPos > 0)
887      {
888        startTime = parseTimestamp(timeRangeStr.substring(0, commaPos).trim());
889        endTime = parseTimestamp(timeRangeStr.substring(commaPos+1).trim());
890      }
891      else
892      {
893        startTime = parseTimestamp(timeRangeStr);
894        endTime = null;
895      }
896    }
897    catch (final LDAPException e)
898    {
899      Debug.debugException(e);
900
901      // NOTE:  The server-side version of the collect-support-data tool has a
902      // not-so-documented feature in which you can provide rotated file names
903      // as an alternative to an actual time range.  We can't handle that
904      // when operating against a remote server, so we'll require strict
905      // timestamp compliance when --useRemoteServer is provided, but we'll just
906      // return null and let the argument value be passed through to the
907      // server-side code otherwise.
908      if (strict)
909      {
910        throw e;
911      }
912      else
913      {
914        return null;
915      }
916    }
917
918    if ((endTime != null) && (startTime.getTime() > endTime.getTime()))
919    {
920      throw new LDAPException(ResultCode.PARAM_ERROR,
921           ERR_CSD_TIME_RANGE_START_GREATER_THAN_END.get());
922    }
923
924    return new ObjectPair<>(startTime, endTime);
925  }
926
927
928
929  /**
930   * Parses the provided string as a timestamp value in either the generalized
931   * time format or the Ping Identity Directory Server's default access log
932   * format (with or without millisecond precision).
933   *
934   * @param  timestampStr  The timestamp to be parsed.  It must not be
935   *                       {@code null}.
936   *
937   * @return  The {@code Date} created by parsing the timestamp.
938   *
939   * @throws  LDAPException  If the provided string cannot be parsed as a
940   *                         valid timestamp.
941   */
942  @NotNull()
943  static Date parseTimestamp(@NotNull final String timestampStr)
944         throws LDAPException
945  {
946    // First, try using the timestamp argument to parse the timestamp.
947    try
948    {
949      return TimestampArgument.parseTimestamp(timestampStr);
950    }
951    catch (final Exception e)
952    {
953      Debug.debugException(Level.FINEST, e);
954    }
955
956
957    // Next, try the server's default access log format with millisecond
958    // precision.
959    try
960    {
961      final SimpleDateFormat timestampFormatter =
962           new SimpleDateFormat(SERVER_LOG_TIMESTAMP_FORMAT_WITH_MILLIS);
963      timestampFormatter.setLenient(false);
964      return timestampFormatter.parse(timestampStr);
965    }
966    catch (final Exception e)
967    {
968      Debug.debugException(Level.FINEST, e);
969    }
970
971
972    // Next, try the server's default access log format with second precision.
973    try
974    {
975      final SimpleDateFormat timestampFormatter =
976           new SimpleDateFormat(SERVER_LOG_TIMESTAMP_FORMAT_WITHOUT_MILLIS);
977      timestampFormatter.setLenient(false);
978      return timestampFormatter.parse(timestampStr);
979    }
980    catch (final Exception e)
981    {
982      Debug.debugException(Level.FINEST, e);
983    }
984
985
986    // If we've gotten here, then we could not parse the timestamp.
987    throw new LDAPException(ResultCode.PARAM_ERROR,
988         ERR_CSD_TIME_RANGE_CANNOT_PARSE_TIMESTAMP.get(timestampStr));
989  }
990
991
992
993  /**
994   * {@inheritDoc}
995   */
996  @Override()
997  @NotNull()
998  public ResultCode doToolProcessing()
999  {
1000    // If the --useRemoteServer argument was provided, then use the extended
1001    // operation to perform the processing.  Otherwise, use reflection to invoke
1002    // the server's version of the collect-support-data tool.
1003    if (useRemoteServerArg.isPresent())
1004    {
1005      return doExtendedOperationProcessing();
1006    }
1007    else
1008    {
1009      return doLocalProcessing();
1010    }
1011  }
1012
1013
1014
1015  /**
1016   * Performs the collect-support-data processing against a remote server using
1017   * the extended operation.
1018   *
1019   * @return  A result code that indicates the result of the processing.
1020   */
1021  @NotNull()
1022  private ResultCode doExtendedOperationProcessing()
1023  {
1024    // Create a connection pool that will be used to communicate with the
1025    // server.  Use an administrative session if appropriate.
1026    final StartAdministrativeSessionPostConnectProcessor p;
1027    if (useAdministrativeSessionArg.isPresent())
1028    {
1029      p = new StartAdministrativeSessionPostConnectProcessor(
1030           new StartAdministrativeSessionExtendedRequest(getToolName(),
1031                true));
1032    }
1033    else
1034    {
1035      p = null;
1036    }
1037
1038    final LDAPConnectionPool pool;
1039    try
1040    {
1041      pool = getConnectionPool(1, 1, 0, p, null, true,
1042           new ReportBindResultLDAPConnectionPoolHealthCheck(this, true,
1043                false));
1044    }
1045    catch (final LDAPException e)
1046    {
1047      Debug.debugException(e);
1048      wrapErr(0, WRAP_COLUMN, e.getMessage());
1049      toolCompletionMessage.set(e.getMessage());
1050      return e.getResultCode();
1051    }
1052
1053
1054    try
1055    {
1056      // Create the properties to use for the extended request.
1057      final CollectSupportDataExtendedRequestProperties properties =
1058           new CollectSupportDataExtendedRequestProperties();
1059
1060      final File outputPath = outputPathArg.getValue();
1061      if (outputPath != null)
1062      {
1063        if (! (outputPath.exists() && outputPath.isDirectory()))
1064        {
1065          properties.setArchiveFileName(outputPath.getName());
1066        }
1067      }
1068
1069      try
1070      {
1071        properties.setEncryptionPassphrase(
1072             getEncryptionPassphraseForExtOpProcessing());
1073      }
1074      catch (final LDAPException e)
1075      {
1076        Debug.debugException(e);
1077        wrapErr(0, WRAP_COLUMN, e.getMessage());
1078        toolCompletionMessage.set(e.getMessage());
1079        return e.getResultCode();
1080      }
1081
1082
1083      properties.setIncludeExpensiveData(collectExpensiveDataArg.isPresent());
1084      properties.setIncludeReplicationStateDump(
1085           collectReplicationStateDumpArg.isPresent());
1086      properties.setIncludeBinaryFiles(includeBinaryFilesArg.isPresent());
1087      properties.setIncludeExtensionSource(
1088           archiveExtensionSourceArg.isPresent());
1089      properties.setUseSequentialMode(sequentialArg.isPresent());
1090
1091      if (securityLevelArg.isPresent())
1092      {
1093        properties.setSecurityLevel(CollectSupportDataSecurityLevel.forName(
1094             securityLevelArg.getValue()));
1095      }
1096
1097      if (jstackCountArg.isPresent())
1098      {
1099        properties.setJStackCount(jstackCountArg.getValue());
1100      }
1101
1102      if (reportCountArg.isPresent())
1103      {
1104        properties.setReportCount(reportCountArg.getValue());
1105      }
1106
1107      if (reportIntervalSecondsArg.isPresent())
1108      {
1109        properties.setReportIntervalSeconds(
1110             reportIntervalSecondsArg.getValue());
1111      }
1112
1113      if (logTimeRangeArg.isPresent())
1114      {
1115        try
1116        {
1117          final ObjectPair<Date,Date> timeRange =
1118               parseTimeRange(logTimeRangeArg.getValue(), true);
1119          properties.setLogCaptureWindow(
1120               new TimeWindowCollectSupportDataLogCaptureWindow(
1121                    timeRange.getFirst(), timeRange.getSecond()));
1122        }
1123        catch (final LDAPException e)
1124        {
1125          // This should never happen because we should have pre-validated the
1126          // value.  But handle it just in case.
1127          Debug.debugException(e);
1128          wrapErr(0, WRAP_COLUMN, e.getMessage());
1129          toolCompletionMessage.set(e.getMessage());
1130          return e.getResultCode();
1131        }
1132      }
1133      else if (logDurationArg.isPresent())
1134      {
1135        properties.setLogCaptureWindow(
1136             new DurationCollectSupportDataLogCaptureWindow(
1137                  logDurationArg.getValue(TimeUnit.MILLISECONDS)));
1138      }
1139      else if (logFileHeadCollectionSizeKBArg.isPresent() ||
1140           logFileTailCollectionSizeKBArg.isPresent())
1141      {
1142        properties.setLogCaptureWindow(
1143             new HeadAndTailSizeCollectSupportDataLogCaptureWindow(
1144                  logFileHeadCollectionSizeKBArg.getValue(),
1145                  logFileTailCollectionSizeKBArg.getValue()));
1146      }
1147
1148      if (commentArg.isPresent())
1149      {
1150        properties.setComment(commentArg.getValue());
1151      }
1152
1153      if (proxyToServerAddressArg.isPresent())
1154      {
1155        properties.setProxyToServer(proxyToServerAddressArg.getValue(),
1156             proxyToServerPortArg.getValue());
1157      }
1158
1159
1160      // Create the intermediate response listener that will be used to handle
1161      // output and archive fragment messages.
1162      ResultCode resultCode = null;
1163      try (CollectSupportDataIRListener listener =
1164           new CollectSupportDataIRListener(this, outputPathArg.getValue()))
1165      {
1166        // Construct the extended request to send to the server.
1167        final Control[] controls;
1168        if (dryRunArg.isPresent())
1169        {
1170          controls = new Control[]
1171          {
1172            new NoOpRequestControl()
1173          };
1174        }
1175        else
1176        {
1177          controls = StaticUtils.NO_CONTROLS;
1178        }
1179
1180        final CollectSupportDataExtendedRequest request =
1181             new CollectSupportDataExtendedRequest(properties, listener,
1182                  controls);
1183        request.setResponseTimeoutMillis(0L);
1184
1185
1186        // Send the request and read the result.
1187        final CollectSupportDataExtendedResult result;
1188        try
1189        {
1190          result = (CollectSupportDataExtendedResult)
1191               pool.processExtendedOperation(request);
1192        }
1193        catch (final LDAPException e)
1194        {
1195          Debug.debugException(e);
1196          final String message = ERR_CSD_ERROR_SENDING_REQUEST.get(
1197               StaticUtils.getExceptionMessage(e));
1198          wrapErr(0, WRAP_COLUMN, message);
1199          toolCompletionMessage.set(message);
1200          return e.getResultCode();
1201        }
1202
1203
1204        resultCode = result.getResultCode();
1205        final String diagnosticMessage = result.getDiagnosticMessage();
1206        if (diagnosticMessage != null)
1207        {
1208          if ((resultCode == ResultCode.SUCCESS) ||
1209               (resultCode == ResultCode.NO_OPERATION))
1210          {
1211            wrapOut(0, WRAP_COLUMN, diagnosticMessage);
1212          }
1213          else
1214          {
1215            wrapErr(0, WRAP_COLUMN, diagnosticMessage);
1216          }
1217
1218          toolCompletionMessage.set(diagnosticMessage);
1219        }
1220        else
1221        {
1222          toolCompletionMessage.set(INFO_CSD_COMPLETED_WITH_RESULT_CODE.get(
1223               String.valueOf(resultCode)));
1224        }
1225      }
1226      catch (final IOException e)
1227      {
1228        Debug.debugException(e);
1229
1230        if (resultCode == ResultCode.SUCCESS)
1231        {
1232          resultCode = ResultCode.LOCAL_ERROR;
1233          toolCompletionMessage.set(e.getMessage());
1234        }
1235      }
1236
1237      return resultCode;
1238    }
1239    finally
1240    {
1241      pool.close();
1242    }
1243  }
1244
1245
1246
1247  /**
1248   * Retrieves the passphrase to use to generate the key for encrypting the
1249   * support data archive.  This method should only be used when the tool
1250   * processing will be performed using an extended operation.
1251   *
1252   * @return  The passphrase to use to generate the key for encrypting the
1253   *          support data archive.
1254   *
1255   * @throws  LDAPException  If a problem is encountered while attempting to
1256   *                         obtain the passphrase.
1257   */
1258  @Nullable()
1259  private ASN1OctetString getEncryptionPassphraseForExtOpProcessing()
1260          throws LDAPException
1261  {
1262    if (! encryptArg.isPresent())
1263    {
1264      return null;
1265    }
1266
1267    if (passphraseFileArg.isPresent())
1268    {
1269      final File passphraseFile = passphraseFileArg.getValue();
1270      if (generatePassphraseArg.isPresent())
1271      {
1272        // Generate a passphrase as a base64url-encoded representation of some
1273        // randomly generated data.
1274        final byte[] randomBytes = new byte[64];
1275        ThreadLocalSecureRandom.get().nextBytes(randomBytes);
1276        final String passphrase = Base64.urlEncode(randomBytes, false);
1277
1278        try (PrintWriter writer = new PrintWriter(passphraseFile))
1279        {
1280          writer.println(passphrase);
1281        }
1282        catch (final Exception e)
1283        {
1284          Debug.debugException(e);
1285          throw new LDAPException(ResultCode.LOCAL_ERROR,
1286               ERR_CSD_CANNOT_WRITE_GENERATED_PASSPHRASE.get(
1287                    passphraseFile.getAbsolutePath(),
1288                    StaticUtils.getExceptionMessage(e)),
1289               e);
1290        }
1291
1292        return new ASN1OctetString(passphrase);
1293      }
1294      else
1295      {
1296        try
1297        {
1298          final char[] passphrase =
1299               getPasswordFileReader().readPassword(passphraseFile);
1300          return new ASN1OctetString(new String(passphrase));
1301        }
1302        catch (final Exception e)
1303        {
1304          Debug.debugException(e);
1305
1306          ResultCode resultCode = ResultCode.LOCAL_ERROR;
1307          if (e instanceof LDAPException)
1308          {
1309            resultCode = ((LDAPException) e).getResultCode();
1310          }
1311
1312          throw new LDAPException(resultCode,
1313               ERR_CSD_CANNOT_READ_PASSPHRASE.get(
1314                    passphraseFile.getAbsolutePath(),
1315                    StaticUtils.getExceptionMessage(e)),
1316               e);
1317        }
1318      }
1319    }
1320
1321
1322    // Prompt for the encryption passphrase.
1323    while (true)
1324    {
1325      try
1326      {
1327        getOut().print(INFO_CSD_PASSPHRASE_INITIAL_PROMPT.get());
1328        final byte[] passphraseBytes = PasswordReader.readPassword();
1329
1330        getOut().print(INFO_CSD_PASSPHRASE_CONFIRM_PROMPT.get());
1331        final byte[] confirmBytes = PasswordReader.readPassword();
1332        if (Arrays.equals(passphraseBytes, confirmBytes))
1333        {
1334          return new ASN1OctetString(passphraseBytes);
1335        }
1336        else
1337        {
1338          wrapErr(0, WRAP_COLUMN, ERR_CSD_PASSPHRASE_MISMATCH.get());
1339          err();
1340        }
1341      }
1342      catch (final Exception e)
1343      {
1344        throw new LDAPException(ResultCode.LOCAL_ERROR,
1345             ERR_CSD_PASSPHRASE_PROMPT_READ_ERROR.get(
1346                  StaticUtils.getExceptionMessage(e)),
1347             e);
1348      }
1349    }
1350  }
1351
1352
1353
1354  /**
1355   * Performs the collect-support-data tool using reflection to invoke the
1356   * server-side version of the tool.
1357 *
1358   * @return  A result code that indicates the result of the processing.
1359     */
1360  @NotNull()
1361  private ResultCode doLocalProcessing()
1362  {
1363    // Construct the argument list to use when invoking the server-side code.
1364    // Although this tool supports all of the arguments that the server-side
1365    // tool provides, the server-side tool does not support all of the arguments
1366    // that this version offers, nor does it support all of the variants (e.g.,
1367    // alternate names) for the arguments that do overlap.
1368    final List<String> argList = new ArrayList<>(20);
1369
1370    if (outputPathArg.isPresent())
1371    {
1372      argList.add("--outputPath");
1373      argList.add(outputPathArg.getValue().getAbsolutePath());
1374    }
1375
1376    if (noLDAPArg.isPresent())
1377    {
1378      argList.add("--noLdap");
1379    }
1380
1381    if (pidArg.isPresent())
1382    {
1383      for (final Integer pid : pidArg.getValues())
1384      {
1385        argList.add("--pid");
1386        argList.add(pid.toString());
1387      }
1388    }
1389
1390    if (sequentialArg.isPresent())
1391    {
1392      argList.add("--sequential");
1393    }
1394
1395    if (reportCountArg.isPresent())
1396    {
1397      argList.add("--reportCount");
1398      argList.add(reportCountArg.getValue().toString());
1399    }
1400
1401    if (reportIntervalSecondsArg.isPresent())
1402    {
1403      argList.add("--reportInterval");
1404      argList.add(reportIntervalSecondsArg.getValue().toString());
1405    }
1406
1407    if (jstackCountArg.isPresent())
1408    {
1409      argList.add("--maxJstacks");
1410      argList.add(jstackCountArg.getValue().toString());
1411    }
1412
1413    if (collectExpensiveDataArg.isPresent())
1414    {
1415      argList.add("--collectExpensiveData");
1416    }
1417
1418    if (collectReplicationStateDumpArg.isPresent())
1419    {
1420      argList.add("--collectReplicationStateDump");
1421    }
1422
1423    if (commentArg.isPresent())
1424    {
1425      argList.add("--comment");
1426      argList.add(commentArg.getValue());
1427    }
1428
1429    if (includeBinaryFilesArg.isPresent())
1430    {
1431      argList.add("--includeBinaryFiles");
1432    }
1433
1434    if (securityLevelArg.isPresent())
1435    {
1436      argList.add("--securityLevel");
1437      argList.add(securityLevelArg.getValue());
1438    }
1439
1440    if (encryptArg.isPresent())
1441    {
1442      argList.add("--encrypt");
1443    }
1444
1445    if (passphraseFileArg.isPresent())
1446    {
1447      argList.add("--passphraseFile");
1448      argList.add(passphraseFileArg.getValue().getAbsolutePath());
1449    }
1450
1451    if (generatePassphraseArg.isPresent())
1452    {
1453      argList.add("--generatePassphrase");
1454    }
1455
1456    if (decryptArg.isPresent())
1457    {
1458      argList.add("--decrypt");
1459      argList.add(decryptArg.getValue().getAbsolutePath());
1460    }
1461
1462    if (logTimeRangeArg.isPresent())
1463    {
1464      final ObjectPair<Date,Date> timeRange;
1465      try
1466      {
1467        timeRange = parseTimeRange(logTimeRangeArg.getValue(), false);
1468      }
1469      catch (final LDAPException e)
1470      {
1471        // This should never happen because we should have pre-validated the
1472        // value.  But handle it just in case.
1473        Debug.debugException(e);
1474        wrapErr(0, WRAP_COLUMN, e.getMessage());
1475        return e.getResultCode();
1476      }
1477
1478      if (timeRange == null)
1479      {
1480        // We'll assume that this means the time range was specified using
1481        // rotated log filenames, which we can't handle in the LDAP SDK code so
1482        // we'll just pass the argument value through to the server code.
1483        argList.add("--timeRange");
1484        argList.add(logTimeRangeArg.getValue());
1485      }
1486      else
1487      {
1488        final Date startTime = timeRange.getFirst();
1489        Date endTime = timeRange.getSecond();
1490        if (endTime == null)
1491        {
1492          endTime = new Date(Math.max(System.currentTimeMillis(),
1493               startTime.getTime()));
1494        }
1495
1496        final SimpleDateFormat timestampFormatter =
1497             new SimpleDateFormat(SERVER_LOG_TIMESTAMP_FORMAT_WITH_MILLIS);
1498        argList.add("--timeRange");
1499        argList.add(timestampFormatter.format(startTime) + ',' +
1500             timestampFormatter.format(endTime));
1501      }
1502    }
1503
1504    if (logDurationArg.isPresent())
1505    {
1506      argList.add("--duration");
1507      argList.add(DurationArgument.nanosToDuration(
1508           logDurationArg.getValue(TimeUnit.NANOSECONDS)));
1509    }
1510
1511    if (logFileHeadCollectionSizeKBArg.isPresent())
1512    {
1513      argList.add("--logFileHeadCollectionSizeKB");
1514      argList.add(String.valueOf(logFileHeadCollectionSizeKBArg.getValue()));
1515    }
1516
1517    if (logFileTailCollectionSizeKBArg.isPresent())
1518    {
1519      argList.add("--logFileTailCollectionSizeKB");
1520      argList.add(String.valueOf(logFileTailCollectionSizeKBArg.getValue()));
1521    }
1522
1523    if (archiveExtensionSourceArg.isPresent())
1524    {
1525      argList.add("--archiveExtensionSource");
1526    }
1527
1528    if (noPromptArg.isPresent())
1529    {
1530      argList.add("--no-prompt");
1531    }
1532
1533    if (scriptFriendlyArg.isPresent())
1534    {
1535      argList.add("--script-friendly");
1536    }
1537
1538
1539    // We also need to include values for arguments provided by the LDAP
1540    // command-line tool framework.
1541    for (final String argumentIdentifier :
1542         Arrays.asList("hostname", "port", "bindDN", "bindPassword",
1543              "bindPasswordFile", "useSSL", "useStartTLS", "trustAll",
1544              "keyStorePath", "keyStorePassword", "keyStorePasswordFile",
1545              "keyStoreFormat", "trustStorePath", "trustStorePassword",
1546              "trustStorePasswordFile", "trustStoreFormat", "certNickname",
1547              "saslOption", "propertiesFilePath", "noPropertiesFile"))
1548    {
1549      final Argument arg = parser.getNamedArgument(argumentIdentifier);
1550      if (arg.getNumOccurrences() > 0)
1551      {
1552        for (final String value : arg.getValueStringRepresentations(false))
1553        {
1554          argList.add("--" + argumentIdentifier);
1555          if (arg.takesValue())
1556          {
1557            argList.add(value);
1558          }
1559        }
1560      }
1561    }
1562
1563
1564    // If the --dryRun argument was provided, then return without actually
1565    // invoking the tool.
1566    if (dryRunArg.isPresent())
1567    {
1568      final String message = INFO_CSD_LOCAL_MODE_DRY_RUN.get();
1569      wrapOut(0, WRAP_COLUMN, message);
1570      toolCompletionMessage.set(message);
1571      return ResultCode.NO_OPERATION;
1572    }
1573
1574
1575    // Make sure that we have access to the method in the server codebase that
1576    // we need to invoke local collect-support-data processing.
1577    final Method doMainMethod;
1578    try
1579    {
1580      final Class<?> csdToolClass = Class.forName(SERVER_CSD_TOOL_CLASS);
1581      doMainMethod = csdToolClass.getMethod("doMain", Boolean.TYPE,
1582           OutputStream.class, OutputStream.class, String[].class);
1583    }
1584    catch (final Throwable t)
1585    {
1586      Debug.debugException(t);
1587      final String message = ERR_CSD_SERVER_CODE_NOT_AVAILABLE.get();
1588      wrapErr(0, WRAP_COLUMN, message);
1589      toolCompletionMessage.set(message);
1590      return ResultCode.NOT_SUPPORTED;
1591    }
1592
1593
1594    // Invoke the doMain method via reflection
1595    final String[] argArray = new String[argList.size()];
1596    argList.toArray(argArray);
1597
1598    try
1599    {
1600      final Object doMainMethodReturnValue = doMainMethod.invoke(null,
1601           true, getOut(), getErr(), argArray);
1602      final int exitCode = ((Integer) doMainMethodReturnValue).intValue();
1603      return ResultCode.valueOf(exitCode);
1604    }
1605    catch (final Throwable t)
1606    {
1607      Debug.debugException(t);
1608      final String message =
1609           ERR_CSD_INVOKE_ERROR.get(StaticUtils.getExceptionMessage(t));
1610      wrapErr(0, WRAP_COLUMN, message);
1611      toolCompletionMessage.set(message);
1612      return ResultCode.LOCAL_ERROR;
1613    }
1614  }
1615
1616
1617
1618  /**
1619   * {@inheritDoc}
1620   */
1621  @Override()
1622  @NotNull()
1623  public LinkedHashMap<String[],String> getExampleUsages()
1624  {
1625    final LinkedHashMap<String[],String> examples =
1626         new LinkedHashMap<>(StaticUtils.computeMapCapacity(3));
1627
1628    examples.put(
1629         new String[]
1630         {
1631           "--bindDN", "uid=admin,dc=example,dc=com",
1632           "--bindPasswordFile", "admin-pw.txt"
1633         },
1634         INFO_CSD_EXAMPLE_1.get());
1635
1636    examples.put(
1637         new String[]
1638         {
1639           "--useRemoteServer",
1640           "--hostname", "ds.example.com",
1641           "--port", "636",
1642           "--useSSL",
1643           "--trustStorePath", "config/truststore",
1644           "--bindDN", "uid=admin,dc=example,dc=com",
1645           "--bindPasswordFile", "admin-pw.txt",
1646           "--collectExpensiveData",
1647           "--collectReplicationStateDump",
1648           "--securityLevel", "maximum",
1649           "--logDuration", "10 minutes",
1650           "--encrypt",
1651           "--passphraseFile", "encryption-passphrase.txt",
1652           "--generatePassphrase",
1653           "--outputPath", "csd.zip"
1654         },
1655         INFO_CSD_EXAMPLE_2.get());
1656
1657    examples.put(
1658         new String[]
1659         {
1660           "--decrypt", "support-data-ds-inst1-" +
1661                StaticUtils.encodeGeneralizedTime(new Date()) +
1662              "-zip-encrypted",
1663           "--passphraseFile", "encryption-passphrase.txt"
1664         },
1665         INFO_CSD_EXAMPLE_3.get());
1666
1667    return examples;
1668  }
1669}