Updated mcollective.init according to OSCI-658
[packages/precise/mcollective.git] / lib / mcollective / ddl / agentddl.rb
diff --git a/lib/mcollective/ddl/agentddl.rb b/lib/mcollective/ddl/agentddl.rb
new file mode 100644 (file)
index 0000000..5952ec0
--- /dev/null
@@ -0,0 +1,208 @@
+module MCollective
+  module DDL
+    # A DDL class specific to agent plugins.
+    #
+    # A full DDL can be seen below with all the possible bells and whistles present.
+    #
+    # metadata    :name        => "Utilities and Helpers for SimpleRPC Agents",
+    #             :description => "General helpful actions that expose stats and internals to SimpleRPC clients",
+    #             :author      => "R.I.Pienaar <rip@devco.net>",
+    #             :license     => "Apache License, Version 2.0",
+    #             :version     => "1.0",
+    #             :url         => "http://marionette-collective.org/",
+    #             :timeout     => 10
+    #
+    # action "get_fact", :description => "Retrieve a single fact from the fact store" do
+    #      display :always
+    #
+    #      input :fact,
+    #            :prompt      => "The name of the fact",
+    #            :description => "The fact to retrieve",
+    #            :type        => :string,
+    #            :validation  => '^[\w\-\.]+$',
+    #            :optional    => false,
+    #            :maxlength   => 40,
+    #            :default     => "fqdn"
+    #
+    #      output :fact,
+    #             :description => "The name of the fact being returned",
+    #             :display_as  => "Fact"
+    #
+    #      output :value,
+    #             :description => "The value of the fact",
+    #             :display_as  => "Value",
+    #             :default     => ""
+    #
+    #     summarize do
+    #         aggregate summary(:value)
+    #     end
+    # end
+    class AgentDDL<Base
+      def initialize(plugin, plugintype=:agent, loadddl=true)
+        @process_aggregate_functions = nil
+
+        super
+      end
+
+      def input(argument, properties)
+        raise "Input needs a :optional property" unless properties.include?(:optional)
+
+        super
+      end
+
+      # Calls the summarize block defined in the ddl. Block will not be called
+      # if the ddl is getting processed on the server side. This means that
+      # aggregate plugins only have to be present on the client side.
+      #
+      # The @process_aggregate_functions variable is used by the method_missing
+      # block to determine if it should kick in, this way we very tightly control
+      # where we activate the method_missing behavior turning it into a noop
+      # otherwise to maximise the chance of providing good user feedback
+      def summarize(&block)
+        unless @config.mode == :server
+          @process_aggregate_functions = true
+          block.call
+          @process_aggregate_functions = nil
+        end
+      end
+
+      # Sets the aggregate array for the given action
+      def aggregate(function, format = {:format => nil})
+        DDL.validation_fail!(:PLMC28, "Formats supplied to aggregation functions should be a hash", :error) unless format.is_a?(Hash)
+        DDL.validation_fail!(:PLMC27, "Formats supplied to aggregation functions must have a :format key", :error) unless format.keys.include?(:format)
+        DDL.validation_fail!(:PLMC26, "Functions supplied to aggregate should be a hash", :error) unless function.is_a?(Hash)
+
+        unless (function.keys.include?(:args)) && function[:args]
+          DDL.validation_fail!(:PLMC25, "aggregate method for action '%{action}' missing a function parameter", :error, :action => entities[@current_entity][:action])
+        end
+
+        entities[@current_entity][:aggregate] ||= []
+        entities[@current_entity][:aggregate] << (format[:format].nil? ? function : function.merge(format))
+      end
+
+      # Sets the display preference to either :ok, :failed, :flatten or :always
+      # operates on action level
+      def display(pref)
+        # defaults to old behavior, complain if its supplied and invalid
+        unless [:ok, :failed, :flatten, :always].include?(pref)
+          raise "Display preference #{pref} is not valid, should be :ok, :failed, :flatten or :always"
+        end
+
+        action = @current_entity
+        @entities[action][:display] = pref
+      end
+
+      # Creates the definition for an action, you can nest input definitions inside the
+      # action to attach inputs and validation to the actions
+      #
+      #    action "status", :description => "Restarts a Service" do
+      #       display :always
+      #
+      #       input  "service",
+      #              :prompt      => "Service Action",
+      #              :description => "The action to perform",
+      #              :type        => :list,
+      #              :optional    => true,
+      #              :list        => ["start", "stop", "restart", "status"]
+      #
+      #       output "status",
+      #              :description => "The status of the service after the action"
+      #
+      #    end
+      def action(name, input, &block)
+        raise "Action needs a :description property" unless input.include?(:description)
+
+        unless @entities.include?(name)
+          @entities[name] = {}
+          @entities[name][:action] = name
+          @entities[name][:input] = {}
+          @entities[name][:output] = {}
+          @entities[name][:display] = :failed
+          @entities[name][:description] = input[:description]
+        end
+
+        # if a block is passed it might be creating input methods, call it
+        # we set @current_entity so the input block can know what its talking
+        # to, this is probably an epic hack, need to improve.
+        @current_entity = name
+        block.call if block_given?
+        @current_entity = nil
+      end
+
+      # If the method name matches a # aggregate function, we return the function
+      # with args as a hash.  This will only be active if the @process_aggregate_functions
+      # is set to true which only happens in the #summarize block
+      def method_missing(name, *args, &block)
+        unless @process_aggregate_functions || is_function?(name)
+          raise NoMethodError, "undefined local variable or method `#{name}'", caller
+        end
+
+        return {:function => name, :args => args}
+      end
+
+      # Checks if a method name matches a aggregate plugin.
+      # This is used by method missing so that we dont greedily assume that
+      # every method_missing call in an agent ddl has hit a aggregate function.
+      def is_function?(method_name)
+        PluginManager.find("aggregate").include?(method_name.to_s)
+      end
+
+      # For a given action and arguments look up the DDL interface to that action
+      # and if any arguments in the DDL have a :default value assign that to any
+      # input that does not have an argument in the input arguments
+      #
+      # This is intended to only be called on clients and not on servers as the
+      # clients should never be able to publish non compliant requests and the
+      # servers should really not tamper with incoming requests since doing so
+      # might raise validation errors that were not raised on the client breaking
+      # our fail-fast approach to input validation
+      def set_default_input_arguments(action, arguments)
+        input = action_interface(action)[:input]
+
+        return unless input
+
+        input.keys.each do |key|
+          if !arguments.include?(key) && !input[key][:default].nil? && !input[key][:optional]
+            Log.debug("Setting default value for input '%s' to '%s'" % [key, input[key][:default]])
+            arguments[key] = input[key][:default]
+          end
+        end
+      end
+
+      # Helper to use the DDL to figure out if the remote call to an
+      # agent should be allowed based on action name and inputs.
+      def validate_rpc_request(action, arguments)
+        # is the action known?
+        unless actions.include?(action)
+          DDL.validation_fail!(:PLMC29, "Attempted to call action %{action} for %{plugin} but it's not declared in the DDL", :debug, :action => action, :plugin => @pluginname)
+        end
+
+        input = action_interface(action)[:input] || {}
+
+        input.keys.each do |key|
+          unless input[key][:optional]
+            unless arguments.keys.include?(key)
+              DDL.validation_fail!(:PLMC30, "Action '%{action}' needs a '%{key}' argument", :debug, :action => action, :key => key)
+            end
+          end
+
+          if arguments.keys.include?(key)
+            validate_input_argument(input, key, arguments[key])
+          end
+        end
+
+        true
+      end
+
+      # Returns the interface for a specific action
+      def action_interface(name)
+        @entities[name] || {}
+      end
+
+      # Returns an array of actions this agent support
+      def actions
+        @entities.keys
+      end
+    end
+  end
+end