class MCollective::DDL::AgentDDL
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 => "https://docs.puppetlabs.com/mcollective/", :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
Public Class Methods
# File lib/mcollective/ddl/agentddl.rb 41 def initialize(plugin, plugintype=:agent, loadddl=true) 42 @process_aggregate_functions = nil 43 44 super 45 end
Public Instance Methods
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
# File lib/mcollective/ddl/agentddl.rb 116 def action(name, input, &block) 117 raise "Action needs a :description property" unless input.include?(:description) 118 119 unless @entities.include?(name) 120 @entities[name] = {} 121 @entities[name][:action] = name 122 @entities[name][:input] = {} 123 @entities[name][:output] = {} 124 @entities[name][:display] = :failed 125 @entities[name][:description] = input[:description] 126 end 127 128 # if a block is passed it might be creating input methods, call it 129 # we set @current_entity so the input block can know what its talking 130 # to, this is probably an epic hack, need to improve. 131 @current_entity = name 132 block.call if block_given? 133 @current_entity = nil 134 end
Returns the interface for a specific action
# File lib/mcollective/ddl/agentddl.rb 243 def action_interface(name) 244 @entities[name] || {} 245 end
Returns an array of actions this agent support
# File lib/mcollective/ddl/agentddl.rb 248 def actions 249 @entities.keys 250 end
Sets the aggregate array for the given action
# File lib/mcollective/ddl/agentddl.rb 70 def aggregate(function, format = {:format => nil}) 71 raise(DDLValidationError, "Formats supplied to aggregation functions should be a hash") unless format.is_a?(Hash) 72 raise(DDLValidationError, "Formats supplied to aggregation functions must have a :format key") unless format.keys.include?(:format) 73 raise(DDLValidationError, "Functions supplied to aggregate should be a hash") unless function.is_a?(Hash) 74 75 unless (function.keys.include?(:args)) && function[:args] 76 raise DDLValidationError, "aggregate method for action '%s' missing a function parameter" % 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
Sets the display preference to either :ok, :failed, :flatten or :always operates on action level
# File lib/mcollective/ddl/agentddl.rb 85 def display(pref) 86 if pref == :flatten 87 Log.warn("The display option :flatten is being deprecated and will be removed in the next minor release.") 88 end 89 90 # defaults to old behavior, complain if its supplied and invalid 91 unless [:ok, :failed, :flatten, :always].include?(pref) 92 raise "Display preference #{pref} is not valid, should be :ok, :failed, :flatten or :always" 93 end 94 95 action = @current_entity 96 @entities[action][:display] = pref 97 end
# File lib/mcollective/ddl/agentddl.rb 47 def input(argument, properties) 48 raise "Input needs a :optional property" unless properties.include?(:optional) 49 50 super 51 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.
# File lib/mcollective/ddl/agentddl.rb 150 def is_function?(method_name) 151 PluginManager.find("aggregate").include?(method_name.to_s) 152 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
# File lib/mcollective/ddl/agentddl.rb 139 def method_missing(name, *args, &block) 140 unless @process_aggregate_functions || is_function?(name) 141 raise NoMethodError, "undefined local variable or method `#{name}'", caller 142 end 143 144 return {:function => name, :args => args} 145 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
# File lib/mcollective/ddl/agentddl.rb 163 def set_default_input_arguments(action, arguments) 164 input = action_interface(action)[:input] 165 166 return unless input 167 168 input.keys.each do |key| 169 if key.is_a?(Symbol) && arguments.include?(key.to_s) && !input.include?(key.to_s) 170 compat_arg = key.to_s 171 else 172 compat_arg = key 173 end 174 175 if !arguments.include?(compat_arg) && !input[key][:default].nil? && !input[key][:optional] 176 Log.debug("Setting default value for input '%s' to '%s'" % [key, input[key][:default]]) 177 arguments[compat_arg] = input[key][:default] 178 end 179 end 180 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
# File lib/mcollective/ddl/agentddl.rb 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
Creates a new set of arguments with string arguments mapped to symbol ones
This is to assist with moving to a JSON pure world where requests might come in from REST or other languages, those languages and indeed JSON itself does not support symbols.
It ensures a backward compatible mode where for rpcutil both of these requests are equivelant
c.get_fact(:fact => "cluster") c.get_fact("fact" => "cluster")
The case where both :fact and “fact” is in the DDL
cannot be handled correctly and this code will assume the caller means “fact” in that case. There's no way to represent such a request in JSON and just in general sounds like a bad idea, so a warning is logged which would in default client configuration appear on the clients display
# File lib/mcollective/ddl/agentddl.rb 199 def symbolize_basic_input_arguments(input, arguments) 200 warned = false 201 202 Hash[arguments.map do |key, value| 203 if input.include?(key.intern) && input.include?(key.to_s) && !warned 204 Log.warn("String and Symbol versions of input %s found in the DDL for %s, ensure your DDL keys are unique." % [key, @pluginname]) 205 warned = true 206 end 207 208 if key.is_a?(String) && input.include?(key.intern) && !input.include?(key) 209 [key.intern, value] 210 else 211 [key, value] 212 end 213 end] 214 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.
# File lib/mcollective/ddl/agentddl.rb 218 def validate_rpc_request(action, arguments) 219 # is the action known? 220 unless actions.include?(action) 221 raise DDLValidationError, "Attempted to call action #{action} for #{@pluginname} but it's not declared in the DDL" 222 end 223 224 input = action_interface(action)[:input] || {} 225 compatible_args = symbolize_basic_input_arguments(input, arguments) 226 227 input.keys.each do |key| 228 unless input[key][:optional] 229 unless compatible_args.include?(key) 230 raise DDLValidationError, "Action #{action} needs a #{key} argument" 231 end 232 end 233 234 if compatible_args.include?(key) 235 validate_input_argument(input, key, compatible_args[key]) 236 end 237 end 238 239 true 240 end