summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLamont Granquist <lamont@scriptkiddie.org>2016-04-06 10:26:56 -0700
committerLamont Granquist <lamont@scriptkiddie.org>2016-04-06 10:26:56 -0700
commitd5894aaa66fc0e3ac9c87bca44cd8405fcf8851b (patch)
treefd474c11b4ad8410f583a87eeae263bc3e8f1ad1
parent5d1a312bbbfc651d4ee4dce1e8fe923002c8aaea (diff)
parent5865d4606ec7a116f09df0f78479fb0a5c7def99 (diff)
downloadchef-d5894aaa66fc0e3ac9c87bca44cd8405fcf8851b.tar.gz
Merge pull request #4750 from jkerry/SFTP_Remote_File_Support
Sftp remote file support
-rw-r--r--chef.gemspec1
-rw-r--r--lib/chef/provider/remote_file/fetcher.rb2
-rw-r--r--lib/chef/provider/remote_file/sftp.rb105
-rw-r--r--lib/chef/providers.rb1
-rw-r--r--spec/unit/provider/remote_file/sftp_spec.rb150
5 files changed, 259 insertions, 0 deletions
diff --git a/chef.gemspec b/chef.gemspec
index acf712ea3d..e1055e01b5 100644
--- a/chef.gemspec
+++ b/chef.gemspec
@@ -26,6 +26,7 @@ Gem::Specification.new do |s|
s.add_dependency "ffi-yajl", "~> 2.2"
s.add_dependency "net-ssh", ">= 2.9", "< 4.0"
s.add_dependency "net-ssh-multi", "~> 1.1"
+ s.add_dependency "net-sftp", "~> 2.1", ">= 2.1.2"
s.add_dependency "highline", "~> 1.6", ">= 1.6.9"
s.add_dependency "erubis", "~> 2.7"
s.add_dependency "diff-lcs", "~> 1.2", ">= 1.2.4"
diff --git a/lib/chef/provider/remote_file/fetcher.rb b/lib/chef/provider/remote_file/fetcher.rb
index dad73cf17f..563d135d6a 100644
--- a/lib/chef/provider/remote_file/fetcher.rb
+++ b/lib/chef/provider/remote_file/fetcher.rb
@@ -31,6 +31,8 @@ class Chef
Chef::Provider::RemoteFile::HTTP.new(uri, new_resource, current_resource)
when "ftp"
Chef::Provider::RemoteFile::FTP.new(uri, new_resource, current_resource)
+ when "sftp"
+ Chef::Provider::RemoteFile::SFTP.new(uri, new_resource, current_resource)
when "file"
Chef::Provider::RemoteFile::LocalFile.new(uri, new_resource, current_resource)
else
diff --git a/lib/chef/provider/remote_file/sftp.rb b/lib/chef/provider/remote_file/sftp.rb
new file mode 100644
index 0000000000..530977e3c8
--- /dev/null
+++ b/lib/chef/provider/remote_file/sftp.rb
@@ -0,0 +1,105 @@
+#
+# Author:: John Kerry (<john@kerryhouse.net>)
+# Copyright:: Copyright 2013-2016, John Kerry
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require "uri"
+require "tempfile"
+require "net/sftp"
+require "chef/provider/remote_file"
+require "chef/file_content_management/tempfile"
+
+class Chef
+ class Provider
+ class RemoteFile
+ class SFTP
+ attr_reader :uri
+ attr_reader :new_resource
+ attr_reader :current_resource
+
+ def initialize(uri, new_resource, current_resource)
+ @uri = uri
+ @new_resource = new_resource
+ @current_resource = current_resource
+ validate_path!
+ validate_userinfo!
+ end
+
+ def hostname
+ @uri.host
+ end
+
+ def port
+ @uri.port
+ end
+
+ def user
+ URI.unescape(uri.user)
+ end
+
+ def fetch
+ get
+ end
+
+ private
+
+ def sftp
+ host = port ? "#{hostname}:#{port}" : hostname
+ @sftp ||= Net::SFTP.start(host, user, :password => pass)
+ end
+
+ def pass
+ URI.unescape(uri.password)
+ end
+
+ def validate_path!
+ path = uri.path.sub(%r{\A/}, "%2F") # re-encode the beginning slash because uri library decodes it.
+ directories = path.split(%r{/}, -1)
+ directories.each {|d|
+ d.gsub!(/%([0-9A-Fa-f][0-9A-Fa-f])/) { [$1].pack("H2") }
+ }
+ unless filename = directories.pop
+ raise ArgumentError, "no filename: #{path.inspect}"
+ end
+ if filename.length == 0 || filename.end_with?( "/" )
+ raise ArgumentError, "no filename: #{path.inspect}"
+ end
+ end
+
+ def validate_userinfo!
+ if uri.userinfo
+ unless uri.user
+ raise ArgumentError, "no user name provided in the sftp URI"
+ end
+ unless uri.password
+ raise ArgumentError, "no password provided in the sftp URI"
+ end
+ else
+ raise ArgumentError, "no userinfo provided in the sftp URI"
+ end
+ end
+
+ def get
+ tempfile =
+ Chef::FileContentManagement::Tempfile.new(@new_resource).tempfile
+ sftp.download!(uri.path, tempfile.path)
+ tempfile.close if tempfile
+ tempfile
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb
index 80fadfc8c2..f1c836614d 100644
--- a/lib/chef/providers.rb
+++ b/lib/chef/providers.rb
@@ -125,6 +125,7 @@ require "chef/provider/deploy/revision"
require "chef/provider/deploy/timestamped"
require "chef/provider/remote_file/ftp"
+require "chef/provider/remote_file/sftp"
require "chef/provider/remote_file/http"
require "chef/provider/remote_file/local_file"
require "chef/provider/remote_file/network_file"
diff --git a/spec/unit/provider/remote_file/sftp_spec.rb b/spec/unit/provider/remote_file/sftp_spec.rb
new file mode 100644
index 0000000000..673ea015c0
--- /dev/null
+++ b/spec/unit/provider/remote_file/sftp_spec.rb
@@ -0,0 +1,150 @@
+#
+# Author:: John Kerry (<john@kerryhouse.net>)
+# Copyright:: Copyright 2013-2016, John Kerry
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require "spec_helper"
+
+describe Chef::Provider::RemoteFile::SFTP do
+ #built out dependencies
+ let(:enclosing_directory) {
+ canonicalize_path(File.expand_path(File.join(CHEF_SPEC_DATA, "templates")))
+ }
+ let(:resource_path) {
+ canonicalize_path(File.expand_path(File.join(enclosing_directory, "seattle.txt")))
+ }
+
+ let(:new_resource) do
+ r = Chef::Resource::RemoteFile.new("remote file sftp backend test (new resource)")
+ r.path(resource_path)
+ r
+ end
+
+ let(:current_resource) do
+ Chef::Resource::RemoteFile.new("remote file sftp backend test (current resource)'")
+ end
+
+ let(:uri) { URI.parse("sftp://conan:cthu1hu@opscode.com/seattle.txt") }
+
+ let(:sftp) do
+ sftp = double(Net::SFTP, {})
+ allow(sftp).to receive(:download!)
+ sftp
+ end
+
+ let(:tempfile_path) { "/tmp/somedir/remote-file-sftp-backend-spec-test" }
+
+ let(:tempfile) do
+ t = StringIO.new
+ allow(t).to receive(:path).and_return(tempfile_path)
+ t
+ end
+
+ before(:each) do
+ allow(Net::SFTP).to receive(:start).with(any_args).and_return(sftp)
+ allow(Tempfile).to receive(:new).and_return(tempfile)
+ end
+ describe "on initialization without user and password provided in the URI" do
+ it "throws an argument exception with no userinfo is given" do
+ uri.userinfo = nil
+ uri.password = nil
+ uri.user = nil
+ expect { Chef::Provider::RemoteFile::SFTP.new(uri, new_resource, current_resource) }.to raise_error(ArgumentError)
+ end
+
+ it "throws an argument exception with no user name is given" do
+ uri.userinfo = ":cthu1hu"
+ uri.password = "cthu1hu"
+ uri.user = nil
+ expect { Chef::Provider::RemoteFile::SFTP.new(uri, new_resource, current_resource) }.to raise_error(ArgumentError)
+ end
+
+ it "throws an argument exception with no password is given" do
+ uri.userinfo = "conan:"
+ uri.password = nil
+ uri.user = "conan"
+ expect { Chef::Provider::RemoteFile::SFTP.new(uri, new_resource, current_resource) }.to raise_error(ArgumentError)
+ end
+
+ end
+
+ describe "on initialization with user and password provided in the URI" do
+
+ it "throws an argument exception when no path is given" do
+ uri.path = ""
+ expect { Chef::Provider::RemoteFile::SFTP.new(uri, new_resource, current_resource) }.to raise_error(ArgumentError)
+ end
+
+ it "throws an argument exception when only a / is given" do
+ uri.path = "/"
+ expect { Chef::Provider::RemoteFile::SFTP.new(uri, new_resource, current_resource) }.to raise_error(ArgumentError)
+ end
+
+ it "throws an argument exception when no filename is given" do
+ uri.path = "/the/whole/path/"
+ expect { Chef::Provider::RemoteFile::SFTP.new(uri, new_resource, current_resource) }.to raise_error(ArgumentError)
+ end
+
+ end
+
+ describe "when fetching the object" do
+
+ let(:cache_control_data) { Chef::Provider::RemoteFile::CacheControlData.new(uri) }
+ let(:current_resource_checksum) { "e2a8938cc31754f6c067b35aab1d0d4864272e9bf8504536ef3e79ebf8432305" }
+
+ subject(:fetcher) { Chef::Provider::RemoteFile::SFTP.new(uri, new_resource, current_resource) }
+
+ before do
+ current_resource.checksum(current_resource_checksum)
+ end
+
+ it "should attempt to download a file from the provided url and path" do
+ expect(sftp).to receive(:download!).with("/seattle.txt", "/tmp/somedir/remote-file-sftp-backend-spec-test")
+ fetcher.fetch
+ end
+
+ context "and the URI specifies an alternate port" do
+ let(:uri) { URI.parse("ftp://conan:cthu1hu@opscode.com:8021/seattle.txt") }
+
+ it "should connect on an alternate port when one is provided" do
+ expect(Net::SFTP).to receive(:start).with("opscode.com:8021", "conan", :password => "cthu1hu")
+ fetcher.fetch
+ end
+
+ end
+
+ context "and the uri specifies a nested path" do
+ let(:uri) { URI.parse("ftp://conan:cthu1hu@opscode.com/the/whole/path/seattle.txt") }
+
+ it "should fetch the file from the correct path" do
+ expect(sftp).to receive(:download!).with("the/whole/path/seattle.txt", "/tmp/somedir/remote-file-sftp-backend-spec-test")
+ fetcher.fetch
+ end
+ end
+
+ context "when not using last modified based conditional fetching" do
+ before do
+ new_resource.use_last_modified(false)
+ end
+
+ it "should return a tempfile in the result" do
+ result = fetcher.fetch
+ expect(result).to equal(tempfile)
+ end
+
+ end
+ end
+end