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 */ 021package com.unboundid.ldap.sdk.examples; 022 023 024 025import java.io.IOException; 026import java.io.OutputStream; 027import java.io.Serializable; 028import java.text.ParseException; 029import java.util.LinkedHashMap; 030import java.util.LinkedHashSet; 031import java.util.List; 032import java.util.concurrent.CyclicBarrier; 033import java.util.concurrent.atomic.AtomicBoolean; 034import java.util.concurrent.atomic.AtomicLong; 035 036import com.unboundid.ldap.sdk.LDAPConnection; 037import com.unboundid.ldap.sdk.LDAPConnectionOptions; 038import com.unboundid.ldap.sdk.LDAPException; 039import com.unboundid.ldap.sdk.ResultCode; 040import com.unboundid.ldap.sdk.SearchScope; 041import com.unboundid.ldap.sdk.Version; 042import com.unboundid.util.ColumnFormatter; 043import com.unboundid.util.FixedRateBarrier; 044import com.unboundid.util.FormattableColumn; 045import com.unboundid.util.HorizontalAlignment; 046import com.unboundid.util.LDAPCommandLineTool; 047import com.unboundid.util.ObjectPair; 048import com.unboundid.util.OutputFormat; 049import com.unboundid.util.RateAdjustor; 050import com.unboundid.util.ResultCodeCounter; 051import com.unboundid.util.ThreadSafety; 052import com.unboundid.util.ThreadSafetyLevel; 053import com.unboundid.util.ValuePattern; 054import com.unboundid.util.WakeableSleeper; 055import com.unboundid.util.args.ArgumentException; 056import com.unboundid.util.args.ArgumentParser; 057import com.unboundid.util.args.BooleanArgument; 058import com.unboundid.util.args.FileArgument; 059import com.unboundid.util.args.IntegerArgument; 060import com.unboundid.util.args.ScopeArgument; 061import com.unboundid.util.args.StringArgument; 062 063import static com.unboundid.util.Debug.*; 064import 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) 157public 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}