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
entities | [R] | |
meta | [R] |
# 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
# 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
# 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
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.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
records valid capabilities for discovery plugins
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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.
# 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