diff options
author | Lamont Granquist <lamont@scriptkiddie.org> | 2016-04-06 10:26:56 -0700 |
---|---|---|
committer | Lamont Granquist <lamont@scriptkiddie.org> | 2016-04-06 10:26:56 -0700 |
commit | d5894aaa66fc0e3ac9c87bca44cd8405fcf8851b (patch) | |
tree | fd474c11b4ad8410f583a87eeae263bc3e8f1ad1 | |
parent | 5d1a312bbbfc651d4ee4dce1e8fe923002c8aaea (diff) | |
parent | 5865d4606ec7a116f09df0f78479fb0a5c7def99 (diff) | |
download | chef-d5894aaa66fc0e3ac9c87bca44cd8405fcf8851b.tar.gz |
Merge pull request #4750 from jkerry/SFTP_Remote_File_Support
Sftp remote file support
-rw-r--r-- | chef.gemspec | 1 | ||||
-rw-r--r-- | lib/chef/provider/remote_file/fetcher.rb | 2 | ||||
-rw-r--r-- | lib/chef/provider/remote_file/sftp.rb | 105 | ||||
-rw-r--r-- | lib/chef/providers.rb | 1 | ||||
-rw-r--r-- | spec/unit/provider/remote_file/sftp_spec.rb | 150 |
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 |