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