lib/gems/pending/util/mount/miq_generic_mount_session.rb
require 'active_support/core_ext/object/blank'
require 'fileutils'
require 'logger'
require 'sys-uname'
require 'uri'
require 'awesome_spawn'
class MiqGenericMountSession < MiqFileStorage::Interface
class NoSuchFileOrDirectory < RuntimeError; end
class << self
def run_command(command, args)
AwesomeSpawn.run!(command, :params => args, :combined_output => true).output
end
end
attr_accessor :settings, :mnt_point
attr_writer :logger
def initialize(log_settings)
raise "URI missing" unless log_settings.key?(:uri)
@settings = log_settings.dup
@mnt_point = nil
end
def logger
@logger ||= $log.nil? ? :: Logger.new(STDOUT) : $log
end
def mount(args)
self.class.run_command("mount", args)
rescue AwesomeSpawn::CommandResultError => err
raise if err.result.exit_status != 1
sudo_mount(args)
end
def sudo_mount(args)
self.class.run_command("sudo mount", args)
end
def self.umount(mount_point)
run_command("umount", [mount_point])
rescue AwesomeSpawn::CommandResultError => err
raise if err.result.exit_status != 1
sudo_umount(mount_point)
end
def self.sudo_umount(mount_point)
run_command("sudo umount", [mount_point])
end
def self.in_depot_session(opts, &_block)
raise "No block provided!" unless block_given?
session = new_session(opts)
yield session
ensure
session.disconnect if session
end
def self.new_session(opts)
klass = uri_scheme_to_class(opts[:uri])
session = klass.new_with_opts(opts)
session.connect
session
end
def self.new_with_opts(opts)
new(opts.slice(:uri, :username, :password))
end
def self.uri_scheme_to_class(uri)
require 'uri'
scheme, userinfo, host, port, registry, share, opaque, query, fragment = URI.split(URI::DEFAULT_PARSER.escape(uri))
case scheme
when 'smb'
MiqSmbSession
when 'nfs'
MiqNfsSession
when 'glusterfs'
MiqGlusterfsSession
else
raise "unsupported scheme #{scheme} from uri: #{uri}"
end
end
def mount_share
require 'tmpdir'
@mnt_point = settings_mount_point || Dir.mktmpdir("miq_")
end
def get_ping_depot_options
@@ping_depot_options ||= begin
opts = ::VMDB::Config.new("vmdb").config[:log][:collection] if defined?(::VMDB) && defined?(::VMDB::CONFIG)
opts = {:ping_depot => false}
opts
end
end
def ping_timeout
get_ping_depot_options
@@ping_timeout ||= (@@ping_depot_options[:ping_depot_timeout] || 20)
end
def do_ping?
get_ping_depot_options
@@do_ping ||= @@ping_depot_options[:ping_depot] == true
end
def pingable?
log_header = "MIQ(#{self.class.name}-pingable?)"
return true unless self.do_ping?
return true unless @settings[:ports].kind_of?(Array)
res = false
require 'net/ping'
begin
# To prevent "no route to host" type issues, assume refused connection indicates the host is reachable
before = Net::Ping::TCP.econnrefused
Net::Ping::TCP.econnrefused = true
@settings[:ports].each do |port|
logger.info("#{log_header} pinging: #{@host} on #{port} with timeout: #{ping_timeout}")
tcp1 = Net::Ping::TCP.new(@host, port, ping_timeout)
res = tcp1.ping
logger.info("#{log_header} pinging: #{@host} on #{port} with timeout: #{ping_timeout}...result: #{res}")
break if res == true
end
ensure
Net::Ping::TCP.econnrefused = before
end
res == true
end
def connect
log_header = "MIQ(#{self.class.name}-connect)"
# Replace any encoded spaces back into spaces since the mount commands accepts quoted spaces
@mount_path = @mount_path.to_s.gsub('%20', ' ')
# # Grab only the share part of a path such as: /temp/default_1/evm_1/current_default_1_evm_1_20091120_192429_20091120_225653.zip
# @mount_path = @mount_path.split("/")[0..1].join("/")
begin
raise "Connect: Cannot communicate with: #{@host} - verify the URI host value and your DNS settings" unless self.pingable?
mount_share
rescue => err
if err.kind_of?(RuntimeError) && err.message =~ /No such file or directory/
msg = "No such file or directory when connecting to host: [#{@host}] share: [#{@mount_path}]"
raise NoSuchFileOrDirectory, msg
end
msg = "Connecting to host: [#{@host}], share: [#{@mount_path}] encountered error: [#{err.class.name}] [#{err.message}]"
logger.error("#{log_header} #{msg}...#{err.backtrace.join("\n")}")
disconnect
raise
end
end
def disconnect
self.class.disconnect(@mnt_point, logger)
@mnt_point = nil
end
def self.disconnect(mnt_point, logger = $log)
return if mnt_point.nil?
log_header = "MIQ(#{self.class.name}-disconnect)"
logger.info("#{log_header} Disconnecting mount point: #{mnt_point}") if logger
begin
raw_disconnect(mnt_point)
rescue => err
# Ignore mount point not found/mounted messages
unless err.message =~ /not found|mounted/
msg = "[#{err.class.name}] [#{err.message}], disconnecting mount point: #{@mnt_point}"
logger.error("#{log_header} #{msg}") if logger
raise
end
end
FileUtils.rmdir(mnt_point) if File.exist?(mnt_point)
logger.info("#{log_header} Disconnecting mount point: #{mnt_point}...Complete") if logger
end
def active?
!@mnt_point.nil?
end
def reconnect!
disconnect
connect
end
def with_test_file(&_block)
raise "requires a block" unless block_given?
file = '/tmp/miq_verify_test_file'
begin
`echo "testing" > #{file}`
yield file
ensure
FileUtils.rm(file, :force => true)
end
end
def verify
log_header = "MIQ(#{self.class.name}-verify)"
logger.info("#{log_header} [#{@settings[:uri]}]...")
res = true
begin
connect
relpath = File.join(@mnt_point, relative_to_mount(@settings[:uri]))
test_path = 'miqverify/test'
to = File.join(test_path, 'test_file')
fq_file_path = File.join(relpath, to)
current_test = "create nested directories"
logger.info("#{log_header} [#{@settings[:uri]}] Testing #{current_test}...")
FileUtils.mkdir_p(File.dirname(fq_file_path))
logger.info("#{log_header} [#{@settings[:uri]}] Testing #{current_test}...complete")
with_test_file do |from|
current_test = "copy file"
logger.info("#{log_header} [#{@settings[:uri]}] Testing #{current_test}...")
FileUtils.cp(from, fq_file_path)
logger.info("#{log_header} [#{@settings[:uri]}] Testing #{current_test}...complete")
end
current_test = "delete file"
logger.info("#{log_header} [#{@settings[:uri]}] Testing #{current_test}...")
FileUtils.rm(fq_file_path, :force => true)
logger.info("#{log_header} [#{@settings[:uri]}] Testing #{current_test}...complete")
current_test = "remove nested directories"
logger.info("#{log_header} [#{@settings[:uri]}] Testing #{current_test}...")
FileUtils.rmdir(File.dirname(fq_file_path))
FileUtils.rmdir(File.dirname(File.dirname(fq_file_path)))
logger.info("#{log_header} [#{@settings[:uri]}] Testing #{current_test}...complete")
rescue => err
logger.error("#{log_header} Verify [#{current_test}] failed with error [#{err.class.name}] [#{err}], [#{err.backtrace[0]}]")
res = false, err.to_s
else
res = true, ""
ensure
disconnect
end
logger.info("#{log_header} [#{@settings[:uri]}]...result: [#{res.first}]")
res
end
def add(*upload_args)
dest_uri = nil
log_header = "MIQ(#{self.class.name}-add)"
# Don't think this log line is possible when using MiqFileStorage::Interface
#
# logger.info("#{log_header} Source: [#{source}], Destination: [#{dest_uri}]...")
begin
reconnect!
dest_uri = super
rescue => err
msg = "Adding [#{source_for_log}] to [#{remote_file_path}], failed due to error: '#{err.message}'"
logger.error("#{log_header} #{msg}")
raise
ensure
disconnect
end
logger.info("#{log_header} File URI added: [#{remote_file_path}] complete")
dest_uri
end
def upload_single(dest_uri)
log_header = "MIQ(#{self.class.name}-upload_single)"
relpath = uri_to_local_path(dest_uri)
if File.exist?(relpath)
logger.info("#{log_header} Skipping add since URI: [#{dest_uri}] already exists")
return dest_uri
end
logger.info("#{log_header} Copying file [#{source_for_log}] to [#{dest_uri}]...")
IO.copy_stream(source_input, relpath, byte_count)
logger.info("#{log_header} Copying file [#{source_for_log}] to [#{dest_uri}] complete")
dest_uri
end
def download_single(remote_file, local_file)
log_header = "MIQ(#{self.class.name}-download)"
logger.info("#{log_header} Target: [#{local_file}], Remote file: [#{remote_file}]...")
begin
reconnect!
relpath = uri_to_local_path(remote_file)
unless File.exist?(relpath)
logger.warn("#{log_header} Remote file: [#{remote_file}] does not exist!")
return
end
logger.info("#{log_header} Copying file [#{relpath}] to [#{local_file}]...")
IO.copy_stream(relpath, local_file, byte_count)
logger.info("#{log_header} Copying file [#{relpath}] to [#{local_file}] complete")
rescue => err
msg = "Downloading [#{remote_file}] to [#{local_file}], failed due to error: '#{err.message}'"
logger.error("#{log_header} #{msg}")
raise
ensure
disconnect
end
logger.info("#{log_header} Download File: [#{remote_file}] complete")
local_file
end
def log_uri_still_configured?(log_uri)
# Only remove the log file if the current depot @settings are based on the same base URI as the log_uri to be removed
return false if log_uri.nil? || @settings[:uri].nil?
scheme, userinfo, host, port, registry, share, opaque, query, fragment = URI.split(URI::DEFAULT_PARSER.escape(@settings[:uri]))
scheme_log, userinfo_log, host_log, port_log, registry_log, share_log, opaque_log, query_log, fragment_log = URI.split(URI::DEFAULT_PARSER.escape(log_uri))
return false if scheme != scheme_log
return false if host != host_log
# Since the depot URI is a base URI, remove all the directories in the log_uri from the base URI and check for empty?
return false unless (share.split("/") - share_log.split("/")).empty?
true
end
def remove(log_uri)
log_header = "MIQ(#{self.class.name}-remove)"
unless self.log_uri_still_configured?(log_uri)
logger.info("#{log_header} Skipping remove because log URI: [#{log_uri}] does not originate from the currently configured base URI: [#{@settings[:uri]}]")
return
end
relpath = nil
begin
# Samba has issues mount directly in the directory of the file so mount on the parent directory
@settings.merge!(:uri => File.dirname(File.dirname(log_uri)))
reconnect!
relpath = File.join(@mnt_point, relative_to_mount(log_uri))
# path is now /temp/default_1/EVM_1/Archive_default_1_EVM_1_20091016_193633_20091016_204855.zip, trim the share and join with the mount point
# /mnt/miq_1258754934/default_1/EVM_1/Archive_default_1_EVM_1_20091016_193633_20091016_204855.zip
# relpath = File.join(@mnt_point, path.split('/')[2..-1] )
logger.info("#{log_header} URI: [#{log_uri}] using relative path: [#{relpath}] and mount path: [#{@mount_path}]...")
unless File.exist?(relpath)
logger.info("#{log_header} Skipping since URI: [#{log_uri}] with relative path: [#{relpath}] does not exist")
return log_uri
end
logger.info("#{log_header} Deleting [#{relpath}] on [#{log_uri}]...")
FileUtils.rm_rf(relpath)
logger.info("#{log_header} Deleting [#{relpath}] on [#{log_uri}]...complete")
rescue NoSuchFileOrDirectory => err
logger.warn("#{log_header} No such file or directory to delete: [#{log_uri}]")
rescue => err
msg = "Deleting [#{relpath}] on [#{log_uri}], failed due to err '#{err.message}'"
logger.error("#{log_header} #{msg}")
raise
ensure
disconnect
end
logger.info("#{log_header} URI: [#{log_uri}]...complete")
log_uri
end
def uri_to_local_path(remote_file)
File.join(@mnt_point, relative_to_mount(remote_file))
end
def local_path_to_uri(local_path)
relative_path = Pathname.new(local_path).relative_path_from(Pathname.new(@mnt_point)).to_s
File.join(@settings[:uri], relative_path)
end
#
# These methods require an existing connection
#
def glob(pattern)
with_mounted_exception_handling do
Dir.glob("#{mount_root}/#{pattern}").collect { |path| (path.split("/") - mount_root.split("/")).join("/") }
end
end
def mkdir(path)
with_mounted_exception_handling do
log_header = "MIQ(#{self.class.name}-mkdir)"
new_path = uri_to_local_path(path)
logger.info("#{log_header} Building relative path: [#{new_path}]...")
FileUtils.mkdir_p(new_path)
logger.info("#{log_header} Building relative path: [#{new_path}]...complete")
end
end
def stat(file)
with_mounted_exception_handling do
File.stat("#{mount_root}/#{file}")
end
end
def read(file)
with_mounted_exception_handling do
File.read("#{mount_root}/#{file}")
end
end
def write(file, contents)
with_mounted_exception_handling do
mkdir(File.dirname(file))
open(file, "w") { |fd| fd.write(contents) }
end
end
def delete(file_or_directory)
with_mounted_exception_handling do
FileUtils.rm_rf("#{mount_root}/#{file_or_directory}")
end
end
def open(*args, &block)
with_mounted_exception_handling do
args[0] = "#{mount_root}/#{args[0]}"
File.open(*args, &block)
end
end
def file?(file)
with_mounted_exception_handling do
File.file?("#{mount_root}/#{file}")
end
end
protected
def mount_root
@mnt_point
end
def relative_to_mount(uri)
log_header = "MIQ(#{self.class.name}-relative_to_mount)"
logger.info("#{log_header} mount point [#{@mount_path}], uri: [#{uri}]...")
scheme, userinfo, host, port, registry, path, opaque, query, fragment = URI.split(URI::DEFAULT_PARSER.escape(uri))
# Replace any encoded spaces back into spaces since the mount commands accepts quoted spaces
path.gsub!('%20', ' ')
raise "path: #{path} or mount_path #{@mount_path} is blank" if path.nil? || @mount_path.nil? || path.empty? || @mount_path.empty?
res = (path.split("/") - @mount_path.split("/")).join("/")
logger.info("#{log_header} mount point [#{@mount_path}], uri: [#{uri}]...relative: [#{res}]")
res
end
def with_mounted_exception_handling
yield
rescue => err
err.message.gsub!(@mnt_point, @settings[:uri])
raise
end
def self.raw_disconnect(mnt_point)
case Sys::Platform::IMPL
when :macosx
sudo_umount(mnt_point)
when :linux
umount(mnt_point)
else
raise "platform not supported"
end
end
private
def settings_read_only?
@settings[:read_only] == true
end
def settings_mount_point
return nil if @settings[:mount_point].blank? # Check if settings contains the mount_point to use
FileUtils.mkdir_p(@settings[:mount_point]).first
end
def source_for_log
if @input_writer
"<STREAMED_FROM_CMD>"
elsif @source_input
@source_input.path
else
"<NOT_READY>"
end
end
end