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

Adds MiqFtpLib #360

Merged
merged 3 commits into from
Sep 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions lib/gems/pending/util/miq_ftp_lib.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
require 'net/ftp'

# 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.
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
1 change: 1 addition & 0 deletions manageiq-gems-pending.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ Gem::Specification.new do |s|
s.add_development_dependency "test-unit"
s.add_development_dependency "timecop", "~> 0.9.1"
s.add_development_dependency "xml-simple", "~> 1.1.0"
s.add_development_dependency "ftpd", "~> 2.1.0"
end
90 changes: 90 additions & 0 deletions spec/support/contexts/with_ftp_server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
require "ftpd"
require "tmpdir"
require "tempfile"
require "fileutils"

class FtpSingletonServer
class << self
attr_reader :driver
end

def self.run_ftp_server
@driver = FTPServerDriver.new
@ftp_server = Ftpd::FtpServer.new(@driver)
@ftp_server.on_exception do |e|
STDOUT.puts e.inspect
end
@ftp_server.start
end

def self.bound_port
@ftp_server.bound_port
end

def self.stop_ftp_server
@ftp_server.stop
@ftp_server = nil

@driver.cleanup
@driver = nil
end
end

class FTPServerDriver
attr_reader :existing_file, :existing_dir

def initialize
create_tmp_dir
end

def authenticate(username, password)
username == "ftpuser" && password == "ftppass"
end

def file_system(_user)
Ftpd::DiskFileSystem.new(@ftp_dir)
end

def cleanup
FileUtils.remove_entry(@ftp_dir)
end

def create_existing_file(size = 0)
@existing_file ||= Tempfile.new("", @ftp_dir).tap { |tmp| tmp.puts "0" * size }
end

# Create a dir under the @ftp_dir, but only return the created directory name
def create_existing_dir
@existing_dir ||= Dir.mktmpdir(nil, @ftp_dir).sub("#{@ftp_dir}/", "")
end

private

def create_tmp_dir
@ftp_dir = Dir.mktmpdir
end
end

shared_context "with ftp server", :with_ftp_server do
before(:all) { FtpSingletonServer.run_ftp_server }
after(:all) { FtpSingletonServer.stop_ftp_server }

# HACK: Avoid permission denied errors with `ftpd` starting on port 21, but
# our FTP lib always assuming that we are using the default port
#
# The hack basically sets the default port for `Net::FTP` to the bound port
# of the running server
before(:each) do
stub_const("Net::FTP::FTP_PORT", FtpSingletonServer.bound_port)
end

let(:valid_ftp_creds) { { :username => "ftpuser", :password => "ftppass" } }

def existing_ftp_file(size = 0)
FtpSingletonServer.driver.create_existing_file(size)
end

def existing_ftp_dir
FtpSingletonServer.driver.create_existing_dir
end
end
193 changes: 193 additions & 0 deletions spec/util/miq_ftp_lib_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
require 'util/miq_ftp_lib'
require 'logger' # probably loaded elsewhere, but for the below classes

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