Class MCollective::DDL
In: lib/mcollective/ddl.rb
Parent: Object

A class that helps creating data description language files for plugins. You can define meta data, actions, input and output describing the behavior of your agent or other plugins

Later you can access this information to assist with creating of user interfaces or online help

A sample DDL can be seen below, you‘d put this in your agent dir as <agent name>.ddl

   metadata :name        => "SimpleRPC Service Agent",
            :description => "Agent to manage services using the Puppet service provider",
            :author      => "R.I.Pienaar",
            :license     => "GPLv2",
            :version     => "1.1",
            :url         => "http://mcollective-plugins.googlecode.com/",
            :timeout     => 60

   action "status", :description => "Gets the status of a service" do
      display :always

      input :service,
            :prompt      => "Service Name",
            :description => "The service to get the status for",
            :type        => :string,
            :validation  => '^[a-zA-Z\-_\d]+$',
            :optional    => true,
            :maxlength   => 30

      output :status,
             :description => "The status of service",
             :display_as  => "Service Status"
  end

Methods

Attributes

entities  [R] 
meta  [R] 
pluginname  [R] 
plugintype  [R] 

Public Class methods

[Source]

    # File lib/mcollective/ddl.rb, line 38
38:     def initialize(plugin, plugintype=:agent, loadddl=true)
39:       @entities = {}
40:       @meta = {}
41:       @config = Config.instance
42:       @pluginname = plugin
43:       @plugintype = plugintype.to_sym
44: 
45:       # used to track if the method_missing that handles the
46:       # aggregate functions should do so or not
47:       @process_aggregate_functions = nil
48: 
49:       loadddlfile if loadddl
50:     end

As we‘re taking arguments on the command line we need a way to input booleans, true on the cli is a string so this method will take the ddl, find all arguments that are supposed to be boolean and if they are the strings "true"/"yes" or "false"/"no" turn them into the matching boolean

[Source]

     # File lib/mcollective/ddl.rb, line 405
405:     def self.string_to_boolean(val)
406:       return true if ["true", "t", "yes", "y", "1"].include?(val.downcase)
407:       return false if ["false", "f", "no", "n", "0"].include?(val.downcase)
408: 
409:       raise "#{val} does not look like a boolean argument"
410:     end

a generic string to number function, if a number looks like a float it turns it into a float else an int. This is naive but should be sufficient for numbers typed on the cli in most cases

[Source]

     # File lib/mcollective/ddl.rb, line 415
415:     def self.string_to_number(val)
416:       return val.to_f if val =~ /^\d+\.\d+$/
417:       return val.to_i if val =~ /^\d+$/
418: 
419:       raise "#{val} does not look like a number"
420:     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

[Source]

     # File lib/mcollective/ddl.rb, line 160
160:     def action(name, input, &block)
161:       raise "Action needs a :description property" unless input.include?(:description)
162: 
163:       unless @entities.include?(name)
164:         @entities[name] = {}
165:         @entities[name][:action] = name
166:         @entities[name][:input] = {}
167:         @entities[name][:output] = {}
168:         @entities[name][:display] = :failed
169:         @entities[name][:description] = input[:description]
170:       end
171: 
172:       # if a block is passed it might be creating input methods, call it
173:       # we set @current_entity so the input block can know what its talking
174:       # to, this is probably an epic hack, need to improve.
175:       @current_entity = name
176:       block.call if block_given?
177:       @current_entity = nil
178:     end

Returns the interface for a specific action

[Source]

     # File lib/mcollective/ddl.rb, line 316
316:     def action_interface(name)
317:       raise "Only agent DDLs have actions" unless @plugintype == :agent
318:       @entities[name] || {}
319:     end

Returns an array of actions this agent support

[Source]

     # File lib/mcollective/ddl.rb, line 304
304:     def actions
305:       raise "Only agent DDLs have actions" unless @plugintype == :agent
306:       @entities.keys
307:     end

Sets the aggregate array for the given action

[Source]

     # File lib/mcollective/ddl.rb, line 262
262:     def aggregate(function, format = {:format => nil})
263:       raise(DDLValidationError, "Formats supplied to aggregation functions should be a hash") unless format.is_a?(Hash)
264:       raise(DDLValidationError, "Formats supplied to aggregation functions must have a :format key") unless format.keys.include?(:format)
265:       raise(DDLValidationError, "Functions supplied to aggregate should be a hash") unless function.is_a?(Hash)
266: 
267:       unless (function.keys.include?(:args)) && function[:args]
268:         raise DDLValidationError, "aggregate method for action '%s' missing a function parameter" % entities[@current_entity][:action]
269:       end
270: 
271:       entities[@current_entity][:aggregate] ||= []
272:       entities[@current_entity][:aggregate] << (format[:format].nil? ? function : function.merge(format))
273:     end

records valid capabilities for discovery plugins

[Source]

     # File lib/mcollective/ddl.rb, line 127
127:     def capabilities(caps)
128:       raise "Only discovery DDLs have capabilities" unless @plugintype == :discovery
129: 
130:       caps = [caps].flatten
131: 
132:       raise "Discovery plugin capabilities can't be empty" if caps.empty?
133: 
134:       caps.each do |cap|
135:         if [:classes, :facts, :identity, :agents, :compound].include?(cap)
136:           @entities[:discovery][:capabilities] << cap
137:         else
138:           raise "%s is not a valid capability, valid capabilities are :classes, :facts, :identity, :agents and :compound" % cap
139:         end
140:       end
141:     end

Creates the definition for a data query

   dataquery :description => "Match data using Augeas" do
      input  :query,
             :prompt      => "Matcher",
             :description => "Valid Augeas match expression",
             :type        => :string,
             :validation  => /.+/,
             :maxlength   => 50

      output :size,
             :description => "The amount of records matched",
             :display_as => "Matched"
   end

[Source]

     # File lib/mcollective/ddl.rb, line 98
 98:     def dataquery(input, &block)
 99:       raise "Data queries need a :description" unless input.include?(:description)
100:       raise "Data queries can only have one definition" if @entities[:data]
101: 
102:       @entities[:data]  = {:description => input[:description],
103:                            :input => {},
104:                            :output => {}}
105: 
106:       @current_entity = :data
107:       block.call if block_given?
108:       @current_entity = nil
109:     end

Returns the interface for the data query

[Source]

     # File lib/mcollective/ddl.rb, line 310
310:     def dataquery_interface
311:       raise "Only data DDLs have data queries" unless @plugintype == :data
312:       @entities[:data] || {}
313:     end

Creates the definition for new discovery plugins

   discovery do
      capabilities [:classes, :facts, :identity, :agents, :compound]
   end

[Source]

     # File lib/mcollective/ddl.rb, line 116
116:     def discovery(&block)
117:       raise "Discovery plugins can only have one definition" if @entities[:discovery]
118: 
119:       @entities[:discovery] = {:capabilities => []}
120: 
121:       @current_entity = :discovery
122:       block.call if block_given?
123:       @current_entity = nil
124:     end

[Source]

     # File lib/mcollective/ddl.rb, line 321
321:     def discovery_interface
322:       raise "Only discovery DDLs have discovery interfaces" unless @plugintype == :discovery
323:       @entities[:discovery]
324:     end

Sets the display preference to either :ok, :failed, :flatten or :always operates on action level

[Source]

     # File lib/mcollective/ddl.rb, line 235
235:     def display(pref)
236:       # defaults to old behavior, complain if its supplied and invalid
237:       unless [:ok, :failed, :flatten, :always].include?(pref)
238:         raise "Display preference #{pref} is not valid, should be :ok, :failed, :flatten or :always"
239:       end
240: 
241:       action = @current_entity
242:       @entities[action][:display] = pref
243:     end

[Source]

    # File lib/mcollective/ddl.rb, line 60
60:     def findddlfile(ddlname=nil, ddltype=nil)
61:       ddlname = @pluginname unless ddlname
62:       ddltype = @plugintype unless ddltype
63: 
64:       @config.libdir.each do |libdir|
65:         ddlfile = File.join([libdir, "mcollective", ddltype.to_s, "#{ddlname}.ddl"])
66: 
67:         if File.exist?(ddlfile)
68:           Log.debug("Found #{ddlname} ddl at #{ddlfile}")
69:           return ddlfile
70:         end
71:       end
72:       return false
73:     end

Generates help using the template based on the data created with metadata and input.

If no template name is provided one will be chosen based on the plugin type. If the provided template path is not absolute then the template will be loaded relative to helptemplatedir configuration parameter

[Source]

     # File lib/mcollective/ddl.rb, line 291
291:     def help(template=nil)
292:       template = template_for_plugintype unless template
293:       template = File.join(@config.helptemplatedir, template) unless template.start_with?(File::SEPARATOR)
294: 
295:       template = File.read(template)
296:       meta = @meta
297:       entities = @entities
298: 
299:       erb = ERB.new(template, 0, '%')
300:       erb.result(binding)
301:     end

Registers an input argument for a given action

See the documentation for action for how to use this

[Source]

     # File lib/mcollective/ddl.rb, line 183
183:     def input(argument, properties)
184:       raise "Cannot figure out what entity input #{argument} belongs to" unless @current_entity
185: 
186:       entity = @current_entity
187: 
188:       raise "The only valid input name for a data query is 'query'" if @plugintype == :data && argument != :query
189: 
190:       if @plugintype == :agent
191:         raise "Input needs a :optional property" unless properties.include?(:optional)
192:       end
193: 
194:       [:prompt, :description, :type].each do |arg|
195:         raise "Input needs a :#{arg} property" unless properties.include?(arg)
196:       end
197: 
198:       @entities[entity][:input][argument] = {:prompt => properties[:prompt],
199:                                              :description => properties[:description],
200:                                              :type => properties[:type],
201:                                              :optional => properties[:optional]}
202: 
203:       case properties[:type]
204:         when :string
205:           raise "Input type :string needs a :validation argument" unless properties.include?(:validation)
206:           raise "Input type :string needs a :maxlength argument" unless properties.include?(:maxlength)
207: 
208:           @entities[entity][:input][argument][:validation] = properties[:validation]
209:           @entities[entity][:input][argument][:maxlength] = properties[:maxlength]
210: 
211:         when :list
212:           raise "Input type :list needs a :list argument" unless properties.include?(:list)
213: 
214:           @entities[entity][:input][argument][:list] = properties[:list]
215:       end
216:     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.

[Source]

     # File lib/mcollective/ddl.rb, line 434
434:     def is_function?(method_name)
435:       PluginManager.find("aggregate").include?(method_name.to_s)
436:     end

[Source]

    # File lib/mcollective/ddl.rb, line 52
52:     def loadddlfile
53:       if ddlfile = findddlfile
54:         instance_eval(File.read(ddlfile), ddlfile, 1)
55:       else
56:         raise("Can't find DDL for #{@plugintype} plugin '#{@pluginname}'")
57:       end
58:     end

Registers meta data for the introspection hash

[Source]

    # File lib/mcollective/ddl.rb, line 76
76:     def metadata(meta)
77:       [:name, :description, :author, :license, :version, :url, :timeout].each do |arg|
78:         raise "Metadata needs a :#{arg} property" unless meta.include?(arg)
79:       end
80: 
81:       @meta = meta
82:     end

If the ddl‘s plugin type is ‘agent’ and the method name matches a aggregate function, we return the function with args as a hash.

[Source]

     # File lib/mcollective/ddl.rb, line 424
424:     def method_missing(name, *args, &block)
425:       super unless @process_aggregate_functions
426:       super unless is_function?(name)
427: 
428:       return {:function => name, :args => args}
429:     end

Registers an output argument for a given action

See the documentation for action for how to use this

[Source]

     # File lib/mcollective/ddl.rb, line 221
221:     def output(argument, properties)
222:       raise "Cannot figure out what action input #{argument} belongs to" unless @current_entity
223:       raise "Output #{argument} needs a description argument" unless properties.include?(:description)
224:       raise "Output #{argument} needs a display_as argument" unless properties.include?(:display_as)
225: 
226:       action = @current_entity
227: 
228:       @entities[action][:output][argument] = {:description => properties[:description],
229:                                               :display_as  => properties[:display_as],
230:                                               :default     => properties[:default]}
231:     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

[Source]

     # File lib/mcollective/ddl.rb, line 253
253:     def summarize(&block)
254:       unless @config.mode == :server
255:         @process_aggregate_functions = true
256:         block.call
257:         @process_aggregate_functions = nil
258:       end
259:     end

[Source]

     # File lib/mcollective/ddl.rb, line 275
275:     def template_for_plugintype
276:       case @plugintype
277:         when :agent
278:           return "rpc-help.erb"
279:         else
280:           return "#{@plugintype}-help.erb"
281:       end
282:     end

validate strings, lists and booleans, we‘ll add more types of validators when all the use cases are clear

only does validation for arguments actually given, since some might be optional. We validate the presense of the argument earlier so this is a safe assumption, just to skip them.

:string can have maxlength and regex. A maxlength of 0 will bypasss checks :list has a array of valid values

[Source]

     # File lib/mcollective/ddl.rb, line 335
335:     def validate_input_argument(input, key, argument)
336:       case input[key][:type]
337:         when :string
338:           raise DDLValidationError, "Input #{key} should be a string for plugin #{meta[:name]}" unless argument.is_a?(String)
339: 
340:           if input[key][:maxlength].to_i > 0
341:             if argument.size > input[key][:maxlength].to_i
342:               raise DDLValidationError, "Input #{key} is longer than #{input[key][:maxlength]} character(s) for plugin #{meta[:name]}"
343:             end
344:           end
345: 
346:           unless argument.match(Regexp.new(input[key][:validation]))
347:             raise DDLValidationError, "Input #{key} does not match validation regex #{input[key][:validation]} for plugin #{meta[:name]}"
348:           end
349: 
350:         when :list
351:           unless input[key][:list].include?(argument)
352:             raise DDLValidationError, "Input #{key} doesn't match list #{input[key][:list].join(', ')} for plugin #{meta[:name]}"
353:           end
354: 
355:         when :boolean
356:           unless [TrueClass, FalseClass].include?(argument.class)
357:             raise DDLValidationError, "Input #{key} should be a boolean for plugin #{meta[:name]}"
358:           end
359: 
360:         when :integer
361:           raise DDLValidationError, "Input #{key} should be a integer for plugin #{meta[:name]}" unless argument.is_a?(Fixnum)
362: 
363:         when :float
364:           raise DDLValidationError, "Input #{key} should be a floating point number for plugin #{meta[:name]}" unless argument.is_a?(Float)
365: 
366:         when :number
367:           raise DDLValidationError, "Input #{key} should be a number for plugin #{meta[:name]}" unless argument.is_a?(Numeric)
368:       end
369: 
370:       return true
371:     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.

[Source]

     # File lib/mcollective/ddl.rb, line 375
375:     def validate_rpc_request(action, arguments)
376:       raise "Can only validate RPC requests against Agent DDLs" unless @plugintype == :agent
377: 
378:       # is the action known?
379:       unless actions.include?(action)
380:         raise DDLValidationError, "Attempted to call action #{action} for #{@pluginname} but it's not declared in the DDL"
381:       end
382: 
383:       input = action_interface(action)[:input]
384: 
385:       input.keys.each do |key|
386:         unless input[key][:optional]
387:           unless arguments.keys.include?(key)
388:             raise DDLValidationError, "Action #{action} needs a #{key} argument"
389:           end
390:         end
391: 
392:         if arguments.keys.include?(key)
393:           validate_input_argument(input, key, arguments[key])
394:         end
395:       end
396: 
397:       true
398:     end

[Validate]