Update code from https://github.com/dmi-try/marionette-collective
[packages/precise/mcollective.git] / lib / mcollective / ddl / agentddl.rb
1 module MCollective
2   module DDL
3     # A DDL class specific to agent plugins.
4     #
5     # A full DDL can be seen below with all the possible bells and whistles present.
6     #
7     # metadata    :name        => "Utilities and Helpers for SimpleRPC Agents",
8     #             :description => "General helpful actions that expose stats and internals to SimpleRPC clients",
9     #             :author      => "R.I.Pienaar <rip@devco.net>",
10     #             :license     => "Apache License, Version 2.0",
11     #             :version     => "1.0",
12     #             :url         => "http://marionette-collective.org/",
13     #             :timeout     => 10
14     #
15     # action "get_fact", :description => "Retrieve a single fact from the fact store" do
16     #      display :always
17     #
18     #      input :fact,
19     #            :prompt      => "The name of the fact",
20     #            :description => "The fact to retrieve",
21     #            :type        => :string,
22     #            :validation  => '^[\w\-\.]+$',
23     #            :optional    => false,
24     #            :maxlength   => 40,
25     #            :default     => "fqdn"
26     #
27     #      output :fact,
28     #             :description => "The name of the fact being returned",
29     #             :display_as  => "Fact"
30     #
31     #      output :value,
32     #             :description => "The value of the fact",
33     #             :display_as  => "Value",
34     #             :default     => ""
35     #
36     #     summarize do
37     #         aggregate summary(:value)
38     #     end
39     # end
40     class AgentDDL<Base
41       def initialize(plugin, plugintype=:agent, loadddl=true)
42         @process_aggregate_functions = nil
43
44         super
45       end
46
47       def input(argument, properties)
48         raise "Input needs a :optional property" unless properties.include?(:optional)
49
50         super
51       end
52
53       # Calls the summarize block defined in the ddl. Block will not be called
54       # if the ddl is getting processed on the server side. This means that
55       # aggregate plugins only have to be present on the client side.
56       #
57       # The @process_aggregate_functions variable is used by the method_missing
58       # block to determine if it should kick in, this way we very tightly control
59       # where we activate the method_missing behavior turning it into a noop
60       # otherwise to maximise the chance of providing good user feedback
61       def summarize(&block)
62         unless @config.mode == :server
63           @process_aggregate_functions = true
64           block.call
65           @process_aggregate_functions = nil
66         end
67       end
68
69       # Sets the aggregate array for the given action
70       def aggregate(function, format = {:format => nil})
71         DDL.validation_fail!(:PLMC28, "Formats supplied to aggregation functions should be a hash", :error) unless format.is_a?(Hash)
72         DDL.validation_fail!(:PLMC27, "Formats supplied to aggregation functions must have a :format key", :error) unless format.keys.include?(:format)
73         DDL.validation_fail!(:PLMC26, "Functions supplied to aggregate should be a hash", :error) unless function.is_a?(Hash)
74
75         unless (function.keys.include?(:args)) && function[:args]
76           DDL.validation_fail!(:PLMC25, "aggregate method for action '%{action}' missing a function parameter", :error, :action => entities[@current_entity][:action])
77         end
78
79         entities[@current_entity][:aggregate] ||= []
80         entities[@current_entity][:aggregate] << (format[:format].nil? ? function : function.merge(format))
81       end
82
83       # Sets the display preference to either :ok, :failed, :flatten or :always
84       # operates on action level
85       def display(pref)
86         # defaults to old behavior, complain if its supplied and invalid
87         unless [:ok, :failed, :flatten, :always].include?(pref)
88           raise "Display preference #{pref} is not valid, should be :ok, :failed, :flatten or :always"
89         end
90
91         action = @current_entity
92         @entities[action][:display] = pref
93       end
94
95       # Creates the definition for an action, you can nest input definitions inside the
96       # action to attach inputs and validation to the actions
97       #
98       #    action "status", :description => "Restarts a Service" do
99       #       display :always
100       #
101       #       input  "service",
102       #              :prompt      => "Service Action",
103       #              :description => "The action to perform",
104       #              :type        => :list,
105       #              :optional    => true,
106       #              :list        => ["start", "stop", "restart", "status"]
107       #
108       #       output "status",
109       #              :description => "The status of the service after the action"
110       #
111       #    end
112       def action(name, input, &block)
113         raise "Action needs a :description property" unless input.include?(:description)
114
115         unless @entities.include?(name)
116           @entities[name] = {}
117           @entities[name][:action] = name
118           @entities[name][:input] = {}
119           @entities[name][:output] = {}
120           @entities[name][:display] = :failed
121           @entities[name][:description] = input[:description]
122         end
123
124         # if a block is passed it might be creating input methods, call it
125         # we set @current_entity so the input block can know what its talking
126         # to, this is probably an epic hack, need to improve.
127         @current_entity = name
128         block.call if block_given?
129         @current_entity = nil
130       end
131
132       # If the method name matches a # aggregate function, we return the function
133       # with args as a hash.  This will only be active if the @process_aggregate_functions
134       # is set to true which only happens in the #summarize block
135       def method_missing(name, *args, &block)
136         unless @process_aggregate_functions || is_function?(name)
137           raise NoMethodError, "undefined local variable or method `#{name}'", caller
138         end
139
140         return {:function => name, :args => args}
141       end
142
143       # Checks if a method name matches a aggregate plugin.
144       # This is used by method missing so that we dont greedily assume that
145       # every method_missing call in an agent ddl has hit a aggregate function.
146       def is_function?(method_name)
147         PluginManager.find("aggregate").include?(method_name.to_s)
148       end
149
150       # For a given action and arguments look up the DDL interface to that action
151       # and if any arguments in the DDL have a :default value assign that to any
152       # input that does not have an argument in the input arguments
153       #
154       # This is intended to only be called on clients and not on servers as the
155       # clients should never be able to publish non compliant requests and the
156       # servers should really not tamper with incoming requests since doing so
157       # might raise validation errors that were not raised on the client breaking
158       # our fail-fast approach to input validation
159       def set_default_input_arguments(action, arguments)
160         input = action_interface(action)[:input]
161
162         return unless input
163
164         input.keys.each do |key|
165           if !arguments.include?(key) && !input[key][:default].nil? && !input[key][:optional]
166             Log.debug("Setting default value for input '%s' to '%s'" % [key, input[key][:default]])
167             arguments[key] = input[key][:default]
168           end
169         end
170       end
171
172       # Helper to use the DDL to figure out if the remote call to an
173       # agent should be allowed based on action name and inputs.
174       def validate_rpc_request(action, arguments)
175         # is the action known?
176         unless actions.include?(action)
177           DDL.validation_fail!(:PLMC29, "Attempted to call action %{action} for %{plugin} but it's not declared in the DDL", :debug, :action => action, :plugin => @pluginname)
178         end
179
180         input = action_interface(action)[:input] || {}
181
182         input.keys.each do |key|
183           unless input[key][:optional]
184             unless arguments.keys.include?(key)
185               DDL.validation_fail!(:PLMC30, "Action '%{action}' needs a '%{key}' argument", :debug, :action => action, :key => key)
186             end
187           end
188
189           if arguments.keys.include?(key)
190             validate_input_argument(input, key, arguments[key])
191           end
192         end
193
194         true
195       end
196
197       # Returns the interface for a specific action
198       def action_interface(name)
199         @entities[name] || {}
200       end
201
202       # Returns an array of actions this agent support
203       def actions
204         @entities.keys
205       end
206     end
207   end
208 end