Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sftp remote file support #4750

Merged
merged 8 commits into from
Apr 6, 2016
1 change: 1 addition & 0 deletions chef.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions lib/chef/provider/remote_file/fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def self.for_resource(uri, new_resource, current_resource)
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
Expand Down
118 changes: 118 additions & 0 deletions lib/chef/provider/remote_file/sftp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#
# Author:: Jesse Campbell (<hikeit@gmail.com>)
# Copyright:: Copyright 2013-2016, Jesse Campbell
# 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 filename
parse_path if @filename.nil?
@filename
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks copied from the ftp class

the thing is that in #get you've removed the one reference to this method. the filename in parse_path is actually a local variable that gets constructed (hella confusingly -- the ftp class could use a bit of cleanup to make it clearer) and therefore both this #filename code and the #parse_path code are unused in this class that you've written and could simply be deleted.


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!
parse_path
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

# Fetches using Net::FTP, returns a Tempfile with the content
def get
tempfile =
Chef::FileContentManagement::Tempfile.new(@new_resource).tempfile
sftp.download!(uri.path, tempfile.path)
tempfile.close if tempfile
tempfile
end

def parse_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

@directories, @filename = directories, filename
end

end
end
end
end
1 change: 1 addition & 0 deletions lib/chef/providers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
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"
Expand Down
150 changes: 150 additions & 0 deletions spec/unit/provider/remote_file/sftp_spec.rb
Original file line number Diff line number Diff line change
@@ -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)