001/* 002 * Copyright 2009-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2009-2018 Ping Identity Corporation 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.ArrayList; 030import java.util.LinkedHashMap; 031import java.util.LinkedHashSet; 032import java.util.List; 033import java.util.concurrent.CyclicBarrier; 034import java.util.concurrent.atomic.AtomicBoolean; 035import java.util.concurrent.atomic.AtomicLong; 036 037import com.unboundid.ldap.sdk.Control; 038import com.unboundid.ldap.sdk.LDAPConnection; 039import com.unboundid.ldap.sdk.LDAPConnectionOptions; 040import com.unboundid.ldap.sdk.LDAPException; 041import com.unboundid.ldap.sdk.ResultCode; 042import com.unboundid.ldap.sdk.SearchScope; 043import com.unboundid.ldap.sdk.Version; 044import com.unboundid.ldap.sdk.controls.AuthorizationIdentityRequestControl; 045import com.unboundid.ldap.sdk.experimental. 046 DraftBeheraLDAPPasswordPolicy10RequestControl; 047import com.unboundid.util.ColumnFormatter; 048import com.unboundid.util.FixedRateBarrier; 049import com.unboundid.util.FormattableColumn; 050import com.unboundid.util.HorizontalAlignment; 051import com.unboundid.util.LDAPCommandLineTool; 052import com.unboundid.util.ObjectPair; 053import com.unboundid.util.OutputFormat; 054import com.unboundid.util.RateAdjustor; 055import com.unboundid.util.ResultCodeCounter; 056import com.unboundid.util.ThreadSafety; 057import com.unboundid.util.ThreadSafetyLevel; 058import com.unboundid.util.ValuePattern; 059import com.unboundid.util.WakeableSleeper; 060import com.unboundid.util.args.ArgumentException; 061import com.unboundid.util.args.ArgumentParser; 062import com.unboundid.util.args.BooleanArgument; 063import com.unboundid.util.args.ControlArgument; 064import com.unboundid.util.args.FileArgument; 065import com.unboundid.util.args.IntegerArgument; 066import com.unboundid.util.args.ScopeArgument; 067import com.unboundid.util.args.StringArgument; 068 069import static com.unboundid.util.Debug.*; 070import 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) 163public 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", true); 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", true); 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", true); 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", true); 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", true); 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", true); 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", true); 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", true); 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", true); 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", true); 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", true); 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", true); 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", true); 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", true); 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", true); 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 true); 699 parser.addArgument(suppressErrorsArgument); 700 701 description = "Generate output in CSV format rather than a " + 702 "display-friendly format"; 703 csvFormat = new BooleanArgument('c', "csv", 1, description); 704 parser.addArgument(csvFormat); 705 706 description = "Specifies the seed to use for the random number generator."; 707 randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}", 708 description); 709 randomSeed.addLongIdentifier("random-seed", true); 710 parser.addArgument(randomSeed); 711 } 712 713 714 715 /** 716 * Indicates whether this tool supports creating connections to multiple 717 * servers. If it is to support multiple servers, then the "--hostname" and 718 * "--port" arguments will be allowed to be provided multiple times, and 719 * will be required to be provided the same number of times. The same type of 720 * communication security and bind credentials will be used for all servers. 721 * 722 * @return {@code true} if this tool supports creating connections to 723 * multiple servers, or {@code false} if not. 724 */ 725 @Override() 726 protected boolean supportsMultipleServers() 727 { 728 return true; 729 } 730 731 732 733 /** 734 * Retrieves the connection options that should be used for connections 735 * created for use with this tool. 736 * 737 * @return The connection options that should be used for connections created 738 * for use with this tool. 739 */ 740 @Override() 741 public LDAPConnectionOptions getConnectionOptions() 742 { 743 final LDAPConnectionOptions options = new LDAPConnectionOptions(); 744 options.setUseSynchronousMode(true); 745 return options; 746 } 747 748 749 750 /** 751 * Performs the actual processing for this tool. In this case, it gets a 752 * connection to the directory server and uses it to perform the requested 753 * searches. 754 * 755 * @return The result code for the processing that was performed. 756 */ 757 @Override() 758 public ResultCode doToolProcessing() 759 { 760 runningThread = Thread.currentThread(); 761 762 try 763 { 764 return doToolProcessingInternal(); 765 } 766 finally 767 { 768 runningThread = null; 769 } 770 } 771 772 773 774 /** 775 * Performs the actual processing for this tool. In this case, it gets a 776 * connection to the directory server and uses it to perform the requested 777 * searches. 778 * 779 * @return The result code for the processing that was performed. 780 */ 781 private ResultCode doToolProcessingInternal() 782 { 783 // If the sample rate file argument was specified, then generate the sample 784 // variable rate data file and return. 785 if (sampleRateFile.isPresent()) 786 { 787 try 788 { 789 RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue()); 790 return ResultCode.SUCCESS; 791 } 792 catch (final Exception e) 793 { 794 debugException(e); 795 err("An error occurred while trying to write sample variable data " + 796 "rate file '", sampleRateFile.getValue().getAbsolutePath(), 797 "': ", getExceptionMessage(e)); 798 return ResultCode.LOCAL_ERROR; 799 } 800 } 801 802 803 // Determine the random seed to use. 804 final Long seed; 805 if (randomSeed.isPresent()) 806 { 807 seed = Long.valueOf(randomSeed.getValue()); 808 } 809 else 810 { 811 seed = null; 812 } 813 814 // Create value patterns for the base DN and filter. 815 final ValuePattern dnPattern; 816 try 817 { 818 dnPattern = new ValuePattern(baseDN.getValue(), seed); 819 } 820 catch (final ParseException pe) 821 { 822 debugException(pe); 823 err("Unable to parse the base DN value pattern: ", pe.getMessage()); 824 return ResultCode.PARAM_ERROR; 825 } 826 827 final ValuePattern filterPattern; 828 try 829 { 830 filterPattern = new ValuePattern(filter.getValue(), seed); 831 } 832 catch (final ParseException pe) 833 { 834 debugException(pe); 835 err("Unable to parse the filter pattern: ", pe.getMessage()); 836 return ResultCode.PARAM_ERROR; 837 } 838 839 840 // Get the attributes to return. 841 final String[] attrs; 842 if (attributes.isPresent()) 843 { 844 final List<String> attrList = attributes.getValues(); 845 attrs = new String[attrList.size()]; 846 attrList.toArray(attrs); 847 } 848 else 849 { 850 attrs = NO_STRINGS; 851 } 852 853 854 // If the --ratePerSecond option was specified, then limit the rate 855 // accordingly. 856 FixedRateBarrier fixedRateBarrier = null; 857 if (ratePerSecond.isPresent() || variableRateData.isPresent()) 858 { 859 // We might not have a rate per second if --variableRateData is specified. 860 // The rate typically doesn't matter except when we have warm-up 861 // intervals. In this case, we'll run at the max rate. 862 final int intervalSeconds = collectionInterval.getValue(); 863 final int ratePerInterval = 864 (ratePerSecond.getValue() == null) 865 ? Integer.MAX_VALUE 866 : ratePerSecond.getValue() * intervalSeconds; 867 fixedRateBarrier = 868 new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval); 869 } 870 871 872 // If --variableRateData was specified, then initialize a RateAdjustor. 873 RateAdjustor rateAdjustor = null; 874 if (variableRateData.isPresent()) 875 { 876 try 877 { 878 rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier, 879 ratePerSecond.getValue(), variableRateData.getValue()); 880 } 881 catch (final IOException e) 882 { 883 debugException(e); 884 err("Initializing the variable rates failed: " + e.getMessage()); 885 return ResultCode.PARAM_ERROR; 886 } 887 catch (final IllegalArgumentException e) 888 { 889 debugException(e); 890 err("Initializing the variable rates failed: " + e.getMessage()); 891 return ResultCode.PARAM_ERROR; 892 } 893 } 894 895 896 // Determine whether to include timestamps in the output and if so what 897 // format should be used for them. 898 final boolean includeTimestamp; 899 final String timeFormat; 900 if (timestampFormat.getValue().equalsIgnoreCase("with-date")) 901 { 902 includeTimestamp = true; 903 timeFormat = "dd/MM/yyyy HH:mm:ss"; 904 } 905 else if (timestampFormat.getValue().equalsIgnoreCase("without-date")) 906 { 907 includeTimestamp = true; 908 timeFormat = "HH:mm:ss"; 909 } 910 else 911 { 912 includeTimestamp = false; 913 timeFormat = null; 914 } 915 916 917 // Get the controls to include in bind requests. 918 final ArrayList<Control> bindControls = new ArrayList<Control>(5); 919 if (authorizationIdentityRequestControl.isPresent()) 920 { 921 bindControls.add(new AuthorizationIdentityRequestControl()); 922 } 923 924 if (passwordPolicyRequestControl.isPresent()) 925 { 926 bindControls.add(new DraftBeheraLDAPPasswordPolicy10RequestControl()); 927 } 928 929 bindControls.addAll(bindControl.getValues()); 930 931 932 // Determine whether any warm-up intervals should be run. 933 final long totalIntervals; 934 final boolean warmUp; 935 int remainingWarmUpIntervals = warmUpIntervals.getValue(); 936 if (remainingWarmUpIntervals > 0) 937 { 938 warmUp = true; 939 totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals; 940 } 941 else 942 { 943 warmUp = true; 944 totalIntervals = 0L + numIntervals.getValue(); 945 } 946 947 948 // Create the table that will be used to format the output. 949 final OutputFormat outputFormat; 950 if (csvFormat.isPresent()) 951 { 952 outputFormat = OutputFormat.CSV; 953 } 954 else 955 { 956 outputFormat = OutputFormat.COLUMNS; 957 } 958 959 final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp, 960 timeFormat, outputFormat, " ", 961 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 962 "Auths/Sec"), 963 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 964 "Avg Dur ms"), 965 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 966 "Errors/Sec"), 967 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall", 968 "Auths/Sec"), 969 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall", 970 "Avg Dur ms")); 971 972 973 // Create values to use for statistics collection. 974 final AtomicLong authCounter = new AtomicLong(0L); 975 final AtomicLong errorCounter = new AtomicLong(0L); 976 final AtomicLong authDurations = new AtomicLong(0L); 977 final ResultCodeCounter rcCounter = new ResultCodeCounter(); 978 979 980 // Determine the length of each interval in milliseconds. 981 final long intervalMillis = 1000L * collectionInterval.getValue(); 982 983 984 // Create the threads to use for the searches. 985 final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1); 986 final AuthRateThread[] threads = new AuthRateThread[numThreads.getValue()]; 987 for (int i=0; i < threads.length; i++) 988 { 989 final LDAPConnection searchConnection; 990 final LDAPConnection bindConnection; 991 try 992 { 993 searchConnection = getConnection(); 994 bindConnection = getConnection(); 995 } 996 catch (final LDAPException le) 997 { 998 debugException(le); 999 err("Unable to connect to the directory server: ", 1000 getExceptionMessage(le)); 1001 return le.getResultCode(); 1002 } 1003 1004 threads[i] = new AuthRateThread(this, i, searchConnection, bindConnection, 1005 dnPattern, scopeArg.getValue(), filterPattern, attrs, 1006 userPassword.getValue(), bindOnly.isPresent(), authType.getValue(), 1007 searchControl.getValues(), bindControls, barrier, authCounter, 1008 authDurations, errorCounter, rcCounter, fixedRateBarrier); 1009 threads[i].start(); 1010 } 1011 1012 1013 // Display the table header. 1014 for (final String headerLine : formatter.getHeaderLines(true)) 1015 { 1016 out(headerLine); 1017 } 1018 1019 1020 // Start the RateAdjustor before the threads so that the initial value is 1021 // in place before any load is generated unless we're doing a warm-up in 1022 // which case, we'll start it after the warm-up is complete. 1023 if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0)) 1024 { 1025 rateAdjustor.start(); 1026 } 1027 1028 1029 // Indicate that the threads can start running. 1030 try 1031 { 1032 barrier.await(); 1033 } 1034 catch (final Exception e) 1035 { 1036 debugException(e); 1037 } 1038 1039 long overallStartTime = System.nanoTime(); 1040 long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis; 1041 1042 1043 boolean setOverallStartTime = false; 1044 long lastDuration = 0L; 1045 long lastNumErrors = 0L; 1046 long lastNumAuths = 0L; 1047 long lastEndTime = System.nanoTime(); 1048 for (long i=0; i < totalIntervals; i++) 1049 { 1050 if (rateAdjustor != null) 1051 { 1052 if (! rateAdjustor.isAlive()) 1053 { 1054 out("All of the rates in " + variableRateData.getValue().getName() + 1055 " have been completed."); 1056 break; 1057 } 1058 } 1059 1060 final long startTimeMillis = System.currentTimeMillis(); 1061 final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis; 1062 nextIntervalStartTime += intervalMillis; 1063 if (sleepTimeMillis > 0) 1064 { 1065 sleeper.sleep(sleepTimeMillis); 1066 } 1067 1068 if (stopRequested.get()) 1069 { 1070 break; 1071 } 1072 1073 final long endTime = System.nanoTime(); 1074 final long intervalDuration = endTime - lastEndTime; 1075 1076 final long numAuths; 1077 final long numErrors; 1078 final long totalDuration; 1079 if (warmUp && (remainingWarmUpIntervals > 0)) 1080 { 1081 numAuths = authCounter.getAndSet(0L); 1082 numErrors = errorCounter.getAndSet(0L); 1083 totalDuration = authDurations.getAndSet(0L); 1084 } 1085 else 1086 { 1087 numAuths = authCounter.get(); 1088 numErrors = errorCounter.get(); 1089 totalDuration = authDurations.get(); 1090 } 1091 1092 final long recentNumAuths = numAuths - lastNumAuths; 1093 final long recentNumErrors = numErrors - lastNumErrors; 1094 final long recentDuration = totalDuration - lastDuration; 1095 1096 final double numSeconds = intervalDuration / 1000000000.0d; 1097 final double recentAuthRate = recentNumAuths / numSeconds; 1098 final double recentErrorRate = recentNumErrors / numSeconds; 1099 1100 final double recentAvgDuration; 1101 if (recentNumAuths > 0L) 1102 { 1103 recentAvgDuration = 1.0d * recentDuration / recentNumAuths / 1000000; 1104 } 1105 else 1106 { 1107 recentAvgDuration = 0.0d; 1108 } 1109 1110 if (warmUp && (remainingWarmUpIntervals > 0)) 1111 { 1112 out(formatter.formatRow(recentAuthRate, recentAvgDuration, 1113 recentErrorRate, "warming up", "warming up")); 1114 1115 remainingWarmUpIntervals--; 1116 if (remainingWarmUpIntervals == 0) 1117 { 1118 out("Warm-up completed. Beginning overall statistics collection."); 1119 setOverallStartTime = true; 1120 if (rateAdjustor != null) 1121 { 1122 rateAdjustor.start(); 1123 } 1124 } 1125 } 1126 else 1127 { 1128 if (setOverallStartTime) 1129 { 1130 overallStartTime = lastEndTime; 1131 setOverallStartTime = false; 1132 } 1133 1134 final double numOverallSeconds = 1135 (endTime - overallStartTime) / 1000000000.0d; 1136 final double overallAuthRate = numAuths / numOverallSeconds; 1137 1138 final double overallAvgDuration; 1139 if (numAuths > 0L) 1140 { 1141 overallAvgDuration = 1.0d * totalDuration / numAuths / 1000000; 1142 } 1143 else 1144 { 1145 overallAvgDuration = 0.0d; 1146 } 1147 1148 out(formatter.formatRow(recentAuthRate, recentAvgDuration, 1149 recentErrorRate, overallAuthRate, overallAvgDuration)); 1150 1151 lastNumAuths = numAuths; 1152 lastNumErrors = numErrors; 1153 lastDuration = totalDuration; 1154 } 1155 1156 final List<ObjectPair<ResultCode,Long>> rcCounts = 1157 rcCounter.getCounts(true); 1158 if ((! suppressErrorsArgument.isPresent()) && (! rcCounts.isEmpty())) 1159 { 1160 err("\tError Results:"); 1161 for (final ObjectPair<ResultCode,Long> p : rcCounts) 1162 { 1163 err("\t", p.getFirst().getName(), ": ", p.getSecond()); 1164 } 1165 } 1166 1167 lastEndTime = endTime; 1168 } 1169 1170 1171 // Shut down the RateAdjustor if we have one. 1172 if (rateAdjustor != null) 1173 { 1174 rateAdjustor.shutDown(); 1175 } 1176 1177 1178 // Stop all of the threads. 1179 ResultCode resultCode = ResultCode.SUCCESS; 1180 for (final AuthRateThread t : threads) 1181 { 1182 final ResultCode r = t.stopRunning(); 1183 if (resultCode == ResultCode.SUCCESS) 1184 { 1185 resultCode = r; 1186 } 1187 } 1188 1189 return resultCode; 1190 } 1191 1192 1193 1194 /** 1195 * Requests that this tool stop running. This method will attempt to wait 1196 * for all threads to complete before returning control to the caller. 1197 */ 1198 public void stopRunning() 1199 { 1200 stopRequested.set(true); 1201 sleeper.wakeup(); 1202 1203 final Thread t = runningThread; 1204 if (t != null) 1205 { 1206 try 1207 { 1208 t.join(); 1209 } 1210 catch (final Exception e) 1211 { 1212 debugException(e); 1213 1214 if (e instanceof InterruptedException) 1215 { 1216 Thread.currentThread().interrupt(); 1217 } 1218 } 1219 } 1220 } 1221 1222 1223 1224 /** 1225 * {@inheritDoc} 1226 */ 1227 @Override() 1228 public LinkedHashMap<String[],String> getExampleUsages() 1229 { 1230 final LinkedHashMap<String[],String> examples = 1231 new LinkedHashMap<String[],String>(2); 1232 1233 String[] args = 1234 { 1235 "--hostname", "server.example.com", 1236 "--port", "389", 1237 "--bindDN", "uid=admin,dc=example,dc=com", 1238 "--bindPassword", "password", 1239 "--baseDN", "dc=example,dc=com", 1240 "--scope", "sub", 1241 "--filter", "(uid=user.[1-1000000])", 1242 "--credentials", "password", 1243 "--numThreads", "10" 1244 }; 1245 String description = 1246 "Test authentication performance by searching randomly across a set " + 1247 "of one million users located below 'dc=example,dc=com' with ten " + 1248 "concurrent threads and performing simple binds with a password of " + 1249 "'password'. The searches will be performed anonymously."; 1250 examples.put(args, description); 1251 1252 args = new String[] 1253 { 1254 "--generateSampleRateFile", "variable-rate-data.txt" 1255 }; 1256 description = 1257 "Generate a sample variable rate definition file that may be used " + 1258 "in conjunction with the --variableRateData argument. The sample " + 1259 "file will include comments that describe the format for data to be " + 1260 "included in this file."; 1261 examples.put(args, description); 1262 1263 return examples; 1264 } 1265}