This is the third revision of the patch I had previous sent out,
including support for multipart form posts and conditional request
invokation depending on 'if' / 'unless' params
Signed-off-by: Mo Morsi <mmo...@redhat.com>
---
Local-branch: feature/master/7474
lib/puppet/external/curl.rb | 82 ++++++++++++++++++++++
lib/puppet/provider/web/curl.rb | 132 +++++++++++++++++++++++++++++++++++
lib/puppet/type/web.rb | 123 ++++++++++++++++++++++++++++++++
spec/unit/provider/web/curl_spec.rb | 122 ++++++++++++++++++++++++++++++++
spec/unit/type/web_spec.rb | 90 ++++++++++++++++++++++++
5 files changed, 549 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..45767e4
--- /dev/null
+++ b/lib/puppet/external/curl.rb
@@ -0,0 +1,82 @@
+# 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, file_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?
+ file_params.each_pair { |k,v| cparams << Curl::PostField.file(k,v) } unless file_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, 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])
+ request_params = params[:parameters]
+ file_params = params[:file_parameters]
+
+ 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.multipart_form_post = true if !file_params.nil? && file_params.size > 0
+ curl.http_post(*format_params(method, request_params, file_params))
+ return curl
+
+ when 'put'
+ curl.url = format_url(method, uri, request_params)
+ curl.multipart_form_post = true if !file_params.nil? && file_params.size > 0
+ curl.http_put(format_params(method, request_params, file_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..eec0858
--- /dev/null
+++ b/lib/puppet/provider/web/curl.rb
@@ -0,0 +1,132 @@
+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
+
+ # verify that we should actually run the request
+ return if skip_request?(params, cookies)
+
+ # Actually run the request and verify the result
+ result = Curl::Easy::web_request(request_method, uri,
+ :parameters => params[:parameters],
+ :file_parameters => params[:file_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 determine if we should skip the request
+ def skip_request?(params, cookie = nil)
+ [:if, :unless].each { |c|
+ condition = params[c]
+ unless condition.nil?
+ method = (condition.keys & ['get', 'post', 'delete', 'put']).first
+ result = Curl::Easy::web_request(method, condition[method],
+ :parameters => condition['parameters'],
+ :file_parameters => condition['file_parameters'],
+ :cookie => cookie, :follow => condition[:follow])
+ result_succeeded = true
+ begin
+ verify_result(result, condition)
+ rescue Puppet::Error
+ result_succeeded = false
+ end
+ return true if (c == :if && !result_succeeded) || (c == :unless && result_succeeded)
+ end
+ }
+ return false
+ end
+
+ # Helper to verify the response
+ def verify_result(result, verify = {})
+ verify[:returns] = verify['returns'] if verify[:returns].nil? && !verify['returns'].nil?
+ verify[:does_not_return] = verify['does_not_return'] if verify[:does_not_return].nil? && !verify['does_not_return'].nil?
+ verify[:contains] = verify['contains'] if verify[:contains].nil? && !verify['contains'].nil?
+ verify[:does_not_contain] = verify['does_not_contain'] if verify[:does_not_contain].nil? && !verify['does_not_contain'].nil?
+
+ 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..b064369
--- /dev/null
+++ b/lib/puppet/type/web.rb
@@ -0,0 +1,123 @@
+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(:file_parameters) do
+ desc "Hash of file 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
+
+ newparam(:if) do
+ desc "Invoke request only if the specified request returns true"
+ end
+
+ newparam(:unless) do
+ desc "Invoke request unless the specified request returns true"
+ 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.4.4