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] 

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:       @plugin = plugin
43:       @plugintype = plugintype.to_sym
44: 
45:       if loadddl
46:         if ddlfile = findddlfile(plugin, plugintype)
47:           instance_eval(File.read(ddlfile))
48:         else
49:           raise("Can't find DDL for #{plugintype} plugin '#{plugin}'")
50:         end
51:       end
52:     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 365
365:     def self.string_to_boolean(val)
366:       return true if ["true", "t", "yes", "y", "1"].include?(val.downcase)
367:       return false if ["false", "f", "no", "n", "0"].include?(val.downcase)
368: 
369:       raise "#{val} does not look like a boolean argument"
370:     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 375
375:     def self.string_to_number(val)
376:       return val.to_f if val =~ /^\d+\.\d+$/
377:       return val.to_i if val =~ /^\d+$/
378: 
379:       raise "#{val} does not look like a number"
380:     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 151
151:     def action(name, input, &block)
152:       raise "Action needs a :description property" unless input.include?(:description)
153: 
154:       unless @entities.include?(name)
155:         @entities[name] = {}
156:         @entities[name][:action] = name
157:         @entities[name][:input] = {}
158:         @entities[name][:output] = {}
159:         @entities[name][:display] = :failed
160:         @entities[name][:description] = input[:description]
161:       end
162: 
163:       # if a block is passed it might be creating input methods, call it
164:       # we set @current_entity so the input block can know what its talking
165:       # to, this is probably an epic hack, need to improve.
166:       @current_entity = name
167:       block.call if block_given?
168:       @current_entity = nil
169:     end

Returns the interface for a specific action

[Source]

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

Returns an array of actions this agent support

[Source]

     # File lib/mcollective/ddl.rb, line 264
264:     def actions
265:       raise "Only agent DDLs have actions" unless @plugintype == :agent
266:       @entities.keys
267:     end

records valid capabilities for discovery plugins

[Source]

     # File lib/mcollective/ddl.rb, line 118
118:     def capabilities(caps)
119:       raise "Only discovery DDLs have capabilities" unless @plugintype == :discovery
120: 
121:       caps = [caps].flatten
122: 
123:       raise "Discovery plugin capabilities can't be empty" if caps.empty?
124: 
125:       caps.each do |cap|
126:         if [:classes, :facts, :identity, :agents, :compound].include?(cap)
127:           @entities[:discovery][:capabilities] << cap
128:         else
129:           raise "%s is not a valid capability, valid capabilities are :classes, :facts, :identity, :agents and :compound" % cap
130:         end
131:       end
132:     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 89
 89:     def dataquery(input, &block)
 90:       raise "Data queries need a :description" unless input.include?(:description)
 91:       raise "Data queries can only have one definition" if @entities[:data]
 92: 
 93:       @entities[:data]  = {:description => input[:description],
 94:                            :input => {},
 95:                            :output => {}}
 96: 
 97:       @current_entity = :data
 98:       block.call if block_given?
 99:       @current_entity = nil
100:     end

Returns the interface for the data query

[Source]

     # File lib/mcollective/ddl.rb, line 270
270:     def dataquery_interface
271:       raise "Only data DDLs have data queries" unless @plugintype == :data
272:       @entities[:data] || {}
273:     end

Creates the definition for new discovery plugins

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

[Source]

     # File lib/mcollective/ddl.rb, line 107
107:     def discovery(&block)
108:       raise "Discovery plugins can only have one definition" if @entities[:discovery]
109: 
110:       @entities[:discovery] = {:capabilities => []}
111: 
112:       @current_entity = :discovery
113:       block.call if block_given?
114:       @current_entity = nil
115:     end

[Source]

     # File lib/mcollective/ddl.rb, line 281
281:     def discovery_interface
282:       raise "Only discovery DDLs have discovery interfaces" unless @plugintype == :discovery
283:       @entities[:discovery]
284:     end

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

[Source]

     # File lib/mcollective/ddl.rb, line 225
225:     def display(pref)
226:       # defaults to old behavior, complain if its supplied and invalid
227:       unless [:ok, :failed, :flatten, :always].include?(pref)
228:         raise "Display preference #{pref} is not valid, should be :ok, :failed, :flatten or :always"
229:       end
230: 
231:       action = @current_entity
232:       @entities[action][:display] = pref
233:     end

[Source]

    # File lib/mcollective/ddl.rb, line 54
54:     def findddlfile(ddlname, ddltype=:agent)
55:       @config.libdir.each do |libdir|
56:         ddlfile = File.join([libdir, "mcollective", ddltype.to_s, "#{ddlname}.ddl"])
57: 
58:         if File.exist?(ddlfile)
59:           Log.debug("Found #{ddlname} ddl at #{ddlfile}")
60:           return ddlfile
61:         end
62:       end
63:       return false
64:     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 251
251:     def help(template=nil)
252:       template = template_for_plugintype unless template
253:       template = File.join(@config.helptemplatedir, template) unless template.start_with?(File::SEPARATOR)
254: 
255:       template = File.read(template)
256:       meta = @meta
257:       entities = @entities
258: 
259:       erb = ERB.new(template, 0, '%')
260:       erb.result(binding)
261:     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 174
174:     def input(argument, properties)
175:       raise "Cannot figure out what entity input #{argument} belongs to" unless @current_entity
176: 
177:       entity = @current_entity
178: 
179:       raise "The only valid input name for a data query is 'query'" if @plugintype == :data && argument != :query
180: 
181:       if @plugintype == :agent
182:         raise "Input needs a :optional property" unless properties.include?(:optional)
183:       end
184: 
185:       [:prompt, :description, :type].each do |arg|
186:         raise "Input needs a :#{arg} property" unless properties.include?(arg)
187:       end
188: 
189:       @entities[entity][:input][argument] = {:prompt => properties[:prompt],
190:                                              :description => properties[:description],
191:                                              :type => properties[:type],
192:                                              :optional => properties[:optional]}
193: 
194:       case properties[:type]
195:         when :string
196:           raise "Input type :string needs a :validation argument" unless properties.include?(:validation)
197:           raise "Input type :string needs a :maxlength argument" unless properties.include?(:maxlength)
198: 
199:           @entities[entity][:input][argument][:validation] = properties[:validation]
200:           @entities[entity][:input][argument][:maxlength] = properties[:maxlength]
201: 
202:         when :list
203:           raise "Input type :list needs a :list argument" unless properties.include?(:list)
204: 
205:           @entities[entity][:input][argument][:list] = properties[:list]
206:       end
207:     end

Registers meta data for the introspection hash

[Source]

    # File lib/mcollective/ddl.rb, line 67
67:     def metadata(meta)
68:       [:name, :description, :author, :license, :version, :url, :timeout].each do |arg|
69:         raise "Metadata needs a :#{arg} property" unless meta.include?(arg)
70:       end
71: 
72:       @meta = meta
73:     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 212
212:     def output(argument, properties)
213:       raise "Cannot figure out what action input #{argument} belongs to" unless @current_entity
214:       raise "Output #{argument} needs a description argument" unless properties.include?(:description)
215:       raise "Output #{argument} needs a display_as argument" unless properties.include?(:display_as)
216: 
217:       action = @current_entity
218: 
219:       @entities[action][:output][argument] = {:description => properties[:description],
220:                                               :display_as  => properties[:display_as]}
221:     end

[Source]

     # File lib/mcollective/ddl.rb, line 235
235:     def template_for_plugintype
236:       case @plugintype
237:         when :agent
238:           return "rpc-help.erb"
239:         else
240:           return "#{@plugintype}-help.erb"
241:       end
242:     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 295
295:     def validate_input_argument(input, key, argument)
296:       case input[key][:type]
297:         when :string
298:           raise DDLValidationError, "Input #{key} should be a string for plugin #{meta[:name]}" unless argument.is_a?(String)
299: 
300:           if input[key][:maxlength].to_i > 0
301:             if argument.size > input[key][:maxlength].to_i
302:               raise DDLValidationError, "Input #{key} is longer than #{input[key][:maxlength]} character(s) for plugin #{meta[:name]}"
303:             end
304:           end
305: 
306:           unless argument.match(Regexp.new(input[key][:validation]))
307:             raise DDLValidationError, "Input #{key} does not match validation regex #{input[key][:validation]} for plugin #{meta[:name]}"
308:           end
309: 
310:         when :list
311:           unless input[key][:list].include?(argument)
312:             raise DDLValidationError, "Input #{key} doesn't match list #{input[key][:list].join(', ')} for plugin #{meta[:name]}"
313:           end
314: 
315:         when :boolean
316:           unless [TrueClass, FalseClass].include?(argument.class)
317:             raise DDLValidationError, "Input #{key} should be a boolean for plugin #{meta[:name]}"
318:           end
319: 
320:         when :integer
321:           raise DDLValidationError, "Input #{key} should be a integer for plugin #{meta[:name]}" unless argument.is_a?(Fixnum)
322: 
323:         when :float
324:           raise DDLValidationError, "Input #{key} should be a floating point number for plugin #{meta[:name]}" unless argument.is_a?(Float)
325: 
326:         when :number
327:           raise DDLValidationError, "Input #{key} should be a number for plugin #{meta[:name]}" unless argument.is_a?(Numeric)
328:       end
329: 
330:       return true
331:     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 335
335:     def validate_rpc_request(action, arguments)
336:       raise "Can only validate RPC requests against Agent DDLs" unless @plugintype == :agent
337: 
338:       # is the action known?
339:       unless actions.include?(action)
340:         raise DDLValidationError, "Attempted to call action #{action} for #{@plugin} but it's not declared in the DDL"
341:       end
342: 
343:       input = action_interface(action)[:input]
344: 
345:       input.keys.each do |key|
346:         unless input[key][:optional]
347:           unless arguments.keys.include?(key)
348:             raise DDLValidationError, "Action #{action} needs a #{key} argument"
349:           end
350:         end
351: 
352:         if arguments.keys.include?(key)
353:           validate_input_argument(input, key, arguments[key])
354:         end
355:       end
356: 
357:       true
358:     end

[Validate]