001 /*
002 * Copyright 2009-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2009-2016 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021 package com.unboundid.ldap.sdk.examples;
022
023
024
025 import java.io.IOException;
026 import java.io.OutputStream;
027 import java.io.Serializable;
028 import java.text.ParseException;
029 import java.util.LinkedHashMap;
030 import java.util.LinkedHashSet;
031 import java.util.List;
032 import java.util.concurrent.CyclicBarrier;
033 import java.util.concurrent.atomic.AtomicBoolean;
034 import java.util.concurrent.atomic.AtomicLong;
035
036 import com.unboundid.ldap.sdk.LDAPConnection;
037 import com.unboundid.ldap.sdk.LDAPConnectionOptions;
038 import com.unboundid.ldap.sdk.LDAPException;
039 import com.unboundid.ldap.sdk.ResultCode;
040 import com.unboundid.ldap.sdk.SearchScope;
041 import com.unboundid.ldap.sdk.Version;
042 import com.unboundid.util.ColumnFormatter;
043 import com.unboundid.util.FixedRateBarrier;
044 import com.unboundid.util.FormattableColumn;
045 import com.unboundid.util.HorizontalAlignment;
046 import com.unboundid.util.LDAPCommandLineTool;
047 import com.unboundid.util.ObjectPair;
048 import com.unboundid.util.OutputFormat;
049 import com.unboundid.util.RateAdjustor;
050 import com.unboundid.util.ResultCodeCounter;
051 import com.unboundid.util.ThreadSafety;
052 import com.unboundid.util.ThreadSafetyLevel;
053 import com.unboundid.util.ValuePattern;
054 import com.unboundid.util.WakeableSleeper;
055 import com.unboundid.util.args.ArgumentException;
056 import com.unboundid.util.args.ArgumentParser;
057 import com.unboundid.util.args.BooleanArgument;
058 import com.unboundid.util.args.FileArgument;
059 import com.unboundid.util.args.IntegerArgument;
060 import com.unboundid.util.args.ScopeArgument;
061 import com.unboundid.util.args.StringArgument;
062
063 import static com.unboundid.util.Debug.*;
064 import static com.unboundid.util.StaticUtils.*;
065
066
067
068 /**
069 * This class provides a tool that can be used to test authentication processing
070 * in an LDAP directory server using multiple threads. Each authentication will
071 * consist of two operations: a search to find the target entry followed by a
072 * bind to verify the credentials for that user. The search will use the given
073 * base DN and filter, either or both of which may be a value pattern as
074 * described in the {@link ValuePattern} class. This makes it possible to
075 * search over a range of entries rather than repeatedly performing searches
076 * with the same base DN and filter.
077 * <BR><BR>
078 * Some of the APIs demonstrated by this example include:
079 * <UL>
080 * <LI>Argument Parsing (from the {@code com.unboundid.util.args}
081 * package)</LI>
082 * <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
083 * package)</LI>
084 * <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk}
085 * package)</LI>
086 * <LI>Value Patterns (from the {@code com.unboundid.util} package)</LI>
087 * </UL>
088 * Each search must match exactly one entry, and this tool will then attempt to
089 * authenticate as the user associated with that entry. It supports simple
090 * authentication, as well as the CRAM-MD5, DIGEST-MD5, and PLAIN SASL
091 * mechanisms.
092 * <BR><BR>
093 * All of the necessary information is provided using command line arguments.
094 * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
095 * class, as well as the following additional arguments:
096 * <UL>
097 * <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use
098 * for the searches. This must be provided. It may be a simple DN, or it
099 * may be a value pattern to express a range of base DNs.</LI>
100 * <LI>"-s {scope}" or "--scope {scope}" -- specifies the scope to use for the
101 * search. The scope value should be one of "base", "one", "sub", or
102 * "subord". If this isn't specified, then a scope of "sub" will be
103 * used.</LI>
104 * <LI>"-f {filter}" or "--filter {filter}" -- specifies the filter to use for
105 * the searches. This must be provided. It may be a simple filter, or it
106 * may be a value pattern to express a range of filters.</LI>
107 * <LI>"-A {name}" or "--attribute {name}" -- specifies the name of an
108 * attribute that should be included in entries returned from the server.
109 * If this is not provided, then all user attributes will be requested.
110 * This may include special tokens that the server may interpret, like
111 * "1.1" to indicate that no attributes should be returned, "*", for all
112 * user attributes, or "+" for all operational attributes. Multiple
113 * attributes may be requested with multiple instances of this
114 * argument.</LI>
115 * <LI>"-C {password}" or "--credentials {password}" -- specifies the password
116 * to use when authenticating users identified by the searches.</LI>
117 * <LI>"-a {authType}" or "--authType {authType}" -- specifies the type of
118 * authentication to attempt. Supported values include "SIMPLE",
119 * "CRAM-MD5", "DIGEST-MD5", and "PLAIN".
120 * <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of
121 * concurrent threads to use when performing the authentication
122 * processing. If this is not provided, then a default of one thread will
123 * be used.</LI>
124 * <LI>"-i {sec}" or "--intervalDuration {sec}" -- specifies the length of
125 * time in seconds between lines out output. If this is not provided,
126 * then a default interval duration of five seconds will be used.</LI>
127 * <LI>"-I {num}" or "--numIntervals {num}" -- specifies the maximum number of
128 * intervals for which to run. If this is not provided, then it will
129 * run forever.</LI>
130 * <LI>"-r {auths-per-second}" or "--ratePerSecond {auths-per-second}" --
131 * specifies the target number of authorizations to perform per second.
132 * It is still necessary to specify a sufficient number of threads for
133 * achieving this rate. If this option is not provided, then the tool
134 * will run at the maximum rate for the specified number of threads.</LI>
135 * <LI>"--variableRateData {path}" -- specifies the path to a file containing
136 * information needed to allow the tool to vary the target rate over time.
137 * If this option is not provided, then the tool will either use a fixed
138 * target rate as specified by the "--ratePerSecond" argument, or it will
139 * run at the maximum rate.</LI>
140 * <LI>"--generateSampleRateFile {path}" -- specifies the path to a file to
141 * which sample data will be written illustrating and describing the
142 * format of the file expected to be used in conjunction with the
143 * "--variableRateData" argument.</LI>
144 * <LI>"--warmUpIntervals {num}" -- specifies the number of intervals to
145 * complete before beginning overall statistics collection.</LI>
146 * <LI>"--timestampFormat {format}" -- specifies the format to use for
147 * timestamps included before each output line. The format may be one of
148 * "none" (for no timestamps), "with-date" (to include both the date and
149 * the time), or "without-date" (to include only time time).</LI>
150 * <LI>"--suppressErrorResultCodes" -- Indicates that information about the
151 * result codes for failed operations should not be displayed.</LI>
152 * <LI>"-c" or "--csv" -- Generate output in CSV format rather than a
153 * display-friendly format.</LI>
154 * </UL>
155 */
156 @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
157 public final class AuthRate
158 extends LDAPCommandLineTool
159 implements Serializable
160 {
161 /**
162 * The serial version UID for this serializable class.
163 */
164 private static final long serialVersionUID = 6918029871717330547L;
165
166
167
168 // Indicates whether a request has been made to stop running.
169 private final AtomicBoolean stopRequested;
170
171 // The argument used to indicate whether to generate output in CSV format.
172 private BooleanArgument csvFormat;
173
174 // The argument used to indicate whether to suppress information about error
175 // result codes.
176 private BooleanArgument suppressErrorsArgument;
177
178 // The argument used to specify the collection interval.
179 private IntegerArgument collectionInterval;
180
181 // The argument used to specify the number of intervals.
182 private IntegerArgument numIntervals;
183
184 // The argument used to specify the number of threads.
185 private IntegerArgument numThreads;
186
187 // The argument used to specify the seed to use for the random number
188 // generator.
189 private IntegerArgument randomSeed;
190
191 // The target rate of auths per second.
192 private IntegerArgument ratePerSecond;
193
194 // The argument used to specify a variable rate file.
195 private FileArgument sampleRateFile;
196
197 // The argument used to specify a variable rate file.
198 private FileArgument variableRateData;
199
200 // The number of warm-up intervals to perform.
201 private IntegerArgument warmUpIntervals;
202
203 // The argument used to specify the attributes to return.
204 private StringArgument attributes;
205
206 // The argument used to specify the type of authentication to perform.
207 private StringArgument authType;
208
209 // The argument used to specify the base DNs for the searches.
210 private StringArgument baseDN;
211
212 // The argument used to specify the filters for the searches.
213 private StringArgument filter;
214
215 // The argument used to specify the scope for the searches.
216 private ScopeArgument scopeArg;
217
218 // The argument used to specify the timestamp format.
219 private StringArgument timestampFormat;
220
221 // The argument used to specify the password to use to authenticate.
222 private StringArgument userPassword;
223
224 // The thread currently being used to run the searchrate tool.
225 private volatile Thread runningThread;
226
227 // A wakeable sleeper that will be used to sleep between reporting intervals.
228 private final WakeableSleeper sleeper;
229
230
231
232 /**
233 * Parse the provided command line arguments and make the appropriate set of
234 * changes.
235 *
236 * @param args The command line arguments provided to this program.
237 */
238 public static void main(final String[] args)
239 {
240 final ResultCode resultCode = main(args, System.out, System.err);
241 if (resultCode != ResultCode.SUCCESS)
242 {
243 System.exit(resultCode.intValue());
244 }
245 }
246
247
248
249 /**
250 * Parse the provided command line arguments and make the appropriate set of
251 * changes.
252 *
253 * @param args The command line arguments provided to this program.
254 * @param outStream The output stream to which standard out should be
255 * written. It may be {@code null} if output should be
256 * suppressed.
257 * @param errStream The output stream to which standard error should be
258 * written. It may be {@code null} if error messages
259 * should be suppressed.
260 *
261 * @return A result code indicating whether the processing was successful.
262 */
263 public static ResultCode main(final String[] args,
264 final OutputStream outStream,
265 final OutputStream errStream)
266 {
267 final AuthRate authRate = new AuthRate(outStream, errStream);
268 return authRate.runTool(args);
269 }
270
271
272
273 /**
274 * Creates a new instance of this tool.
275 *
276 * @param outStream The output stream to which standard out should be
277 * written. It may be {@code null} if output should be
278 * suppressed.
279 * @param errStream The output stream to which standard error should be
280 * written. It may be {@code null} if error messages
281 * should be suppressed.
282 */
283 public AuthRate(final OutputStream outStream, final OutputStream errStream)
284 {
285 super(outStream, errStream);
286
287 stopRequested = new AtomicBoolean(false);
288 sleeper = new WakeableSleeper();
289 }
290
291
292
293 /**
294 * Retrieves the name for this tool.
295 *
296 * @return The name for this tool.
297 */
298 @Override()
299 public String getToolName()
300 {
301 return "authrate";
302 }
303
304
305
306 /**
307 * Retrieves the description for this tool.
308 *
309 * @return The description for this tool.
310 */
311 @Override()
312 public String getToolDescription()
313 {
314 return "Perform repeated authentications against an LDAP directory " +
315 "server, where each authentication consists of a search to " +
316 "find a user followed by a bind to verify the credentials " +
317 "for that user.";
318 }
319
320
321
322 /**
323 * Retrieves the version string for this tool.
324 *
325 * @return The version string for this tool.
326 */
327 @Override()
328 public String getToolVersion()
329 {
330 return Version.NUMERIC_VERSION_STRING;
331 }
332
333
334
335 /**
336 * Indicates whether this tool should provide support for an interactive mode,
337 * in which the tool offers a mode in which the arguments can be provided in
338 * a text-driven menu rather than requiring them to be given on the command
339 * line. If interactive mode is supported, it may be invoked using the
340 * "--interactive" argument. Alternately, if interactive mode is supported
341 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
342 * interactive mode may be invoked by simply launching the tool without any
343 * arguments.
344 *
345 * @return {@code true} if this tool supports interactive mode, or
346 * {@code false} if not.
347 */
348 @Override()
349 public boolean supportsInteractiveMode()
350 {
351 return true;
352 }
353
354
355
356 /**
357 * Indicates whether this tool defaults to launching in interactive mode if
358 * the tool is invoked without any command-line arguments. This will only be
359 * used if {@link #supportsInteractiveMode()} returns {@code true}.
360 *
361 * @return {@code true} if this tool defaults to using interactive mode if
362 * launched without any command-line arguments, or {@code false} if
363 * not.
364 */
365 @Override()
366 public boolean defaultsToInteractiveMode()
367 {
368 return true;
369 }
370
371
372
373 /**
374 * Indicates whether this tool should provide arguments for redirecting output
375 * to a file. If this method returns {@code true}, then the tool will offer
376 * an "--outputFile" argument that will specify the path to a file to which
377 * all standard output and standard error content will be written, and it will
378 * also offer a "--teeToStandardOut" argument that can only be used if the
379 * "--outputFile" argument is present and will cause all output to be written
380 * to both the specified output file and to standard output.
381 *
382 * @return {@code true} if this tool should provide arguments for redirecting
383 * output to a file, or {@code false} if not.
384 */
385 @Override()
386 protected boolean supportsOutputFile()
387 {
388 return true;
389 }
390
391
392
393 /**
394 * Indicates whether this tool should default to interactively prompting for
395 * the bind password if a password is required but no argument was provided
396 * to indicate how to get the password.
397 *
398 * @return {@code true} if this tool should default to interactively
399 * prompting for the bind password, or {@code false} if not.
400 */
401 @Override()
402 protected boolean defaultToPromptForBindPassword()
403 {
404 return true;
405 }
406
407
408
409 /**
410 * Indicates whether this tool supports the use of a properties file for
411 * specifying default values for arguments that aren't specified on the
412 * command line.
413 *
414 * @return {@code true} if this tool supports the use of a properties file
415 * for specifying default values for arguments that aren't specified
416 * on the command line, or {@code false} if not.
417 */
418 @Override()
419 public boolean supportsPropertiesFile()
420 {
421 return true;
422 }
423
424
425
426 /**
427 * Indicates whether the LDAP-specific arguments should include alternate
428 * versions of all long identifiers that consist of multiple words so that
429 * they are available in both camelCase and dash-separated versions.
430 *
431 * @return {@code true} if this tool should provide multiple versions of
432 * long identifiers for LDAP-specific arguments, or {@code false} if
433 * not.
434 */
435 @Override()
436 protected boolean includeAlternateLongIdentifiers()
437 {
438 return true;
439 }
440
441
442
443 /**
444 * Adds the arguments used by this program that aren't already provided by the
445 * generic {@code LDAPCommandLineTool} framework.
446 *
447 * @param parser The argument parser to which the arguments should be added.
448 *
449 * @throws ArgumentException If a problem occurs while adding the arguments.
450 */
451 @Override()
452 public void addNonLDAPArguments(final ArgumentParser parser)
453 throws ArgumentException
454 {
455 String description = "The base DN to use for the searches. It may be a " +
456 "simple DN or a value pattern to specify a range of DNs (e.g., " +
457 "\"uid=user.[1-1000],ou=People,dc=example,dc=com\"). See " +
458 ValuePattern.PUBLIC_JAVADOC_URL + " for complete details about the " +
459 "value pattern syntax. This must be provided.";
460 baseDN = new StringArgument('b', "baseDN", true, 1, "{dn}", description);
461 baseDN.setArgumentGroupName("Search and Authentication Arguments");
462 baseDN.addLongIdentifier("base-dn");
463 parser.addArgument(baseDN);
464
465
466 description = "The scope to use for the searches. It should be 'base', " +
467 "'one', 'sub', or 'subord'. If this is not provided, a " +
468 "default scope of 'sub' will be used.";
469 scopeArg = new ScopeArgument('s', "scope", false, "{scope}", description,
470 SearchScope.SUB);
471 scopeArg.setArgumentGroupName("Search and Authentication Arguments");
472 parser.addArgument(scopeArg);
473
474
475 description = "The filter to use for the searches. It may be a simple " +
476 "filter or a value pattern to specify a range of filters " +
477 "(e.g., \"(uid=user.[1-1000])\"). See " +
478 ValuePattern.PUBLIC_JAVADOC_URL + " for complete details " +
479 "about the value pattern syntax. This must be provided.";
480 filter = new StringArgument('f', "filter", true, 1, "{filter}",
481 description);
482 filter.setArgumentGroupName("Search and Authentication Arguments");
483 parser.addArgument(filter);
484
485
486 description = "The name of an attribute to include in entries returned " +
487 "from the searches. Multiple attributes may be requested " +
488 "by providing this argument multiple times. If no return " +
489 "attributes are specified, then entries will be returned " +
490 "with all user attributes.";
491 attributes = new StringArgument('A', "attribute", false, 0, "{name}",
492 description);
493 attributes.setArgumentGroupName("Search and Authentication Arguments");
494 parser.addArgument(attributes);
495
496
497 description = "The password to use when binding as the users returned " +
498 "from the searches. This must be provided.";
499 userPassword = new StringArgument('C', "credentials", true, 1, "{password}",
500 description);
501 userPassword.setSensitive(true);
502 userPassword.setArgumentGroupName("Search and Authentication Arguments");
503 parser.addArgument(userPassword);
504
505
506 description = "The type of authentication to perform. Allowed values " +
507 "are: SIMPLE, CRAM-MD5, DIGEST-MD5, and PLAIN. If no "+
508 "value is provided, then SIMPLE authentication will be " +
509 "performed.";
510 final LinkedHashSet<String> allowedAuthTypes = new LinkedHashSet<String>(4);
511 allowedAuthTypes.add("simple");
512 allowedAuthTypes.add("cram-md5");
513 allowedAuthTypes.add("digest-md5");
514 allowedAuthTypes.add("plain");
515 authType = new StringArgument('a', "authType", true, 1, "{authType}",
516 description, allowedAuthTypes, "simple");
517 authType.setArgumentGroupName("Search and Authentication Arguments");
518 authType.addLongIdentifier("auth-type");
519 parser.addArgument(authType);
520
521
522 description = "The number of threads to use to perform the " +
523 "authentication processing. If this is not provided, then " +
524 "a default of one thread will be used.";
525 numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}",
526 description, 1, Integer.MAX_VALUE, 1);
527 numThreads.setArgumentGroupName("Rate Management Arguments");
528 numThreads.addLongIdentifier("num-threads");
529 parser.addArgument(numThreads);
530
531
532 description = "The length of time in seconds between output lines. If " +
533 "this is not provided, then a default interval of five " +
534 "seconds will be used.";
535 collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1,
536 "{num}", description, 1,
537 Integer.MAX_VALUE, 5);
538 collectionInterval.setArgumentGroupName("Rate Management Arguments");
539 collectionInterval.addLongIdentifier("interval-duration");
540 parser.addArgument(collectionInterval);
541
542
543 description = "The maximum number of intervals for which to run. If " +
544 "this is not provided, then the tool will run until it is " +
545 "interrupted.";
546 numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}",
547 description, 1, Integer.MAX_VALUE,
548 Integer.MAX_VALUE);
549 numIntervals.setArgumentGroupName("Rate Management Arguments");
550 numIntervals.addLongIdentifier("num-intervals");
551 parser.addArgument(numIntervals);
552
553 description = "The target number of authorizations to perform per " +
554 "second. It is still necessary to specify a sufficient " +
555 "number of threads for achieving this rate. If neither " +
556 "this option nor --variableRateData is provided, then the " +
557 "tool will run at the maximum rate for the specified " +
558 "number of threads.";
559 ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1,
560 "{auths-per-second}", description,
561 1, Integer.MAX_VALUE);
562 ratePerSecond.setArgumentGroupName("Rate Management Arguments");
563 ratePerSecond.addLongIdentifier("rate-per-second");
564 parser.addArgument(ratePerSecond);
565
566 final String variableRateDataArgName = "variableRateData";
567 final String generateSampleRateFileArgName = "generateSampleRateFile";
568 description = RateAdjustor.getVariableRateDataArgumentDescription(
569 generateSampleRateFileArgName);
570 variableRateData = new FileArgument(null, variableRateDataArgName, false, 1,
571 "{path}", description, true, true, true,
572 false);
573 variableRateData.setArgumentGroupName("Rate Management Arguments");
574 variableRateData.addLongIdentifier("variable-rate-data");
575 parser.addArgument(variableRateData);
576
577 description = RateAdjustor.getGenerateSampleVariableRateFileDescription(
578 variableRateDataArgName);
579 sampleRateFile = new FileArgument(null, generateSampleRateFileArgName,
580 false, 1, "{path}", description, false,
581 true, true, false);
582 sampleRateFile.setArgumentGroupName("Rate Management Arguments");
583 sampleRateFile.addLongIdentifier("generate-sample-rate-file");
584 sampleRateFile.setUsageArgument(true);
585 parser.addArgument(sampleRateFile);
586 parser.addExclusiveArgumentSet(variableRateData, sampleRateFile);
587
588 description = "The number of intervals to complete before beginning " +
589 "overall statistics collection. Specifying a nonzero " +
590 "number of warm-up intervals gives the client and server " +
591 "a chance to warm up without skewing performance results.";
592 warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1,
593 "{num}", description, 0, Integer.MAX_VALUE, 0);
594 warmUpIntervals.setArgumentGroupName("Rate Management Arguments");
595 warmUpIntervals.addLongIdentifier("warm-up-intervals");
596 parser.addArgument(warmUpIntervals);
597
598 description = "Indicates the format to use for timestamps included in " +
599 "the output. A value of 'none' indicates that no " +
600 "timestamps should be included. A value of 'with-date' " +
601 "indicates that both the date and the time should be " +
602 "included. A value of 'without-date' indicates that only " +
603 "the time should be included.";
604 final LinkedHashSet<String> allowedFormats = new LinkedHashSet<String>(3);
605 allowedFormats.add("none");
606 allowedFormats.add("with-date");
607 allowedFormats.add("without-date");
608 timestampFormat = new StringArgument(null, "timestampFormat", true, 1,
609 "{format}", description, allowedFormats, "none");
610 timestampFormat.addLongIdentifier("timestamp-format");
611 parser.addArgument(timestampFormat);
612
613 description = "Indicates that information about the result codes for " +
614 "failed operations should not be displayed.";
615 suppressErrorsArgument = new BooleanArgument(null,
616 "suppressErrorResultCodes", 1, description);
617 suppressErrorsArgument.addLongIdentifier("suppress-error-result-codes");
618 parser.addArgument(suppressErrorsArgument);
619
620 description = "Generate output in CSV format rather than a " +
621 "display-friendly format";
622 csvFormat = new BooleanArgument('c', "csv", 1, description);
623 parser.addArgument(csvFormat);
624
625 description = "Specifies the seed to use for the random number generator.";
626 randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}",
627 description);
628 randomSeed.addLongIdentifier("random-seed");
629 parser.addArgument(randomSeed);
630 }
631
632
633
634 /**
635 * Indicates whether this tool supports creating connections to multiple
636 * servers. If it is to support multiple servers, then the "--hostname" and
637 * "--port" arguments will be allowed to be provided multiple times, and
638 * will be required to be provided the same number of times. The same type of
639 * communication security and bind credentials will be used for all servers.
640 *
641 * @return {@code true} if this tool supports creating connections to
642 * multiple servers, or {@code false} if not.
643 */
644 @Override()
645 protected boolean supportsMultipleServers()
646 {
647 return true;
648 }
649
650
651
652 /**
653 * Retrieves the connection options that should be used for connections
654 * created for use with this tool.
655 *
656 * @return The connection options that should be used for connections created
657 * for use with this tool.
658 */
659 @Override()
660 public LDAPConnectionOptions getConnectionOptions()
661 {
662 final LDAPConnectionOptions options = new LDAPConnectionOptions();
663 options.setUseSynchronousMode(true);
664 return options;
665 }
666
667
668
669 /**
670 * Performs the actual processing for this tool. In this case, it gets a
671 * connection to the directory server and uses it to perform the requested
672 * searches.
673 *
674 * @return The result code for the processing that was performed.
675 */
676 @Override()
677 public ResultCode doToolProcessing()
678 {
679 runningThread = Thread.currentThread();
680
681 try
682 {
683 return doToolProcessingInternal();
684 }
685 finally
686 {
687 runningThread = null;
688 }
689 }
690
691
692
693 /**
694 * Performs the actual processing for this tool. In this case, it gets a
695 * connection to the directory server and uses it to perform the requested
696 * searches.
697 *
698 * @return The result code for the processing that was performed.
699 */
700 private ResultCode doToolProcessingInternal()
701 {
702 // If the sample rate file argument was specified, then generate the sample
703 // variable rate data file and return.
704 if (sampleRateFile.isPresent())
705 {
706 try
707 {
708 RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue());
709 return ResultCode.SUCCESS;
710 }
711 catch (final Exception e)
712 {
713 debugException(e);
714 err("An error occurred while trying to write sample variable data " +
715 "rate file '", sampleRateFile.getValue().getAbsolutePath(),
716 "': ", getExceptionMessage(e));
717 return ResultCode.LOCAL_ERROR;
718 }
719 }
720
721
722 // Determine the random seed to use.
723 final Long seed;
724 if (randomSeed.isPresent())
725 {
726 seed = Long.valueOf(randomSeed.getValue());
727 }
728 else
729 {
730 seed = null;
731 }
732
733 // Create value patterns for the base DN and filter.
734 final ValuePattern dnPattern;
735 try
736 {
737 dnPattern = new ValuePattern(baseDN.getValue(), seed);
738 }
739 catch (ParseException pe)
740 {
741 debugException(pe);
742 err("Unable to parse the base DN value pattern: ", pe.getMessage());
743 return ResultCode.PARAM_ERROR;
744 }
745
746 final ValuePattern filterPattern;
747 try
748 {
749 filterPattern = new ValuePattern(filter.getValue(), seed);
750 }
751 catch (ParseException pe)
752 {
753 debugException(pe);
754 err("Unable to parse the filter pattern: ", pe.getMessage());
755 return ResultCode.PARAM_ERROR;
756 }
757
758
759 // Get the attributes to return.
760 final String[] attrs;
761 if (attributes.isPresent())
762 {
763 final List<String> attrList = attributes.getValues();
764 attrs = new String[attrList.size()];
765 attrList.toArray(attrs);
766 }
767 else
768 {
769 attrs = NO_STRINGS;
770 }
771
772
773 // If the --ratePerSecond option was specified, then limit the rate
774 // accordingly.
775 FixedRateBarrier fixedRateBarrier = null;
776 if (ratePerSecond.isPresent() || variableRateData.isPresent())
777 {
778 // We might not have a rate per second if --variableRateData is specified.
779 // The rate typically doesn't matter except when we have warm-up
780 // intervals. In this case, we'll run at the max rate.
781 final int intervalSeconds = collectionInterval.getValue();
782 final int ratePerInterval =
783 (ratePerSecond.getValue() == null)
784 ? Integer.MAX_VALUE
785 : ratePerSecond.getValue() * intervalSeconds;
786 fixedRateBarrier =
787 new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval);
788 }
789
790
791 // If --variableRateData was specified, then initialize a RateAdjustor.
792 RateAdjustor rateAdjustor = null;
793 if (variableRateData.isPresent())
794 {
795 try
796 {
797 rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier,
798 ratePerSecond.getValue(), variableRateData.getValue());
799 }
800 catch (IOException e)
801 {
802 debugException(e);
803 err("Initializing the variable rates failed: " + e.getMessage());
804 return ResultCode.PARAM_ERROR;
805 }
806 catch (IllegalArgumentException e)
807 {
808 debugException(e);
809 err("Initializing the variable rates failed: " + e.getMessage());
810 return ResultCode.PARAM_ERROR;
811 }
812 }
813
814
815 // Determine whether to include timestamps in the output and if so what
816 // format should be used for them.
817 final boolean includeTimestamp;
818 final String timeFormat;
819 if (timestampFormat.getValue().equalsIgnoreCase("with-date"))
820 {
821 includeTimestamp = true;
822 timeFormat = "dd/MM/yyyy HH:mm:ss";
823 }
824 else if (timestampFormat.getValue().equalsIgnoreCase("without-date"))
825 {
826 includeTimestamp = true;
827 timeFormat = "HH:mm:ss";
828 }
829 else
830 {
831 includeTimestamp = false;
832 timeFormat = null;
833 }
834
835
836 // Determine whether any warm-up intervals should be run.
837 final long totalIntervals;
838 final boolean warmUp;
839 int remainingWarmUpIntervals = warmUpIntervals.getValue();
840 if (remainingWarmUpIntervals > 0)
841 {
842 warmUp = true;
843 totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals;
844 }
845 else
846 {
847 warmUp = true;
848 totalIntervals = 0L + numIntervals.getValue();
849 }
850
851
852 // Create the table that will be used to format the output.
853 final OutputFormat outputFormat;
854 if (csvFormat.isPresent())
855 {
856 outputFormat = OutputFormat.CSV;
857 }
858 else
859 {
860 outputFormat = OutputFormat.COLUMNS;
861 }
862
863 final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp,
864 timeFormat, outputFormat, " ",
865 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
866 "Auths/Sec"),
867 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
868 "Avg Dur ms"),
869 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
870 "Errors/Sec"),
871 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
872 "Auths/Sec"),
873 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
874 "Avg Dur ms"));
875
876
877 // Create values to use for statistics collection.
878 final AtomicLong authCounter = new AtomicLong(0L);
879 final AtomicLong errorCounter = new AtomicLong(0L);
880 final AtomicLong authDurations = new AtomicLong(0L);
881 final ResultCodeCounter rcCounter = new ResultCodeCounter();
882
883
884 // Determine the length of each interval in milliseconds.
885 final long intervalMillis = 1000L * collectionInterval.getValue();
886
887
888 // Create the threads to use for the searches.
889 final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1);
890 final AuthRateThread[] threads = new AuthRateThread[numThreads.getValue()];
891 for (int i=0; i < threads.length; i++)
892 {
893 final LDAPConnection searchConnection;
894 final LDAPConnection bindConnection;
895 try
896 {
897 searchConnection = getConnection();
898 bindConnection = getConnection();
899 }
900 catch (LDAPException le)
901 {
902 debugException(le);
903 err("Unable to connect to the directory server: ",
904 getExceptionMessage(le));
905 return le.getResultCode();
906 }
907
908 threads[i] = new AuthRateThread(this, i, searchConnection, bindConnection,
909 dnPattern, scopeArg.getValue(), filterPattern, attrs,
910 userPassword.getValue(), authType.getValue(), barrier, authCounter,
911 authDurations, errorCounter, rcCounter, fixedRateBarrier);
912 threads[i].start();
913 }
914
915
916 // Display the table header.
917 for (final String headerLine : formatter.getHeaderLines(true))
918 {
919 out(headerLine);
920 }
921
922
923 // Start the RateAdjustor before the threads so that the initial value is
924 // in place before any load is generated unless we're doing a warm-up in
925 // which case, we'll start it after the warm-up is complete.
926 if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0))
927 {
928 rateAdjustor.start();
929 }
930
931
932 // Indicate that the threads can start running.
933 try
934 {
935 barrier.await();
936 }
937 catch (final Exception e)
938 {
939 debugException(e);
940 }
941
942 long overallStartTime = System.nanoTime();
943 long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis;
944
945
946 boolean setOverallStartTime = false;
947 long lastDuration = 0L;
948 long lastNumErrors = 0L;
949 long lastNumAuths = 0L;
950 long lastEndTime = System.nanoTime();
951 for (long i=0; i < totalIntervals; i++)
952 {
953 if (rateAdjustor != null)
954 {
955 if (! rateAdjustor.isAlive())
956 {
957 out("All of the rates in " + variableRateData.getValue().getName() +
958 " have been completed.");
959 break;
960 }
961 }
962
963 final long startTimeMillis = System.currentTimeMillis();
964 final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis;
965 nextIntervalStartTime += intervalMillis;
966 if (sleepTimeMillis > 0)
967 {
968 sleeper.sleep(sleepTimeMillis);
969 }
970
971 if (stopRequested.get())
972 {
973 break;
974 }
975
976 final long endTime = System.nanoTime();
977 final long intervalDuration = endTime - lastEndTime;
978
979 final long numAuths;
980 final long numErrors;
981 final long totalDuration;
982 if (warmUp && (remainingWarmUpIntervals > 0))
983 {
984 numAuths = authCounter.getAndSet(0L);
985 numErrors = errorCounter.getAndSet(0L);
986 totalDuration = authDurations.getAndSet(0L);
987 }
988 else
989 {
990 numAuths = authCounter.get();
991 numErrors = errorCounter.get();
992 totalDuration = authDurations.get();
993 }
994
995 final long recentNumAuths = numAuths - lastNumAuths;
996 final long recentNumErrors = numErrors - lastNumErrors;
997 final long recentDuration = totalDuration - lastDuration;
998
999 final double numSeconds = intervalDuration / 1000000000.0d;
1000 final double recentAuthRate = recentNumAuths / numSeconds;
1001 final double recentErrorRate = recentNumErrors / numSeconds;
1002
1003 final double recentAvgDuration;
1004 if (recentNumAuths > 0L)
1005 {
1006 recentAvgDuration = 1.0d * recentDuration / recentNumAuths / 1000000;
1007 }
1008 else
1009 {
1010 recentAvgDuration = 0.0d;
1011 }
1012
1013 if (warmUp && (remainingWarmUpIntervals > 0))
1014 {
1015 out(formatter.formatRow(recentAuthRate, recentAvgDuration,
1016 recentErrorRate, "warming up", "warming up"));
1017
1018 remainingWarmUpIntervals--;
1019 if (remainingWarmUpIntervals == 0)
1020 {
1021 out("Warm-up completed. Beginning overall statistics collection.");
1022 setOverallStartTime = true;
1023 if (rateAdjustor != null)
1024 {
1025 rateAdjustor.start();
1026 }
1027 }
1028 }
1029 else
1030 {
1031 if (setOverallStartTime)
1032 {
1033 overallStartTime = lastEndTime;
1034 setOverallStartTime = false;
1035 }
1036
1037 final double numOverallSeconds =
1038 (endTime - overallStartTime) / 1000000000.0d;
1039 final double overallAuthRate = numAuths / numOverallSeconds;
1040
1041 final double overallAvgDuration;
1042 if (numAuths > 0L)
1043 {
1044 overallAvgDuration = 1.0d * totalDuration / numAuths / 1000000;
1045 }
1046 else
1047 {
1048 overallAvgDuration = 0.0d;
1049 }
1050
1051 out(formatter.formatRow(recentAuthRate, recentAvgDuration,
1052 recentErrorRate, overallAuthRate, overallAvgDuration));
1053
1054 lastNumAuths = numAuths;
1055 lastNumErrors = numErrors;
1056 lastDuration = totalDuration;
1057 }
1058
1059 final List<ObjectPair<ResultCode,Long>> rcCounts =
1060 rcCounter.getCounts(true);
1061 if ((! suppressErrorsArgument.isPresent()) && (! rcCounts.isEmpty()))
1062 {
1063 err("\tError Results:");
1064 for (final ObjectPair<ResultCode,Long> p : rcCounts)
1065 {
1066 err("\t", p.getFirst().getName(), ": ", p.getSecond());
1067 }
1068 }
1069
1070 lastEndTime = endTime;
1071 }
1072
1073
1074 // Shut down the RateAdjustor if we have one.
1075 if (rateAdjustor != null)
1076 {
1077 rateAdjustor.shutDown();
1078 }
1079
1080
1081 // Stop all of the threads.
1082 ResultCode resultCode = ResultCode.SUCCESS;
1083 for (final AuthRateThread t : threads)
1084 {
1085 final ResultCode r = t.stopRunning();
1086 if (resultCode == ResultCode.SUCCESS)
1087 {
1088 resultCode = r;
1089 }
1090 }
1091
1092 return resultCode;
1093 }
1094
1095
1096
1097 /**
1098 * Requests that this tool stop running. This method will attempt to wait
1099 * for all threads to complete before returning control to the caller.
1100 */
1101 public void stopRunning()
1102 {
1103 stopRequested.set(true);
1104 sleeper.wakeup();
1105
1106 final Thread t = runningThread;
1107 if (t != null)
1108 {
1109 try
1110 {
1111 t.join();
1112 }
1113 catch (final Exception e)
1114 {
1115 debugException(e);
1116 }
1117 }
1118 }
1119
1120
1121
1122 /**
1123 * {@inheritDoc}
1124 */
1125 @Override()
1126 public LinkedHashMap<String[],String> getExampleUsages()
1127 {
1128 final LinkedHashMap<String[],String> examples =
1129 new LinkedHashMap<String[],String>(2);
1130
1131 String[] args =
1132 {
1133 "--hostname", "server.example.com",
1134 "--port", "389",
1135 "--bindDN", "uid=admin,dc=example,dc=com",
1136 "--bindPassword", "password",
1137 "--baseDN", "dc=example,dc=com",
1138 "--scope", "sub",
1139 "--filter", "(uid=user.[1-1000000])",
1140 "--credentials", "password",
1141 "--numThreads", "10"
1142 };
1143 String description =
1144 "Test authentication performance by searching randomly across a set " +
1145 "of one million users located below 'dc=example,dc=com' with ten " +
1146 "concurrent threads and performing simple binds with a password of " +
1147 "'password'. The searches will be performed anonymously.";
1148 examples.put(args, description);
1149
1150 args = new String[]
1151 {
1152 "--generateSampleRateFile", "variable-rate-data.txt"
1153 };
1154 description =
1155 "Generate a sample variable rate definition file that may be used " +
1156 "in conjunction with the --variableRateData argument. The sample " +
1157 "file will include comments that describe the format for data to be " +
1158 "included in this file.";
1159 examples.put(args, description);
1160
1161 return examples;
1162 }
1163 }