Adds a new resource type to puppet for web requests and implements a provider of that type using the ruby curl interface provided by the 'curb' rubygem
This is the second revision of the patch I had previous sent out, including put and delete methods, better session security and handling, and many more tests Signed-off-by: Mo Morsi <[email protected]> --- Local-branch: feature/master/7474 lib/puppet/external/curl.rb | 77 ++++++++++++++++++++++ lib/puppet/provider/web/curl.rb | 100 ++++++++++++++++++++++++++++ lib/puppet/type/web.rb | 110 +++++++++++++++++++++++++++++++ spec/unit/provider/web/curl_spec.rb | 122 +++++++++++++++++++++++++++++++++++ spec/unit/type/web_spec.rb | 90 ++++++++++++++++++++++++++ 5 files changed, 499 insertions(+), 0 deletions(-) create mode 100644 lib/puppet/external/curl.rb create mode 100644 lib/puppet/provider/web/curl.rb create mode 100644 lib/puppet/type/web.rb create mode 100644 spec/unit/provider/web/curl_spec.rb create mode 100644 spec/unit/type/web_spec.rb diff --git a/lib/puppet/external/curl.rb b/lib/puppet/external/curl.rb new file mode 100644 index 0000000..ecffb5e --- /dev/null +++ b/lib/puppet/external/curl.rb @@ -0,0 +1,77 @@ +# Provides an interface to curl using the curb gem for puppet +require 'curb' + +# uses nokogiri to verify responses w/ xpath +require 'nokogiri' + +class Curl::Easy + + # Format request parameters for the specified request method + def self.format_params(method, params) + if([:get, :delete].include?(method)) + return params.collect { |k,v| "#{k}=#{v}" }.join("&") unless params.nil? + return "" + end + # post, put: + cparams = [] + params.each_pair { |k,v| cparams << Curl::PostField.content(k,v) } unless params.nil? + return cparams + end + + # Format a url for the specified request method, base uri, and parameters + def self.format_url(method, uri, params) + if([:get, :delete].include?(method)) + url = uri + url += ";" + format_params(method, params) + return url + end + # post, put: + return uri + end + + # Invoke a new curl request and return result + def self.web_request(method, uri, request_params, params = {}) + raise Puppet::Error, "Must specify http method (#{method}) and uri (#{uri})" if method.nil? || uri.nil? + + curl = self.new + + if params.has_key?(:cookie) && !params[:cookie].nil? + curl.enable_cookies = true + curl.cookiefile = params[:cookie] + curl.cookiejar = params[:cookie] + end + + curl.follow_location = (params.has_key?(:follow) && params[:follow]) + + case(method) + when 'get' + curl.url = format_url(method, uri, request_params) + curl.http_get + return curl + + when 'post' + curl.url = format_url(method, uri, request_params) + curl.http_post(format_params(method, request_params)) + return curl + + when 'put' + curl.url = format_url(method, uri, request_params) + curl.http_put(format_params(method, request_params)) + return curl + + when 'delete' + curl.url = format_url(method, uri, request_params) + curl.http_delete + return curl + end + end + + def valid_status_code?(valid_values=[]) + valid_values.include?(response_code.to_s) + end + + def valid_xpath?(xpath="/") + !Nokogiri::HTML(body_str.to_s).xpath(xpath.to_s).empty? + end + +end diff --git a/lib/puppet/provider/web/curl.rb b/lib/puppet/provider/web/curl.rb new file mode 100644 index 0000000..5f37742 --- /dev/null +++ b/lib/puppet/provider/web/curl.rb @@ -0,0 +1,100 @@ +require 'fileutils' +require 'puppet/external/curl' + +# Puppet provider definition +Puppet::Type.type(:web).provide :curl do + desc "Use curl to access web resources" + + def get + @uri + end + + def post + @uri + end + + def delete + @uri + end + + def put + @uri + end + + def get=(uri) + @uri = uri + process_params('get', @resource, uri) + end + + def post=(uri) + @uri = uri + process_params('post', @resource, uri) + end + + def delete=(uri) + @uri = uri + process_params('delete', @resource, uri) + end + + def put=(uri) + @uri = uri + process_params('put', @resource, uri) + end + + private + + # Helper to process/parse web parameters + def process_params(request_method, params, uri) + begin + cookies = nil + if params[:store_cookies_at] + FileUtils.touch(params[:store_cookies_at]) if !File.exist?(params[:store_cookies_at]) + cookies = params[:store_cookies_at] + elsif params[:use_cookies_at] + cookies = params[:use_cookies_at] + end + + # Actually run the request and verify the result + result = Curl::Easy::web_request(request_method, uri, params[:parameters], + :cookie => cookies, + :follow => params[:follow]) + verify_result(result, + :returns => params[:returns], + :does_not_return => params[:does_not_return], + :contains => params[:contains], + :does_not_contain => params[:does_not_contain] ) + result.close + + rescue Exception => e + raise Puppet::Error, "An exception was raised when invoking web request: #{e}" + + ensure + FileUtils.rm_f(cookies) if params[:remove_cookies] + end + end + + # Helper to verify the response + def verify_result(result, verify = {}) + if !verify[:returns].nil? && + !result.valid_status_code?(verify[:returns]) + raise Puppet::Error, "Invalid HTTP Return Code: #{result.response_code}, + was expecting one of #{verify[:returns].join(", ")}" + end + + if !verify[:does_not_return].nil? && + result.valid_status_code?(verify[:does_not_return]) + raise Puppet::Error, "Invalid HTTP Return Code: #{result.response_code}, + was not expecting one of #{verify[:does_not_return].join(", ")}" + end + + if !verify[:contains].nil? && + !result.valid_xpath?(verify[:contains]) + raise Puppet::Error, "Expecting #{verify[:contains]} in the result" + end + + if !verify[:does_not_contain].nil? && + result.valid_xpath?(verify[:does_not_contain]) + raise Puppet::Error, "Not expecting #{verify[:does_not_contain]} in the result" + end + end +end diff --git a/lib/puppet/type/web.rb b/lib/puppet/type/web.rb new file mode 100644 index 0000000..c59fece --- /dev/null +++ b/lib/puppet/type/web.rb @@ -0,0 +1,110 @@ +require 'uri' + +# A puppet resource type used to access resources on the World Wide Web +Puppet::Type.newtype(:web) do + @doc = "Issue a request to a resource on the world wide web" + + private + + # Validates uris passed in + def self.validate_uri(url) + begin + uri = URI.parse(url) + raise ArgumentError, "Specified uri #{url} is not valid" if ![URI::HTTP, URI::HTTPS].include?(uri.class) + rescue URI::InvalidURIError + raise ArgumentError, "Specified uri #{url} is not valid" + end + end + + # Validates http statuses passed in + def self.validate_http_status(status) + status = [status] unless status.is_a?(Array) + status.each { |stat| + stat = stat.to_s + unless ['100', '101', '102', '122', + '200', '201', '202', '203', '204', '205', '206', '207', '226', + '300', '301', '302', '303', '304', '305', '306', '307', + '400', '401', '402', '403', '404', '405', '406', '407', '408', '409', + '410', '411', '412', '413', '414', '415', '416', '417', '418', + '422', '423', '424', '425', '426', '444', '449', '450', '499', + '500', '501', '502', '503', '504', '505', '506', '507', '508', ' 509', '510' + ].include?(stat) + raise ArgumentError, "Invalid http status code #{stat} specified" + end + } + end + + # Convert singular params into arrays of strings + def self.munge_array_params(value) + value = [value] unless value.is_a?(Array) + value = value.collect { |val| val.to_s } + value + end + + newparam :name + + newproperty(:get) do + desc "Issue get request to the specified uri" + validate do |value| Puppet::Type::Web.validate_uri(value) end + end + + newproperty(:post) do + desc "Issue post request to the specified uri" + validate do |value| Puppet::Type::Web.validate_uri(value) end + end + + newproperty(:delete) do + desc "Issue delete request to the specified uri" + validate do |value| Puppet::Type::Web.validate_uri(value) end + end + + newproperty(:put) do + desc "Issue put request to the specified uri" + validate do |value| Puppet::Type::Web.validate_uri(value) end + end + + newparam(:parameters) do + desc "Hash of parameters to include in the web request" + end + + newparam(:follow) do + desc "Boolean indicating if redirects should be followed" + newvalues(:true, :false) + end + + newparam(:store_cookies_at) do + desc "String indicating where session cookies should be stored" + end + + newparam(:use_cookies_at) do + desc "String indicating where session cookies should be read from" + end + + newparam(:remove_cookies) do + desc "Boolean indicating if cookies should be removed after using them" + newvalues(:true, :false) + end + + newparam(:returns) do + desc "Expected http return codes of the request" + defaultto ["200"] + validate do |value| Puppet::Type::Web.validate_http_status(value) end + munge do |value| Puppet::Type::Web.munge_array_params(value) end + end + + newparam(:does_not_return) do + desc "Unexecpected http return codes of the request" + validate do |value| Puppet::Type::Web.validate_http_status(value) end + munge do |value| Puppet::Type::Web.munge_array_params(value) end + end + + newparam(:contains) do + desc "XPath to verify as part of the result" + munge do |value| Puppet::Type::Web.munge_array_params(value) end + end + + newparam(:does_not_contain) do + desc "XPath to verify as not being part of the result" + munge do |value| Puppet::Type::Web.munge_array_params(value) end + end +end diff --git a/spec/unit/provider/web/curl_spec.rb b/spec/unit/provider/web/curl_spec.rb new file mode 100644 index 0000000..36f1ddf --- /dev/null +++ b/spec/unit/provider/web/curl_spec.rb @@ -0,0 +1,122 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +provider_class = Puppet::Type.type(:web).provider(:curl) + +describe provider_class do + before :each do + @resource = Puppet::Resource.new(:web, 'foo') + @resource.stubs(:[]).returns(nil) + @provider = provider_class.new(@resource) + end + + def http_request(http_method, url) + @provider.method("#{http_method}=".to_sym).call url + end + + ['get', 'post', 'put', 'delete'].each do |http_method| + describe "##{http_method}" do + it "should issue #{http_method} request to uri" do + proc { + http_request http_method, "http://www.puppetlabs.com" + }.should_not raise_error(Puppet::Error) + end + + it "should accept parameters for #{http_method} request to uri" do + proc { + @resource.stubs(:[]).with(:parameters).returns({:q => 'puppet' }) + @resource.stubs(:[]).with(:follow).returns(true) + http_request http_method, "http://www.google.com" + }.should_not raise_error(Puppet::Error) + end + + it "should verify default success return http status code for #{http_method} request to uri" do + proc { + http_request http_method, "http://foobar" + }.should raise_error(Puppet::Error) + + proc { + @resource.stubs(:[]).with(:follow).returns(true) + http_request http_method, "http://google.com" + }.should_not raise_error(Puppet::Error) + end + + it "should verify return http status code for #{http_method} request to uri" do + proc { + expected_return_code = '500' + @resource.stubs(:[]).with(:follow).returns(false) + @resource.stubs(:[]).with(:returns).returns([expected_return_code]) + http_request http_method, "http://google.com" + }.should raise_error(Puppet::Error) + + proc { + expected_return_code = http_method == "get" ? '301' : '405' + @resource.stubs(:[]).with(:follow).returns(false) + @resource.stubs(:[]).with(:returns).returns([expected_return_code]) + http_request http_method, "http://google.com" + }.should_not raise_error(Puppet::Error) + end + + it "should verify does_not_return http status code for #{http_method} request to uri" do + proc { + return_code_not_expected = http_method == "get" ? '301' : '405' + @resource.stubs(:[]).with(:does_not_return).returns(return_code_not_expected) + http_request http_method, "http://google.com" + }.should raise_error(Puppet::Error) + + proc { + return_code_not_expected = "500" + @resource.stubs(:[]).with(:follow).returns(true) + @resource.stubs(:[]).with(:does_not_return).returns(return_code_not_expected) + http_request http_method, "http://google.com" + }.should_not raise_error(Puppet::Error) + end + + it "should verify result contains specified xpath for #{http_method} request to uri" do + proc { + @resource.stubs(:[]).with(:contains).returns('/html/body') + @resource.stubs(:[]).with(:follow).returns(true) + http_request http_method, "http://www.puppetlabs.com" + }.should_not raise_error(Puppet::Error) + + proc { + @resource.stubs(:[]).with(:contains).returns('/html/head/body') + @resource.stubs(:[]).with(:follow).returns(true) + http_request http_method, "http://www.puppetlabs.com" + }.should raise_error(Puppet::Error) + end + + it "should verify result does_not_contain specified xpath for #{http_method} request to uri" do + proc { + @resource.stubs(:[]).with(:does_not_contain).returns('/html/head/body') + @resource.stubs(:[]).with(:follow).returns(true) + http_request http_method, "http://www.puppetlabs.com" + }.should_not raise_error(Puppet::Error) + + proc { + @resource.stubs(:[]).with(:does_not_contain).returns('/html/body') + @resource.stubs(:[]).with(:follow).returns(true) + http_request http_method, "http://www.puppetlabs.com" + }.should raise_error(Puppet::Error) + end + + it "should verify cookies are stored for a #{http_method} request to uri" do + @resource.stubs(:[]).with(:store_cookies_at).returns("/tmp/#{http_method}.cookie") + http_request http_method, "http://amazon.com" + File.exist?("/tmp/#{http_method}.cookie").should be_true + FileUtils.rm_f "/tmp/#{http_method}.cookie" + end + + #it "should verify cookies are used when for a #{http_method} request to uri" do + # how ? + #end + + it "should verify cookies are removed for a #{http_method} request to uri" do + @resource.stubs(:[]).with(:store_cookies_at).returns("/tmp/#{http_method}.cookie") + @resource.stubs(:[]).with(:remove_cookies).returns(true) + http_request http_method, "http://amazon.com" + File.exist?("/tmp/#{http_method}.cookie").should be_false + end + end + end +end diff --git a/spec/unit/type/web_spec.rb b/spec/unit/type/web_spec.rb new file mode 100644 index 0000000..e170681 --- /dev/null +++ b/spec/unit/type/web_spec.rb @@ -0,0 +1,90 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +host = Puppet::Type.type(:web) + +describe Puppet::Type.type(:web) do + before do + @class = host + end + + it "should have :name be its namevar" do + @class.key_attributes.should == [:name] + end + + describe "when validating attributes" do + [:parameters, :follow, :store_cookies_at, :use_cookies_at, :remove_cookies, + :returns, :does_not_return, :contains, :does_not_contain].each do |param| + it "should have a #{param} parameter" do + @class.attrtype(param).should == :param + end + end + + [:get, :post, :put, :delete ].each do |property| + it "should have a #{property} property" do + @class.attrtype(property).should == :property + end + end + end + + describe "when validating values" do + it "should validate uris" do + proc { Puppet::Type::Web.validate_uri("http://google.com") }.should_not raise_error + proc { Puppet::Type::Web.validate_uri("foobar123") }.should raise_error(ArgumentError) end + + it "should validate http_status" do + proc { Puppet::Type::Web.validate_http_status("200") }.should_not raise_error + proc { Puppet::Type::Web.validate_http_status(["200", "400"]) }.should_not raise_error + proc { Puppet::Type::Web.validate_http_status("909") }.should raise_error(ArgumentError) + proc { Puppet::Type::Web.validate_http_status(["200", "909"]) }.should raise_error(ArgumentError) + end + + it "should munge array parameters" do + Puppet::Type::Web.munge_array_params(200).should == ["200"] + Puppet::Type::Web.munge_array_params([200]).should == ["200"] + Puppet::Type::Web.munge_array_params(["200"]).should == ["200"] + end + + [:get, :post, :put, :delete ].each do |property| + it "should require a valid uri for #{property} requests" do + proc { @class.new(:name => "#{property}_uri", property.to_sym => "") }.should raise_error(Puppet::Error,"Parameter #{property} failed: Specified uri is not valid") + end + + it "should require a valid returns value for #{property} requests if specified" do + proc { @class.new(:name => "#{property}_uri", property.to_sym => "http://www.puppetlabs.com", :returns => "invalid" ) }.should raise_error(Puppet::Error,"Parameter returns failed: Invalid http status code invalid specified") + end + + it "should munge the returns value for #{property} requests if specified" do + type = @class.new(:name => "#{property}_uri", property.to_sym => "http://www.puppetlabs.com", :returns => 200 ) + type.parameters[:returns].value.should == ['200'] + end + + it "should require a valid does_not_return value for #{property} requests if specified" do + proc { @class.new(:name => "#{property}_uri", property.to_sym => "http://www.puppetlabs.com", :does_not_return => "invalid" ) }.should raise_error(Puppet::Error,"Parameter does_not_return failed: Invalid http status code invalid specified") + end + + it "should munge the does_not_return value for #{property} requests if specified" do + type = @class.new(:name => "#{property}_uri", property.to_sym => "http://www.puppetlabs.com", :does_not_return => 200 ) + type.parameters[:does_not_return].value.should == ['200'] + end + + it "should require a valid follow value for #{property} requests" do + proc { @class.new(:name => "#{property}_uri", property.to_sym => "http://www.puppetlabs.com", :follow => "invalid" ) }.should raise_error(Puppet::Error,'Parameter follow failed: Invalid value "invalid". Valid values are true, false. ') + end + + it "should munge the contains value for #{property} requests if specified" do + type = @class.new(:name => "#{property}_uri", property.to_sym => "http://www.puppetlabs.com", :contains => '/foobar' ) + type.parameters[:contains].value.should == ['/foobar'] + end + + it "should munge the does_not_contains value for #{property} requests if specified" do + type = @class.new(:name => "#{property}_uri", property.to_sym => "http://www.puppetlabs.com", :does_not_contain => '/foobar' ) + type.parameters[:does_not_contain].value.should == ['/foobar'] + end + + it "should require a valid remove_cookies value for #{property} requests" do + proc { @class.new(:name => "#{property}_uri", property.to_sym => "http://www.puppetlabs.com", :remove_cookies => 5 ) }.should raise_error(Puppet::Error,'Parameter remove_cookies failed: Invalid value 5. Valid values are true, false. ') + end + end + end +end -- 1.7.5.4 -- You received this message because you are subscribed to the Google Groups "Puppet Developers" group. To post to this group, send email to [email protected]. To unsubscribe from this group, send email to [email protected]. For more options, visit this group at http://groups.google.com/group/puppet-dev?hl=en.
