001/* 002 * $RCSfile: Target.java,v $ 003 * $Revision: 1.1.2.17.2.2 $ $Date: 2012/08/28 23:48:09 $ 004 * 005 * COPYRIGHT 006 * Copyright 2009-2012 Cloud Software Group, Inc. ALL RIGHTS RESERVED. 007 * Cloud Software Group, Inc. Confidential Information 008 */ 009package com.kabira.platform.management; 010 011import com.kabira.platform.KeyFieldValueList; 012import com.kabira.platform.KeyManager; 013import com.kabira.platform.KeyQuery; 014import com.kabira.platform.LockMode; 015import com.kabira.platform.ManagedObject; 016import com.kabira.platform.ObjectNotUniqueError; 017import com.kabira.platform.OutParameter; 018import com.kabira.platform.switchadmin.AdminReport; 019import com.kabira.platform.switchadmin.CommandStatus; 020import com.kabira.platform.switchadmin.JavaCommand; 021import com.kabira.ktvm.transaction.DeadlockError; 022import java.io.PrintWriter; 023import java.io.StringWriter; 024import java.lang.annotation.Annotation; 025import java.lang.reflect.InvocationTargetException; 026import java.lang.reflect.Method; 027import java.util.HashMap; 028import java.util.logging.Logger; 029 030/** 031 * System management targets allow developers to extend the system 032 * management framework. System management targets are accessible from 033 * via JMX, from the command line using the "administrator" command, and from 034 * the adminstrative web interface. 035 * System management targets are defined by extending Target. 036 * Note that Target is a managed object, and is always called by the system 037 * management framework with an active transaction. 038 * <h2> 039 * Management target implementation 040 * </h2> 041 * Targets may return result sets, which consist of a list of column names and 042 * any number of rows. All rows must contain the same number of m_columns. 043 * setColumnNames() must be called before addRow(). setColumnNames may be 044 * called only once, or a TargetError 045 * will be thrown. After setColumnNames() is called, addRow() may be called 046 * any number of times. The number of m_columns in each row must match the 047 * number of column names, or TargetError will be thrown. 048 * Targets are registered by calling Target.register(). A new instance of the 049 * Target is created for every command invocation. Targets implementing 050 * asynchronous commands may store command-context-specific data in private 051 * members. 052 * 053 * Here's a simple example of an management target with a synchronous 054 * command which returns a result set: 055 * <p> 056 * <pre> 057 * public class ExampleTarget extends Target { 058 * 059 * public ExampleTarget() { 060 * super("exampletarget"); 061 * } 062 * 063 * @Command 064 * public void exampleCommand( 065 * @Parameter(name = "count", description = "number to display", 066 * defaultValue = @Default(value = "1")) 067 * int count) { 068 * String[] names = { "Who", "Where", "What" }; 069 * setColumnNames(names); 070 * 071 * String[] row = { "Colonel Mustard", "Library", "Candlestick" }; 072 * addRow(row); 073 * commandComplete(); 074 * } 075 * } 076 * </pre> 077 * 078 * <h2>Execution JVM</h2> 079 * 080 * All commands associated with an administrative target 081 * execute in the JVM in which the target was registered 082 * using the register() method. 083 * 084 * <h2>Synchronous Commands</h2> 085 * 086 * Any command which calls commandComplete() or commandFailed() before returning 087 * is a synchronous command. 088 * 089 * <h2>Asynchronous commands</h2> 090 * 091 * Commands may return without completing. In this case, the 092 * command will be held open until commandComplete() or commandFailed() is 093 * called. 094 * Note that asynchronous commands which are abandoned 095 * (e.g. the client never collects the results) will cause a leak in the 096 * underlying framework. 097 * 098 * <h2>Transactions</h2> 099 * 100 * Command methods are always called with a valid transaction. 101 * The transaction is committed after the method returns, unless an exception is 102 * thrown, in which case the transaction is rolled back. Asynchronous commands 103 * must start a transaction to call back into the Target to complete the 104 * command. 105 * 106 * <h2>Security Configuration</h2> 107 * 108 * Management targets use configuration to define access 109 * control on the methods exposed as commands on the target. 110 * 111 * Here's an example of a security configuration file for ExampleTarget: 112 * <pre> 113 * configuration "ExampleTargetSecurity" version "1" type "security" 114 * { 115 * configure security 116 * { 117 * configure AccessControl 118 * { 119 * Rule 120 * { 121 * name = "com.kabira.platform.management.ExampleTarget"; 122 * lockAllElements = true; 123 * accessRules = { }; 124 * } 125 * Rule 126 * { 127 * name = "com.kabira.platform.management.ExampleTarget.examplecommand"; 128 * 129 * accessRules = 130 * { 131 * { 132 * roleName = "switchmonitor"; 133 * permission = Execute; 134 * } 135 * }; 136 * }; 137 * }; 138 * }; 139 * }; 140 * </pre> 141 */ 142 143public class Target extends JavaCommand 144{ 145 AdminReport adminReport = null; 146 private CommandStatus m_commandStatus = CommandStatus.CommandInProgress; 147 private long m_columns = 0; 148 149 /** 150 * Register a Target. 151 * <p> 152 * All commands associated with the target being registered will 153 * execute in the JVM in which register was called. 154 * @param targetClass the class of the target, e.g. MyTarget.class 155 * @throws TargetError The target failed registration audit. 156 * 157 */ 158 public static void register(Class targetClass) 159 throws TargetError 160 { 161 String targetName = getTargetName(targetClass); 162 163 Target.audit(targetClass, targetName); 164 165 // 166 // Always unregister the target name before registering it, since 167 // a Factory created in a previous instance of the engine will not 168 // be accessible. 169 // 170 unregister(targetName); 171 172 try 173 { 174 Factory factory = new Factory(targetClass, targetName); 175 } 176 catch (ObjectNotUniqueError ex) 177 { 178 // two callers got here at once. swallow this 179 // error. the other caller won. 180 } 181 } 182 183 /** 184 * Unregister a previously-registered target. 185 * @param targetName the name of the target 186 */ 187 public static void unregister(String targetName) 188 { 189 KeyManager<Factory> keyManager = new KeyManager<Factory>(); 190 KeyQuery<Factory> keyQuery = keyManager.createKeyQuery( 191 Factory.class, "ByName"); 192 KeyFieldValueList keyFieldValueList = new KeyFieldValueList(); 193 194 // 195 // Define the query to find the Factory 196 // 197 keyFieldValueList.add("name", targetName); 198 keyQuery.defineQuery(keyFieldValueList); 199 200 Factory factory = keyQuery.getSingleResult(LockMode.WRITELOCK); 201 202 if (factory != null) 203 { 204 ManagedObject.delete(factory); 205 } 206 } 207 208 /** 209 * Indicate that the command has completed successfully. 210 */ 211 protected final void commandComplete() 212 { 213 String results = ""; 214 215 m_commandStatus = CommandStatus.CommandCompleted; 216 217 if (adminReport != null) 218 { 219 OutParameter<String> out = new OutParameter<String>(); 220 adminReport.getReport(out); 221 results = out.getValue(); 222 223 ManagedObject.delete(adminReport); 224 adminReport = null; 225 226 } 227 update(m_commandStatus, results, ""); 228 } 229 230 /** 231 * Indicate that the command has failed. 232 * The Transaction will be 233 * committed. To roll back the transaction, throw an exception from the 234 * method implementing the command. 235 * @param reason The reason the command failed. 236 */ 237 protected final void commandFailed(String reason) 238 { 239 m_commandStatus = CommandStatus.CommandFailed; 240 update(m_commandStatus, "", reason); 241 } 242 243 /** 244 * Set the names of the m_columns for the result set. 245 * @param names The names of the m_columns for the result set 246 * @throws TargetError The method was called more than once 247 */ 248 protected final void setColumnNames(String[] names) 249 throws TargetError 250 { 251 if (adminReport != null) 252 { 253 throw new TargetError("setColumnNames called twice"); 254 } 255 if (names.length == 0) 256 { 257 throw new TargetError("zero column names provided"); 258 } 259 260 adminReport = new AdminReport(); 261 262 adminReport.setHeader(names); 263 264 m_columns = names.length; 265 } 266 267 /** 268 * Add a row to the result set 269 * @param values The list of values for this row 270 * @throws TargetError The number of values does not match 271 * the number of column names, or setColumnNames() has not been called yet. 272 */ 273 protected final void addRow(String[] values) 274 throws TargetError 275 { 276 if (adminReport == null) 277 { 278 throw new TargetError( 279 "addRow called without calling setColumnNames"); 280 } 281 282 if (values.length != m_columns) 283 { 284 285 throw new TargetError( 286 "addRow: number of values (" + values.length + 287 ") does not match number of columns (" + m_columns + 288 ")"); 289 } 290 291 adminReport.addRecord(values); 292 } 293 294 /** 295 * Execute a command. This is an internal method which should never 296 * be called. 297 * @param parameters the command parameters 298 */ 299 // 300 // unchecked conversion warnings are suppressed for Enum.valueOf(), 301 // which cannot be fixed because generics are not covariant. 302 // 303 @SuppressWarnings("unchecked") 304 @Override 305 protected final void execute(final String[] parameters) 306 { 307 if (commandName.equals("help")) 308 { 309 help(); 310 return; 311 } 312 313 // 314 // Check to see if the current user has permission to execute 315 // this command 316 // 317 Boolean ok = false; 318 ok = checkAccess(this.getClass().getName() + "." + commandName); 319 if (!ok) 320 { 321 String msg = "An access violation occurred while " + 322 "attempting to execute command [" + commandName + 323 "] on target [" + targetName + 324 "]. The calling principal " + 325 "does not have execute privileges for this command."; 326 throw new SecurityException(msg); 327 } 328 329 Method method = null; 330 331 for (Method m : this.getClass().getDeclaredMethods()) 332 { 333 if ((m.getAnnotation(Command.class) != null) && 334 (m.getName().equals(commandName))) 335 { 336 method = m; 337 break; 338 } 339 } 340 341 if (method == null) 342 { 343 commandFailed("command '" + commandName + 344 "' not found in target '" + targetName + "'"); 345 return; 346 } 347 348 Annotation[][] all = method.getParameterAnnotations(); 349 Class pt[] = method.getParameterTypes(); 350 Object[] args = new Object[pt.length]; 351 352 HashMap<String, String> paramap = new HashMap<String, String>(); 353 354 for (int pi = 0; pi < parameters.length;) 355 { 356 paramap.put(parameters[pi++], parameters[pi++]); 357 } 358 359 // 360 // for each of the method's parameters, find the supplied parameter 361 // or use the default (if it isn't required) 362 // 363 for (int i = 0; i < pt.length; i++) 364 { 365 Parameter pp = null; 366 Default defaultValue = null; 367 368 // 369 // find the Parameter annotation for this parameter 370 // 371 for (Annotation a : all[i]) 372 { 373 if (a.annotationType() == Parameter.class) 374 { 375 pp = (Parameter) a; 376 } 377 } 378 379 String name = pp.name(); 380 String value = paramap.get(name); 381 382 if (value != null) 383 { 384 paramap.remove(name); 385 } 386 else 387 { 388 if (pp.defaultValue().provided()) 389 { 390 value = pp.defaultValue().value(); 391 } 392 else if (pp.required() == true) 393 { 394 commandFailed("required parameter " + 395 pp.name() + " missing"); 396 return; 397 } 398 } 399 400 Class type = pt[i]; 401 402 try 403 { 404 if (value == null) 405 { 406 args[i] = null; 407 } 408 else if (type == String.class) 409 { 410 args[i] = value; 411 } 412 else if (type.isEnum()) 413 { 414 // 415 // unchecked warnings if not suppressed 416 // 417 args[i] = Enum.valueOf(type, value); 418 } 419 else if (type == Boolean.class) 420 { 421 args[i] = Boolean.valueOf(value); 422 } 423 else if (type == Character.class) 424 { 425 args[i] = Character.valueOf(value.charAt(0)); 426 } 427 else if (type == Integer.class) 428 { 429 args[i] = Integer.parseInt(value); 430 } 431 else if (type == Byte.class) 432 { 433 args[i] = Byte.parseByte(value); 434 } 435 else if (type == Short.class) 436 { 437 args[i] = Short.parseShort(value); 438 } 439 else if (type == Long.class) 440 { 441 args[i] = Long.parseLong(value); 442 } 443 else if (type == Float.class) 444 { 445 args[i] = Float.parseFloat(value); 446 } 447 else 448 { 449 assert (type == Double.class); 450 args[i] = Double.parseDouble(value); 451 } 452 } 453 catch (NumberFormatException ex) 454 { 455 commandFailed("Invalid numeric format '" + value + 456 "' for parameter '" + name + ": " + 457 ex.getMessage()); 458 return; 459 } 460 catch (IllegalArgumentException ex) 461 { 462 commandFailed("Illegal argument '" + value + 463 "' for parameter '" + name + ": " + 464 ex.getMessage()); 465 return; 466 } 467 } 468 469 if (!paramap.isEmpty()) 470 { 471 commandFailed("invalid parameter(s): " + paramap.toString()); 472 return; 473 } 474 475 try 476 { 477 method.setAccessible(true); 478 method.invoke(this, args); 479 } 480 catch (InvocationTargetException ex) 481 { 482 String reason = "InvocationTargetException: "; 483 484 Throwable cause = ex.getCause(); 485 486 if (cause != null) 487 { 488 if (cause instanceof DeadlockError) 489 { 490 throw (java.lang.Error)cause; 491 } 492 493 reason = cause.getMessage(); 494 495 StringWriter stringWriter = new StringWriter(); 496 PrintWriter printWriter = new PrintWriter(stringWriter); 497 cause.printStackTrace(printWriter); 498 499 reason += stringWriter.toString(); 500 } 501 else 502 { 503 reason += ex.getMessage(); 504 } 505 506 setAbortFlag(); 507 commandFailed(reason); 508 } 509 catch (Exception ex) 510 { 511 ex.printStackTrace(); 512 setAbortFlag(); 513 commandFailed(ex.getMessage()); 514 } 515 } 516 517 /** 518 * Retrieve the name of the currently active Principal. This operation 519 * may be called during command execution to get the name of the Principal 520 * that invoked the command. 521 */ 522 protected final String getActivePrincipalName() 523 { 524 String result = getCurrentPrincipalName(); 525 526 if ((result == null) || (result.length() == 0)) 527 { 528 return null; 529 } 530 531 return result; 532 } 533 534 private static String getTargetName(Class<?> targetClass) 535 { 536 ManagementTarget adminTarget = 537 targetClass.getAnnotation(ManagementTarget.class); 538 539 if (adminTarget == null) 540 { 541 throw new TargetError("Missing ManagementTarget annotation on type " 542 + targetClass.getName() + ". "); 543 } 544 return adminTarget.name(); 545 } 546 547 private static void audit(Class targetClass, String targetName) 548 throws TargetError 549 { 550 String error = new String(); 551 552 for (Method m : targetClass.getMethods()) 553 { 554 if (m.getAnnotation(Command.class) == null) 555 { 556 continue; 557 } 558 559 Annotation[][] all = m.getParameterAnnotations(); 560 Class pt[] = m.getParameterTypes(); 561 562 for (int i = 0; i < pt.length; i++) 563 { 564 Parameter pp = null; 565 Default defaultValue = null; 566 567 Class type = pt[i]; 568 569 if ((type != String.class) && 570 (!type.isEnum()) && 571 (type != Boolean.class) && 572 (type != Character.class) && 573 (type != Integer.class) && 574 (type != Byte.class) && 575 (type != Short.class) && 576 (type != Long.class) && 577 (type != Float.class) && 578 (type != Double.class)) 579 { 580 error += "Unsupported parameter type " + 581 type.getName() + " in method " + m.getName() + ". "; 582 } 583 584 // 585 // find the Parameter annotation for this parameter 586 // 587 for (Annotation a : all[i]) 588 { 589 if (a.annotationType() == Parameter.class) 590 { 591 pp = (Parameter) a; 592 } 593 } 594 if (pp == null) 595 { 596 error += "Missing @Parameter annotation on parameter " + 597 "of type " + type.getName() + " in method " + 598 m.getName() + ". "; 599 } 600 } 601 } 602 603 if (!error.isEmpty()) 604 { 605 throw new TargetError("Admin target " + targetName + 606 " failed audit: " + error); 607 } 608 } 609 610 private void help() 611 { 612 String help = new String(); 613 614 help += " valid commands and parameters for target \""; 615 help += targetName; 616 help += "\":\n\n"; 617 618 for (Method m : this.getClass().getDeclaredMethods()) 619 { 620 if (m.getName().charAt(0) == '$') 621 { 622 continue; 623 } 624 625 Command command = m.getAnnotation(Command.class); 626 627 if (command == null) 628 { 629 continue; 630 } 631 632 help += " " + m.getName() + " " + targetName + "\n"; 633 634 Annotation[][] all = m.getParameterAnnotations(); 635 Class pt[] = m.getParameterTypes(); 636 637 for (int i = 0; i < pt.length; i++) 638 { 639 Parameter pp = null; 640 Class type = pt[i]; 641 642 // 643 // find the Parameter annotation for this parameter 644 // 645 for (Annotation a : all[i]) 646 { 647 if (a.annotationType() == Parameter.class) 648 { 649 pp = (Parameter) a; 650 } 651 } 652 653 String ps = pp.name() + "=<" + 654 type.getName().replace('.', ' ').replaceAll(".* ", ""); 655 656 if (pp.defaultValue().provided()) 657 { 658 ps += ", default = " + pp.defaultValue().value(); 659 } 660 ps += ">"; 661 662 if (!pp.required()) 663 { 664 ps = "[ " + ps + " ]"; 665 } 666 667 help += "\t" + ps + "\n"; 668 if (!pp.description().equals("")) 669 { 670 help += "\t\t" + pp.description() + "\n"; 671 } 672 help += "\n"; 673 } 674 675 if (!command.description().equals("")) 676 { 677 help += "\t" + command.description() + "\n"; 678 } 679 help += "\n"; 680 } 681 682 ManagementTarget adminTarget = 683 this.getClass().getAnnotation(ManagementTarget.class); 684 685 if (adminTarget != null) 686 { 687 help += "\nDescription:\n\n" + adminTarget.description() + "\n"; 688 } 689 690 update(CommandStatus.CommandCompleted, help, ""); 691 } 692}