summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesse Campbell <hikeit@gmail.com>2013-02-26 13:29:01 -0500
committerJesse Campbell <hikeit@gmail.com>2013-02-26 13:29:01 -0500
commit691e0195fdbea22c35f2b472c7c2330a432942ac (patch)
tree82dd305ec9e6cfe6be6197711c26f0f453b118f8
parentd731a62c7680a4f7d81ca2862e3c2f1ee3701e44 (diff)
downloadchef-691e0195fdbea22c35f2b472c7c2330a432942ac.tar.gz
support etags and last modified times for remote_file
-rw-r--r--lib/chef/provider/remote_file.rb91
-rw-r--r--lib/chef/provider/remote_file/ftp.rb55
-rw-r--r--lib/chef/resource/remote_file.rb27
-rw-r--r--spec/unit/provider/remote_file_spec.rb29
4 files changed, 163 insertions, 39 deletions
diff --git a/lib/chef/provider/remote_file.rb b/lib/chef/provider/remote_file.rb
index 4d1e696d0e..e979f5a4ba 100644
--- a/lib/chef/provider/remote_file.rb
+++ b/lib/chef/provider/remote_file.rb
@@ -30,6 +30,12 @@ class Chef
def load_current_resource
@current_resource = Chef::Resource::RemoteFile.new(@new_resource.name)
+ fileinfo = load_fileinfo
+ if fileinfo["src"]
+ @current_resource.etag fileinfo["etag"]
+ @current_resource.last_modified fileinfo["last_modified"]
+ @current_resource.source fileinfo["src"]
+ end
super
end
@@ -40,10 +46,13 @@ class Chef
Chef::Log.debug("#{@new_resource} checksum matches target checksum (#{@new_resource.checksum}) - not updating")
else
sources = @new_resource.source
- raw_file, raw_file_source = try_multiple_sources(sources)
- if matches_current_checksum?(raw_file)
- Chef::Log.debug "#{@new_resource} target and source checksums are the same - not updating"
+ raw_file, raw_file_source, target_matched = try_multiple_sources(sources)
+ if target_matched
+ Chef::Log.info("#{@new_resource} matched #{raw_file_source}, not updating")
+ elsif matches_current_checksum?(raw_file)
+ Chef::Log.info("#{@new_resource} downloaded from #{raw_file_source}, checksums match, not updating")
else
+ Chef::Log.info("#{@new_resource} downloaded from #{raw_file_source}")
description = []
description << "copy file downloaded from #{raw_file_source} into #{@new_resource.path}"
description << diff_current(raw_file.path)
@@ -98,7 +107,7 @@ class Chef
source = sources.shift
begin
uri = URI.parse(source)
- raw_file = grab_file_from_uri(uri)
+ raw_file, target_matched = grab_file_from_uri(uri)
rescue ArgumentError => e
raise e
rescue => e
@@ -107,9 +116,9 @@ class Chef
else
error = e.to_s
end
- Chef::Log.debug("#{@new_resource} cannot be downloaded from #{source}: #{error}")
+ Chef::Log.info("#{@new_resource} cannot be downloaded from #{source}: #{error}")
if source = sources.shift
- Chef::Log.debug("#{@new_resource} trying to download from another mirror")
+ Chef::Log.info("#{@new_resource} trying to download from another mirror")
retry
else
raise e
@@ -118,29 +127,91 @@ class Chef
if uri.userinfo
uri.password = "********"
end
- return raw_file, uri.to_s
+ return raw_file, uri.to_s, target_matched
end
# Given a source uri, return a Tempfile, or a File that acts like a Tempfile (close! method)
def grab_file_from_uri(uri)
+ if_modified_since = @new_resource.last_modified
+ if_none_match = @new_resource.etag
+ if uri == @current_resource.source[0]
+ if_modified_since ||= @current_resource.last_modified
+ if_none_match ||= @current_resource.etag
+ end
+ target_matched = false
+ raw_file = nil
+ last_modified = nil
+ etag = nil
if URI::HTTP === uri
#HTTP or HTTPS
- raw_file = RestClient::Request.execute(:method => :get, :url => uri.to_s, :raw_response => true).file
+ begin
+ headers = Hash.new
+ if if_none_match
+ headers[:if_none_match] = "\"#{if_none_match}\""
+ elsif if_modified_since
+ headers[:if_modified_since] = if_modified_since.strftime("%a, %d %b %Y %H:%M:%S %Z")
+ end
+ rest = RestClient::Request.execute(:method => :get, :url => uri.to_s, :headers => headers, :raw_response => true)
+ raw_file = rest.file
+ if rest.headers.include?(:last_modified)
+ last_modified = Time.parse(rest.headers[:last_modified])
+ end
+ if rest.headers.include?(:etag)
+ etag = rest.headers[:etag]
+ end
+ rescue RestClient::Exception => e
+ if e.http_code == 304
+ target_matched = true
+ else
+ raise e
+ end
+ end
elsif URI::FTP === uri
#FTP
- raw_file = FTP::fetch(uri, @new_resource.ftp_active_mode)
+ raw_file, last_modified = FTP::fetch_if_modified(uri, @new_resource.ftp_active_mode, if_modified_since)
+ if last_modified && if_modified_since && last_modified <= if_modified_since
+ target_matched = true
+ end
elsif uri.scheme == "file"
#local/network file
+ last_modified = ::File.mtime(uri.path)
raw_file = ::File.new(uri.path, "r")
def raw_file.close!
self.close
end
+ if last_modified && if_modified_since && last_modified.to_i <= if_modified_since.to_i
+ target_matched = true
+ end
else
raise ArgumentError, "Invalid uri. Only http(s), ftp, and file are currently supported"
end
- raw_file
+ unless target_matched
+ @new_resource.etag etag unless @new_resource.etag
+ @new_resource.last_modified last_modified unless @new_resource.last_modified
+ save_fileinfo(uri)
+ end
+ return raw_file, target_matched
end
+ def load_fileinfo
+ begin
+ Chef::JSONCompat.from_json(Chef::FileCache.load("remote_file/#{new_resource.name}"))
+ rescue Chef::Exceptions::FileNotFound
+ cache = Hash.new
+ cache["etag"] = nil
+ cache["last_modified"] = nil
+ cache["src"] = nil
+ cache
+ end
+ end
+
+ def save_fileinfo(uri)
+ cache = Hash.new
+ cache["etag"] = @new_resource.etag
+ cache["last_modified"] = @new_resource.last_modified
+ cache["src"] = uri
+ Chef::FileCache.store("remote_file/#{new_resource.name}", cache.to_json)
+ end
end
end
end
diff --git a/lib/chef/provider/remote_file/ftp.rb b/lib/chef/provider/remote_file/ftp.rb
index 3c5d3e0a91..b0e28a4de8 100644
--- a/lib/chef/provider/remote_file/ftp.rb
+++ b/lib/chef/provider/remote_file/ftp.rb
@@ -28,7 +28,24 @@ class Chef
# Fetches the file at uri using Net::FTP, returning a Tempfile
def self.fetch(uri, ftp_active_mode)
- self.new(uri, ftp_active_mode).fetch()
+ ftp = self.new(uri, ftp_active_mode)
+ ftp.connect
+ tempfile = ftp.fetch
+ ftp.disconnect
+ tempfile
+ end
+
+ def self.fetch_if_modified(uri, ftp_active_mode, last_modified)
+ ftp = self.new(uri, ftp_active_mode)
+ ftp.connect
+ mtime = ftp.mtime
+ if mtime && last_modified && mtime.to_i <= last_modified.to_i
+ tempfile = nil
+ else
+ tempfile = ftp.fetch
+ end
+ ftp.disconnect
+ return tempfile, mtime
end
# Parse the uri into instance variables
@@ -42,6 +59,7 @@ class Chef
@ftp_active_mode = ftp_active_mode
@hostname = uri.hostname
@port = uri.port
+ @ftp = Net::FTP.new
if uri.userinfo
@user = URI.unescape(uri.user)
@pass = URI.unescape(uri.password)
@@ -51,28 +69,35 @@ class Chef
end
end
- # Fetches using Net::FTP, returns a Tempfile with the content
- def fetch()
- tempfile = Tempfile.new(@filename)
-
+ def connect
# The access sequence is defined by RFC 1738
- ftp = Net::FTP.new
- ftp.connect(@hostname, @port)
- ftp.passive = !@ftp_active_mode
- ftp.login(@user, @pass)
+ @ftp.connect(@hostname, @port)
+ @ftp.passive = !@ftp_active_mode
+ @ftp.login(@user, @pass)
@directories.each do |cwd|
- ftp.voidcmd("CWD #{cwd}")
+ @ftp.voidcmd("CWD #{cwd}")
end
+ end
+
+ def disconnect
+ @ftp.close
+ end
+
+ def mtime
+ @ftp.mtime(@filename)
+ end
+
+ # Fetches using Net::FTP, returns a Tempfile with the content
+ def fetch
+ tempfile = Tempfile.new(@filename)
if @typecode
- ftp.voidcmd("TYPE #{@typecode.upcase}")
+ @ftp.voidcmd("TYPE #{@typecode.upcase}")
end
- ftp.getbinaryfile(@filename, tempfile.path)
- ftp.close
-
+ @ftp.getbinaryfile(@filename, tempfile.path)
tempfile
end
- private
+ private
def parse_path(path)
path = path.sub(%r{\A/}, '%2F') # re-encode the beginning slash because uri library decodes it.
diff --git a/lib/chef/resource/remote_file.rb b/lib/chef/resource/remote_file.rb
index 524e00186b..df055ab6b4 100644
--- a/lib/chef/resource/remote_file.rb
+++ b/lib/chef/resource/remote_file.rb
@@ -33,6 +33,8 @@ class Chef
@resource_name = :remote_file
@action = "create"
@source = []
+ @etag = nil
+ @last_modified = nil
@ftp_active_mode = false
@provider = Chef::Provider::RemoteFile
end
@@ -55,6 +57,31 @@ class Chef
)
end
+ def etag(args=nil)
+ # Only store the etag itself, skip the quotes (and leading W/ if it's there)
+ if args.is_a?(String) && args.include?('"')
+ args = args.split('"')[1]
+ end
+ set_or_return(
+ :etag,
+ args,
+ :kind_of => String
+ )
+ end
+
+ def last_modified(args=nil)
+ if args.is_a?(String)
+ args = Time.parse(args).gmtime
+ elsif args.is_a?(Time)
+ args.gmtime
+ end
+ set_or_return(
+ :last_modified,
+ args,
+ :kind_of => Time
+ )
+ end
+
def ftp_active_mode(args=nil)
set_or_return(
:ftp_active_mode,
diff --git a/spec/unit/provider/remote_file_spec.rb b/spec/unit/provider/remote_file_spec.rb
index 149f0462c3..2cd7559c95 100644
--- a/spec/unit/provider/remote_file_spec.rb
+++ b/spec/unit/provider/remote_file_spec.rb
@@ -46,7 +46,7 @@ describe Chef::Provider::RemoteFile, "action_create" do
describe "when fetching the file from the remote" do
before(:each) do
@tempfile = Tempfile.new("chef-rspec-remote_file_spec-line#{__LINE__}--")
- @rawresp = RestClient::RawResponse.new(@tempfile, nil, nil)
+ @rawresp = RestClient::RawResponse.new(@tempfile, Hash.new, nil)
RestClient::Request.stub!(:execute).and_return(@rawresp)
@resource.cookbook_name = "monkey"
@@ -79,14 +79,14 @@ describe Chef::Provider::RemoteFile, "action_create" do
shared_examples_for "source specified with multiple URIs" do
it "should try to download the next URI when the first one fails" do
- RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://foo", :raw_response => true).once.and_raise(SocketError)
- RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://bar", :raw_response => true).once.and_return(@rawresp)
+ RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://foo", :headers => {}, :raw_response => true).once.and_raise(SocketError)
+ RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://bar", :headers => {}, :raw_response => true).once.and_return(@rawresp)
@provider.run_action(:create)
end
it "should raise an exception when all the URIs fail" do
- RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://foo", :raw_response => true).once.and_raise(SocketError)
- RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://bar", :raw_response => true).once.and_raise(SocketError)
+ RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://foo", :headers => {}, :raw_response => true).once.and_raise(SocketError)
+ RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://bar", :headers => {}, :raw_response => true).once.and_raise(SocketError)
lambda { @provider.run_action(:create) }.should raise_error(SocketError)
end
@@ -121,7 +121,7 @@ describe Chef::Provider::RemoteFile, "action_create" do
end
it "does not download the file" do
- RestClient::Request.should_not_receive(:execute).with("http://opscode.com/seattle.txt").and_return(@tempfile)
+ RestClient::Request.should_not_receive(:execute)
@provider.run_action(:create)
end
@@ -138,7 +138,7 @@ describe Chef::Provider::RemoteFile, "action_create" do
end
it "should not download the file if the checksum is a partial match from the beginning" do
- @rawresp.should_not_receive(:fetch).with("http://opscode.com/seattle.txt").and_return(@tempfile)
+ RestClient::Request.should_not_receive(:execute)
@provider.run_action(:create)
end
@@ -152,7 +152,7 @@ describe Chef::Provider::RemoteFile, "action_create" do
describe "and the existing file doesn't match the given checksum" do
it "downloads the file" do
@resource.checksum("this hash doesn't match")
- RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://opscode.com/seattle.txt", :raw_response => true).and_return(@rawresp)
+ RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://opscode.com/seattle.txt", :headers => {}, :raw_response => true).and_return(@rawresp)
@provider.stub!(:update_new_file_state)
@provider.run_action(:create)
end
@@ -160,7 +160,7 @@ describe Chef::Provider::RemoteFile, "action_create" do
it "does not consider the checksum a match if the matching string is offset" do
# i.e., the existing file is "0fd012fdc96e96f8f7cf2046522a54aed0ce470224513e45da6bc1a17a4924aa"
@resource.checksum("fd012fd")
- RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://opscode.com/seattle.txt", :raw_response => true).and_return(@rawresp)
+ RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://opscode.com/seattle.txt", :headers => {}, :raw_response => true).and_return(@rawresp)
@provider.stub!(:update_new_file_state)
@provider.run_action(:create)
end
@@ -171,7 +171,7 @@ describe Chef::Provider::RemoteFile, "action_create" do
describe "and the resource doesn't specify a checksum" do
it "should download the file from the remote URL" do
@resource.checksum(nil)
- RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://opscode.com/seattle.txt", :raw_response => true).and_return(@rawresp)
+ RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://opscode.com/seattle.txt", :headers => {}, :raw_response => true).and_return(@rawresp)
@provider.run_action(:create)
end
end
@@ -188,7 +188,7 @@ describe Chef::Provider::RemoteFile, "action_create" do
context "and the target file is a tarball" do
before do
@resource.path(File.expand_path(File.join(CHEF_SPEC_DATA, "seattle.tar.gz")))
- RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://opscode.com/seattle.txt", :raw_response => true).and_return(@rawresp)
+ RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://opscode.com/seattle.txt", :headers => {}, :raw_response => true).and_return(@rawresp)
end
it "disables gzip in the http client" do
@@ -200,7 +200,7 @@ describe Chef::Provider::RemoteFile, "action_create" do
context "and the source appears to be a tarball" do
before do
@resource.source("http://example.com/tarball.tgz")
- RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://example.com/tarball.tgz", :raw_response => true).and_return(@rawresp)
+ RestClient::Request.should_receive(:execute).with(:method => :get, :url => "http://example.com/tarball.tgz", :headers => {}, :raw_response => true).and_return(@rawresp)
end
it "disables gzip in the http client" do
@@ -214,13 +214,13 @@ describe Chef::Provider::RemoteFile, "action_create" do
end
it "should fetch with ftp in passive mode" do
- Chef::Provider::RemoteFile::FTP.should_receive(:fetch).with(URI.parse("ftp://opscode.com/seattle.txt"), false).and_return(@tempfile)
+ Chef::Provider::RemoteFile::FTP.should_receive(:fetch_if_modified).with(URI.parse("ftp://opscode.com/seattle.txt"), false, nil).and_return(@tempfile)
@provider.run_action(:create)
end
it "should fetch with ftp in active mode" do
@resource.ftp_active_mode true
- Chef::Provider::RemoteFile::FTP.should_receive(:fetch).with(URI.parse("ftp://opscode.com/seattle.txt"), true).and_return(@tempfile)
+ Chef::Provider::RemoteFile::FTP.should_receive(:fetch_if_modified).with(URI.parse("ftp://opscode.com/seattle.txt"), true, nil).and_return(@tempfile)
@provider.run_action(:create)
end
end
@@ -231,6 +231,7 @@ describe Chef::Provider::RemoteFile, "action_create" do
end
it "should load the local file" do
+ File.should_receive(:mtime).with("/nyan_cat.png").and_return(Time.now)
File.should_receive(:new).with("/nyan_cat.png", "r").and_return(File.open(File.join(CHEF_SPEC_DATA, "remote_file", "nyan_cat.png"), "r"))
@provider.run_action(:create)
end