Permalink
Browse files

15850 - add required mco version to metadata

The DDL can now have a section like:

  requires :mcollective => "2.1.1"

Which will prevent this DDL file from loading on any machine with
mcollective younger than 2.1.1.  This will prevent agents from
being loaded on older mcollectived where they might rely on newer
features.

This will be verified on both the client and server and the client
now requires agent DDLs before you can interact with any agents

To get this done we included the versioncmp function from Puppet after a
rewrite to be more ruby like
  • Loading branch information...
1 parent 1d844ee commit 9196a3a7c18a0c2272432a07eec9f5407488cd32 @ripienaar ripienaar committed Aug 9, 2012
@@ -255,14 +255,11 @@ def application_failure(e, err_dest=STDERR)
raise(e)
end
+ err_dest.puts "\nThe %s application failed to run, use -v for full error details: %s\n" % [ Util.colorize(:bold, $0), Util.colorize(:red, e.to_s)]
+
if options.nil? || options[:verbose]
e.backtrace.first << Util.colorize(:red, " <----")
err_dest.puts "\n%s %s" % [ Util.colorize(:red, e.to_s), Util.colorize(:bold, "(#{e.class.to_s})")]
- else
- err_dest.puts "\nThe %s application failed to run, use -v for full error details: %s\n" % [ Util.colorize(:bold, $0), Util.colorize(:red, e.to_s)]
- end
-
- if options.nil? || options[:verbose]
e.backtrace.each{|l| err_dest.puts "\tfrom #{l}"}
end
@@ -352,6 +349,7 @@ def halt(stats)
# cli options wouldnt take effect which could have a disasterous outcome
def rpcclient(agent, flags = {})
flags[:options] = options unless flags.include?(:options)
+ flags[:exit_on_failure] = false
super
end
@@ -132,8 +132,9 @@ def action(name, input, &block)
# 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
def method_missing(name, *args, &block)
- super unless @process_aggregate_functions
- super unless is_function?(name)
+ unless @process_aggregate_functions || is_function?(name)
+ raise NoMethodError, "undefined local variable or method `#{name}'", caller
+ end
return {:function => name, :args => args}
end
@@ -153,7 +154,7 @@ def validate_rpc_request(action, arguments)
raise DDLValidationError, "Attempted to call action #{action} for #{@pluginname} but it's not declared in the DDL"
end
- input = action_interface(action)[:input]
+ input = action_interface(action)[:input] || {}
input.keys.each do |key|
unless input[key][:optional]
@@ -17,7 +17,7 @@ module DDL
# plugin DDL then add a PlugintypeDDL class here and add your
# specific behaviors to those.
class Base
- attr_reader :meta, :entities, :pluginname, :plugintype, :usage
+ attr_reader :meta, :entities, :pluginname, :plugintype, :usage, :requirements
def initialize(plugin, plugintype=:agent, loadddl=true)
@entities = {}
@@ -26,6 +26,7 @@ def initialize(plugin, plugintype=:agent, loadddl=true)
@config = Config.instance
@pluginname = plugin
@plugintype = plugintype.to_sym
+ @requirements = {}
loadddlfile if loadddl
end
@@ -96,6 +97,21 @@ def findddlfile(ddlname=nil, ddltype=nil)
return false
end
+ def validate_requirements
+ if requirement = @requirements[:mcollective]
+ if Util.mcollective_version == "@DEVELOPMENT_VERSION@"
+ Log.warn("DDL requirements validation being skipped in development")
+ return true
+ end
+
+ if Util.versioncmp(Util.mcollective_version, requirement) < 0
+ raise DDLValidationError, "%s plugin '%s' requires MCollective version %s or newer" % [@plugintype.to_s.capitalize, @pluginname, requirement]
+ end
+ end
+
+ true
+ end
+
# validate strings, lists and booleans, we'll add more types of validators when
# all the use cases are clear
#
@@ -175,6 +191,21 @@ def output(argument, properties)
:default => properties[:default]}
end
+ def requires(requirement)
+ raise "Requirement should be a hash in the form :item => 'requirement'" unless requirement.is_a?(Hash)
+
+ valid_requirements = [:mcollective]
+
+ requirement.keys.each do |key|
+ unless valid_requirements.include?(key)
+ raise "Requirement %s is not a valid requirement, only %s is supported" % [key, valid_requirements.join(", ")]
+ end
+
+ @requirements[key] = requirement[key]
+ end
+
+ validate_requirements
+ end
# Registers meta data for the introspection hash
def metadata(meta)
@@ -43,7 +43,7 @@ def initialize(agent, flags = {})
@agent = agent
@timeout = initial_options[:timeout] || 5
@verbose = initial_options[:verbose]
- @filter = initial_options[:filter]
+ @filter = initial_options[:filter] || Util.empty_filter
@config = initial_options[:config]
@discovered_agents = nil
@progress = initial_options[:progress_bar]
@@ -84,14 +84,11 @@ def initialize(agent, flags = {})
# We do this only if the timeout is the default 5
# seconds, so that users cli overrides will still
# get applied
- begin
- @ddl = DDL.new(agent)
- @stats.ddl = @ddl
- @timeout = @ddl.meta[:timeout] + @discovery_timeout if @timeout == 5
- rescue Exception => e
- Log.debug("Could not find DDL: #{e}")
- @ddl = nil
- end
+ #
+ # DDLs are required, failure to find a DDL is fatal
+ @ddl = DDL.new(agent)
+ @stats.ddl = @ddl
+ @timeout = @ddl.meta[:timeout] + @discovery_timeout if @timeout == 5
# allows stderr and stdout to be overridden for testing
# but also for web apps that might not want a bunch of stuff
@@ -118,11 +115,7 @@ def disconnect
# Returns help for an agent if a DDL was found
def help(template)
- if @ddl
- @ddl.help(template)
- else
- return "Can't find DDL for agent '#{@agent}'"
- end
+ @ddl.help(template)
end
# Creates a suitable request hash for the SimpleRPC agent.
View
@@ -279,6 +279,10 @@ def self.ruby_version
RUBY_VERSION
end
+ def self.mcollective_version
+ MCollective::VERSION
+ end
+
# Returns an aligned_string of text relative to the size of the terminal
# window. If a line in the string exceeds the width of the terminal window
# the line will be chopped off at the whitespace chacter closest to the
@@ -401,5 +405,41 @@ def self.command_in_path?(command)
found.include?(true)
end
+
+ # compare two software versions as commonly found in
+ # package versions.
+ #
+ # returns 0 if a == b
+ # returns -1 if a < b
+ # returns 1 if a > b
+ #
+ # Code originally from Puppet but refactored to a more
+ # ruby style that fits in better with this code base
+ def self.versioncmp(version_a, version_b)
+ vre = /[-.]|\d+|[^-.\d]+/
+ ax = version_a.scan(vre)
+ bx = version_b.scan(vre)
+
+ until ax.empty? || bx.empty?
+ a = ax.shift
+ b = bx.shift
+
+ next if a == b
+ next if a == '-' && b == '-'
+ return -1 if a == '-'
+ return 1 if b == '-'
+ next if a == '.' && b == '.'
+ return -1 if a == '.'
+ return 1 if b == '.'
+
+ if a =~ /^[^0]\d+$/ && b =~ /^[^0]\d+$/
+ return Integer(a) <=> Integer(b)
+ else
+ return a.upcase <=> b.upcase
+ end
+ end
+
+ version_a <=> version_b
+ end
end
end
@@ -41,8 +41,11 @@ class Application::Completion<MCollective::Application
def list_agents
if options[:verbose]
PluginManager.find(:agent, "ddl").each do |agent|
- ddl = DDL.new(agent)
- puts "%s:%s" % [ agent, ddl.meta[:description] ]
+ begin
+ ddl = DDL.new(agent)
+ puts "%s:%s" % [ agent, ddl.meta[:description] ]
+ rescue
+ end
end
else
PluginManager.find(:agent, "ddl").each {|p| puts p}
@@ -15,7 +15,10 @@ def main
puts
Applications.list.sort.each do |app|
- puts " %-15s %s" % [app, Applications[app].application_description]
+ begin
+ puts " %-15s %s" % [app, Applications[app].application_description]
+ rescue
+ end
end
puts
@@ -238,17 +238,31 @@ def doc_command
puts "Please specify a plugin. Available plugins are:"
puts
+ load_errors = []
+
known_plugin_types.each do |plugin_type|
puts "%s:" % plugin_type[0]
PluginManager.find(plugin_type[1], "ddl").each do |ddl|
- help = DDL.new(ddl, plugin_type[1])
- pluginname = ddl.gsub(/_#{plugin_type[1]}$/, "")
- puts " %-25s %s" % [pluginname, help.meta[:description]]
+ begin
+ help = DDL.new(ddl, plugin_type[1])
+ pluginname = ddl.gsub(/_#{plugin_type[1]}$/, "")
+ puts " %-25s %s" % [pluginname, help.meta[:description]]
+ rescue => e
+ load_errors << [plugin_type[1], ddl, e]
+ end
end
puts
end
+
+ unless load_errors.empty?
+ puts "Plugin Load Errors:"
+
+ load_errors.each do |e|
+ puts " %-25s %s" % ["#{e[0]}/#{e[1]}", Util.colorize(:yellow, e[2])]
+ end
+ end
end
end
@@ -574,6 +574,7 @@ module MCollective
e.stubs(:to_s).returns("rspec")
@app.expects(:options).returns({:verbose => true}).twice
+ out.expects(:puts).with(regexp_matches(/ application failed to run, use -v for full error details/))
out.expects(:puts).with(regexp_matches(/from rspec <---/))
out.expects(:puts).with(regexp_matches(/rspec.+Mocha::Mock/))
@@ -92,7 +92,6 @@ module DDL
it "should return the function hash" do
Config.instance.mode = :client
- @ddl.expects(:is_function?).returns(true)
@ddl.instance_variable_set(:@process_aggregate_functions, true)
result = @ddl.method_missing(:test_function, :rspec)
result.should == {:args => [:rspec], :function => :test_function }
View
@@ -174,6 +174,42 @@ module DDL
end
end
+ describe "#requires" do
+ it "should only accept hashes as arguments" do
+ expect { @ddl.requires(1) }.to raise_error(/should be a hash/)
+ end
+
+ it "should only accept valid requirement types" do
+ expect { @ddl.requires(:rspec => "1") }.to raise_error(/is not a valid requirement/)
+ @ddl.requires(:mcollective => "1.0.0")
+ end
+
+ it "should save the requirement" do
+ @ddl.requires(:mcollective => "1.0.0")
+
+ @ddl.requirements.should == {:mcollective => "1.0.0"}
+ end
+ end
+
+ describe "#validate_requirements" do
+ it "should fail for older versions of mcollective" do
+ Util.stubs(:mcollective_version).returns("0.1")
+ expect { @ddl.requires(:mcollective => "2.0") }.to raise_error(/requires.+version 2.0/)
+ end
+
+ it "should pass for newer versions of mcollective" do
+ Util.stubs(:mcollective_version).returns("2.0")
+ @ddl.requires(:mcollective => "0.1")
+ @ddl.validate_requirements.should == true
+ end
+
+ it "should bypass checks in development" do
+ Util.stubs(:mcollective_version).returns("@DEVELOPMENT_VERSION@")
+ Log.expects(:warn).with(regexp_matches(/skipped in development/))
+ @ddl.requires(:mcollective => "0.1")
+ end
+ end
+
describe "#loaddlfile" do
it "should raise the correct error when a ddl isnt present" do
@ddl.expects(:findddlfile).returns(false)
@@ -8,15 +8,18 @@ module RPC
before do
@coreclient = mock
@discoverer = mock
- ddl = stub
+
+ ddl = DDL.new("foo", "agent", false)
+ ddl.action("rspec", :description => "mock agent")
ddl.stubs(:meta).returns({:timeout => 2})
+ DDL.stubs(:new).returns(ddl)
@discoverer.stubs(:force_direct_mode?).returns(false)
- @discoverer.stubs(:ddl).returns(ddl)
@discoverer.stubs(:discovery_method).returns("mc")
@discoverer.stubs(:force_discovery_method_by_filter).returns(false)
@discoverer.stubs(:discovery_timeout).returns(2)
+ @discoverer.stubs(:ddl).returns(ddl)
@coreclient.stubs("options=")
@coreclient.stubs(:collective).returns("mcollective")
@@ -35,6 +38,20 @@ module RPC
@client.stubs(:ddl).returns(ddl)
end
+ describe "#initialize" do
+ it "should fail for missing DDLs" do
+ DDL.stubs(:new).raises("DDL failure")
+ expect { Client.new("foo", {:options => {:config => "/nonexisting"}}) }.to raise_error("DDL failure")
+ end
+
+ it "should set a empty filter when none is supplied" do
+ filter = Util.empty_filter
+ Util.expects(:empty_filter).once.returns(filter)
+
+ Client.new("foo", :options => {:config => "/nonexisting"})
+ end
+ end
+
describe "#process_results_with_block" do
it "should inform the stats object correctly for passed requests" do
response = {:senderid => "rspec", :body => {:statuscode => 0}}
@@ -265,7 +282,7 @@ module RPC
client.stubs(:call_agent)
Stats.any_instance.expects(:reset).once
- client.foo
+ client.rspec
end
it "should validate the request against the ddl" do
@@ -285,9 +302,9 @@ module RPC
client.limit_targets = 10
client.expects(:pick_nodes_from_discovered).with(10).returns(["one", "two"])
- client.expects(:custom_request).with("foo", {}, ["one", "two"], {"identity" => /^(one|two)$/}).once
+ client.expects(:custom_request).with("rspec", {}, ["one", "two"], {"identity" => /^(one|two)$/}).once
- client.foo
+ client.rspec
end
describe "batch mode" do
@@ -337,9 +354,9 @@ module RPC
it "should support normal calls" do
client = Client.new("foo", {:options => {:filter => Util.empty_filter, :config => "/nonexisting"}})
- client.expects(:call_agent).with("foo", {}, client.options, :auto).once
+ client.expects(:call_agent).with("rspec", {}, client.options, :auto).once
- client.foo
+ client.rspec
end
end
Oops, something went wrong.

0 comments on commit 9196a3a

Please sign in to comment.