Skip to content

Commit

Permalink
Adds MiqFtpLib
Browse files Browse the repository at this point in the history
This is lifted from the FileDepotFtp code in the manageiq repo, and is
just some shared methods around connecting to an FTP endpoint.

Adds some tests around it as well.
  • Loading branch information
NickLaMuro committed Sep 7, 2018
1 parent 04552b0 commit afa4832
Show file tree
Hide file tree
Showing 2 changed files with 268 additions and 0 deletions.
76 changes: 76 additions & 0 deletions lib/gems/pending/util/miq_ftp_lib.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
require 'net/ftp'
require 'logger'

# Helper methods for net/ftp based classes and files.
#
# Will setup a `@ftp` attr_accessor to be used as the return value for
# `.connect`, the main method being provided in this class. Will also setup
# logging if not already done for the particular class (that follows the
# VmdbLogger conventions, and setup a `uri` attr_accessor if that doesn't
# already exist.
module MiqFtpLib
def self.included(klass)
klass.send(:attr_accessor, :ftp)
end

def connect(cred_hash = nil)
host = URI(uri).hostname

begin
_log.info("Connecting to FTP host #{host_ref}...")
@ftp = Net::FTP.new(host)
# Use passive mode to avoid firewall issues see http://slacksite.com/other/ftp.html#passive
@ftp.passive = true
# @ftp.debug_mode = true if settings[:debug] # TODO: add debug option
creds = cred_hash ? [cred_hash[:username], cred_hash[:password]] : login_credentials
@ftp.login(*creds)
_log.info("Successfully connected FTP host #{host_ref}...")
rescue SocketError => err
_log.error("Failed to connect. #{err.message}")
raise
rescue Net::FTPPermError => err
_log.error("Failed to login. #{err.message}")
raise
else
@ftp
end
end

def file_exists?(file_or_directory)
!ftp.nlst(file_or_directory.to_s).empty?
rescue Net::FTPPermError
false
end

private

def host_ref
return @host_ref if @host_ref
@host_ref = URI(uri).hostname
@host_ref << " (#{name})" if respond_to?(:name)
@host_ref
end

def create_directory_structure(directory_path)
pwd = ftp.pwd
directory_path.to_s.split('/').each do |directory|
unless ftp.nlst.include?(directory)
_log.info("creating #{directory}")
ftp.mkdir(directory)
end
ftp.chdir(directory)
end
ftp.chdir(pwd)
end

def with_connection(cred_hash = nil)
raise _("no block given") unless block_given?
_log.info("Connecting through #{self.class.name}: [#{host_ref}]")
begin
connect(cred_hash)
yield @ftp
ensure
@ftp.try(:close) && @ftp = nil
end
end
end
192 changes: 192 additions & 0 deletions spec/util/miq_ftp_lib_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
require 'util/miq_ftp_lib'

class FTPKlass
include MiqFtpLib

attr_accessor :uri

def self.instance_logger
Logger.new(File::NULL) # null logger (for testing)
end

private

def _log
self.class.instance_logger
end
end

class OtherFTPKlass
include MiqFtpLib

attr_accessor :uri

def _log
private_log_method
end

private

def private_log_method
Logger.new(File::NULL) # null logger (for testing)
end

def login_credentials
%w(ftpuser ftppass)
end
end

shared_examples "connecting" do |valid_cred_hash|
let(:cred_hash) { valid_cred_hash }

before { subject.uri = "ftp://localhost" }

it "logs in with valid credentials" do
expect { subject.connect(cred_hash) }.not_to raise_error
end

it "sets the connection to passive" do
subject.connect(cred_hash)
expect(subject.ftp.passive).to eq(true)
end

context "with an invalid ftp credentials" do
let(:cred_hash) { { :username => "invalid", :password => "alsoinvalid" } }

it "raises a Net::FTPPermError" do
expect { subject.connect(cred_hash) }.to raise_error(Net::FTPPermError)
end
end
end

shared_examples "with a connection" do |valid_cred_hash|
let(:cred_hash) { valid_cred_hash }
let(:error_msg) { "no block given" }

before do
subject.uri = "ftp://localhost"
allow(subject).to receive(:_).with(error_msg).and_return(error_msg)
end

def with_connection(&block)
subject.send(:with_connection, cred_hash, &block)
end

def get_socket(ftp)
ftp.instance_variable_get(:@sock).instance_variable_get(:@io)
end

it "passes the ftp object to the block" do
with_connection do |ftp|
expect(ftp).to be_a(Net::FTP)
expect(subject.ftp).to be(ftp)
end
end

it "closes the ftp connection after the block is finished" do
ftp_instance = subject.connect(cred_hash)
# stub further calls to `#connect`
expect(subject).to receive(:connect).and_return(ftp_instance)

with_connection { |ftp| }
expect(subject.ftp).to eq(nil)
expect(ftp_instance.closed?).to eq(true)
end

it "raises an error if no block is given" do
expect { with_connection }.to raise_error(RuntimeError, error_msg)
end
end

describe MiqFtpLib do
subject { FTPKlass.new }

describe "when included" do
it "has a `ftp` accessor" do
ftp_instance = Net::FTP.new
subject.ftp = ftp_instance

expect(subject.ftp).to eq ftp_instance
end
end

describe "#connect", :with_ftp_server do
context "with credentials hash" do
subject { FTPKlass.new }

include_examples "connecting", :username => "ftpuser", :password => "ftppass"
end

context "with login_credentials method" do
subject { OtherFTPKlass.new }

include_examples "connecting"
end
end

describe "#with_connection", :with_ftp_server do
context "with credentials hash" do
subject { FTPKlass.new }

include_examples "with a connection", :username => "ftpuser", :password => "ftppass"
end

context "with login_credentials method" do
subject { OtherFTPKlass.new }

include_examples "with a connection"
end
end

describe "#file_exists?", :with_ftp_server do
let(:existing_file) { File.basename(existing_ftp_file) }

subject { FTPKlass.new.tap { |ftp| ftp.uri = "ftp://localhost" } }
before { subject.connect(valid_ftp_creds) }

it "returns true if the file exists" do
expect(subject.file_exists?(existing_file)).to eq(true)
end

it "returns false if the file does not exist" do
expect(subject.file_exists?("#{existing_file}.fake")).to eq(false)
end
end

# Note: Don't use `file_exists?` to try and test the directory existance.
# Most FTP implementations will send the results of `nlst` as the contents of
# a directory if a directory is given.
#
# In our current implementation, this will return a empty list if the
# directory is empty, thus causing the check to fail. Testing against the
# `ftp.nlst(parent_dir)` will make sure the directory in question is included
# in it's parent.
describe "#create_directory_structure", :with_ftp_server do
subject { OtherFTPKlass.new.tap { |ftp| ftp.uri = "ftp://localhost" } }
before { subject.connect(valid_ftp_creds) }

it "creates a new nested directory" do
new_dir = "foo/bar/baz"
parent_dir = File.dirname(new_dir)

expect(subject.ftp.nlst(parent_dir).include?("baz")).to eq(false)
subject.send(:create_directory_structure, new_dir)
expect(subject.ftp.nlst(parent_dir).include?("baz")).to eq(true)
end

context "to an existing directory" do
it "creates the nested directory without messing with the existing" do
existing_dir = existing_ftp_dir
new_dir = File.join(existing_ftp_dir, "foo/bar/baz")
parent_dir = File.dirname(new_dir)

expect(subject.ftp.nlst.include?(existing_dir)).to eq(true)
expect(subject.ftp.nlst(parent_dir).include?("baz")).to eq(false)

subject.send(:create_directory_structure, new_dir)
expect(subject.ftp.nlst.include?(existing_dir)).to eq(true)
expect(subject.ftp.nlst(parent_dir).include?("baz")).to eq(true)
end
end
end
end

0 comments on commit afa4832

Please sign in to comment.