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