From e548fcfa753afec2ab0490f0e921713facc635da Mon Sep 17 00:00:00 2001 From: Ted Wang Date: Wed, 23 Aug 2017 11:46:23 -0500 Subject: [PATCH] Unify file handling for local and remote transports (#189) Local and remote transports expose different file interfaces. This is because local transport uses LinuxFile with some methods overriden with Ruby's File calls whereas remote transport uses the appropriate file classes from Train::Extras. This causes problems when users are running locally on Windows because some LinuxFile implementations are not appropriate there. There are two potential compat breaks with this change: 1. pw_username is no longer provided for local transport 2. pw_groupname is no longer provided for local transport These are not available for remote transport, so this doesn't seem like it should be a problem for clients. Signed-off-by: Ted Wang --- appveyor.yml | 1 + lib/train/transports/local.rb | 12 +- lib/train/transports/local_file.rb | 90 ------------ test/unit/transports/local_file_test.rb | 184 ------------------------ test/windows/local_test.rb | 68 +++++++++ test/windows/winrm_test.rb | 68 +++++++++ 6 files changed, 147 insertions(+), 276 deletions(-) delete mode 100644 lib/train/transports/local_file.rb delete mode 100644 test/unit/transports/local_file_test.rb diff --git a/appveyor.yml b/appveyor.yml index 433fcfc8..2d165194 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -38,6 +38,7 @@ install: - ps: winrm create winrm/config/Listener?Address=*+Transport=HTTPS "@{Hostname=`"localhost`";CertificateThumbprint=`"$($env:winrm_cert)`"}" - ps: $env:PATH="C:\Ruby$env:ruby_version\bin;$env:PATH" - ps: Write-Host $env:PATH + - ps: Set-Content -Value "hello world" -NoNewLine -Path C:\train_test_file - ruby --version - gem --version - appveyor DownloadFile -Url %bundler_url% -FileName bundler.gem diff --git a/lib/train/transports/local.rb b/lib/train/transports/local.rb index caf068a0..37bf8e69 100644 --- a/lib/train/transports/local.rb +++ b/lib/train/transports/local.rb @@ -17,7 +17,6 @@ def connection(_ = nil) end class Connection < BaseConnection - require 'train/transports/local_file' require 'train/transports/local_os' def initialize(options) @@ -40,7 +39,16 @@ def os end def file(path) - @files[path] ||= File.new(self, path) + @files[path] ||= \ + if os.aix? + AixFile.new(self, path) + elsif os.solaris? + UnixFile.new(self, path) + elsif os.windows? + WindowsFile.new(self, path) + else + LinuxFile.new(self, path) + end end def login_command diff --git a/lib/train/transports/local_file.rb b/lib/train/transports/local_file.rb deleted file mode 100644 index 67203430..00000000 --- a/lib/train/transports/local_file.rb +++ /dev/null @@ -1,90 +0,0 @@ -# encoding: utf-8 -# -# author: Dominik Richter -# author: Christoph Hartmann - -require 'train/extras' - -class Train::Transports::Local::Connection - class File < Train::Extras::LinuxFile - def content - @content ||= ::File.read(@path, encoding: 'UTF-8') - rescue StandardError => _ - nil - end - - %w{ - exist? file? socket? directory? symlink? pipe? - }.each do |m| - define_method m.to_sym do - ::File.method(m.to_sym).call(@path) - end - end - - def link_path - return nil unless symlink? - begin - @link_path ||= ::File.realpath(@path) - rescue Errno::ELOOP => _ - # Leave it blank on symbolic loop, same as readlink - @link_path = '' - end - end - - def block_device? - ::File.blockdev?(@path) - end - - def character_device? - ::File.chardev?(@path) - end - - private - - def pw_username(uid) - Etc.getpwuid(uid).name - rescue ArgumentError => _ - nil - end - - def pw_groupname(gid) - Etc.getgrgid(gid).name - rescue ArgumentError => _ - nil - end - - def stat - return @stat if defined? @stat - - begin - file_stat = - if @follow_symlink - ::File.stat(@path) - else - ::File.lstat(@path) - end - rescue StandardError => _err - return @stat = {} - end - - @stat = { - type: Train::Extras::Stat.find_type(file_stat.mode), - mode: file_stat.mode & 07777, - mtime: file_stat.mtime.to_i, - size: file_stat.size, - owner: pw_username(file_stat.uid), - uid: file_stat.uid, - group: pw_groupname(file_stat.gid), - gid: file_stat.gid, - } - - lstat = @follow_symlink ? ' -L' : '' - res = @backend.run_command("stat#{lstat} #{@spath} 2>/dev/null --printf '%C'") - if res.exit_status == 0 && !res.stdout.empty? && res.stdout != '?' - @stat[:selinux_label] = res.stdout.strip - end - - @stat - end - end -end diff --git a/test/unit/transports/local_file_test.rb b/test/unit/transports/local_file_test.rb deleted file mode 100644 index 12efbeba..00000000 --- a/test/unit/transports/local_file_test.rb +++ /dev/null @@ -1,184 +0,0 @@ -# encoding: utf-8 -# -# author: Dominik Richter -# author: Christoph Hartmann - -require 'helper' -require 'train/transports/local' - -describe 'local file transport' do - let(:transport) { Train::Transports::Local.new } - let(:connection) { transport.connection } - - it 'gets file contents' do - res = rand.to_s - File.stub :read, res do - connection.file(rand.to_s).content.must_equal(res) - end - end - - it 'checks for file existance' do - File.stub :exist?, true do - connection.file(rand.to_s).exist?.must_equal(true) - end - end - - { - exist?: :exist?, - file?: :file?, - socket?: :socket?, - directory?: :directory?, - symlink?: :symlink?, - pipe?: :pipe?, - character_device?: :chardev?, - block_device?: :blockdev?, - }.each do |method, file_method| - it "checks if file is a #{method}" do - File.stub file_method.to_sym, true do - connection.file(rand.to_s).method(method.to_sym).call.must_equal(true) - end - end - end - - it 'checks a file\'s link path' do - out = rand.to_s - File.stub :realpath, out do - File.stub :symlink?, true do - connection.file(rand.to_s).link_path.must_equal out - end - end - end - - describe 'file metadata' do - let(:stat) { Struct.new(:mode, :size, :mtime, :uid, :gid) } - let(:uid) { rand } - let(:gid) { rand } - let(:statres) { stat.new(00140755, rand, (rand*100).to_i, uid, gid) } - - def meta_stub(method, param, &block) - pwres = Struct.new(:name) - Etc.stub :getpwuid, pwres.new('owner') do - Etc.stub :getgrgid, pwres.new('group') do - File.stub method, param do; yield; end - end - end - end - - it 'recognizes type' do - meta_stub :stat, statres do - connection.file(rand.to_s).type.must_equal :socket - end - end - - it 'recognizes mode' do - meta_stub :stat, statres do - connection.file(rand.to_s).mode.must_equal 00755 - end - end - - it 'recognizes mtime' do - meta_stub :stat, statres do - connection.file(rand.to_s).mtime.must_equal statres.mtime - end - end - - it 'recognizes size' do - meta_stub :stat, statres do - connection.file(rand.to_s).size.must_equal statres.size - end - end - - it 'recognizes owner' do - meta_stub :stat, statres do - connection.file(rand.to_s).owner.must_equal 'owner' - end - end - - it 'recognizes uid' do - meta_stub :stat, statres do - connection.file(rand.to_s).uid.must_equal uid - end - end - - it 'recognizes group' do - meta_stub :stat, statres do - connection.file(rand.to_s).group.must_equal 'group' - end - end - - it 'recognizes gid' do - meta_stub :stat, statres do - connection.file(rand.to_s).gid.must_equal gid - end - end - - it 'recognizes selinux label' do - meta_stub :stat, statres do - label = rand.to_s - res = Train::Extras::CommandResult.new(label, nil, 0) - connection.stub :run_command, res do - connection.file(rand.to_s).selinux_label.must_equal label - end - end - end - - it 'recognizes source type' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.type.must_equal :socket - end - end - - it 'recognizes source mode' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.mode.must_equal 00755 - end - end - - it 'recognizes source mtime' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.mtime.must_equal statres.mtime - end - end - - it 'recognizes source size' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.size.must_equal statres.size - end - end - - it 'recognizes source owner' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.owner.must_equal 'owner' - end - end - - it 'recognizes source uid' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.uid.must_equal uid - end - end - - it 'recognizes source owner' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.owner.must_equal 'owner' - end - end - - it 'recognizes source gid' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.gid.must_equal gid - end - end - - it 'recognizes source selinux label' do - meta_stub :lstat, statres do - label = rand.to_s - res = Train::Extras::CommandResult.new(label, nil, 0) - connection.stub :run_command, res do - connection.file(rand.to_s).source.selinux_label.must_equal label - end - end - end - end - -end diff --git a/test/windows/local_test.rb b/test/windows/local_test.rb index 30c903e4..f97d85b0 100644 --- a/test/windows/local_test.rb +++ b/test/windows/local_test.rb @@ -39,6 +39,74 @@ cmd.stderr.must_equal '' end + describe 'verify file' do + let(:file) { conn.file('C:\\train_test_file') } + + it 'exists' do + file.exist?.must_equal(true) + end + + it 'is a file' do + file.file?.must_equal(true) + end + + it 'has type :file' do + file.type.must_equal(:file) + end + + it 'has content' do + # TODO: this shouldn't include newlines that aren't in the original file + file.content.must_equal("hello world\r\n\r\n") + end + + it 'has owner name' do + file.owner.wont_be_nil + end + + it 'has no group name' do + file.group.must_be_nil + end + + it 'has no mode' do + file.mode.must_be_nil + end + + it 'has an md5sum' do + file.md5sum.wont_be_nil + end + + it 'has an sha256sum' do + file.sha256sum.wont_be_nil + end + + it 'has no modified time' do + file.mtime.must_be_nil + end + + it 'has no size' do + # TODO: this really ought to be implemented + file.size.must_be_nil + end + + it 'has no selinux label handling' do + file.selinux_label.must_be_nil + end + + it 'has product_version' do + file.product_version.wont_be_nil + end + + it 'has file_version' do + file.file_version.wont_be_nil + end + + it 'provides a json representation' do + j = file.to_json + j.must_be_kind_of Hash + j['type'].must_equal :file + end + end + after do # close the connection conn.close diff --git a/test/windows/winrm_test.rb b/test/windows/winrm_test.rb index 37140b37..f73e5cbe 100644 --- a/test/windows/winrm_test.rb +++ b/test/windows/winrm_test.rb @@ -45,6 +45,74 @@ cmd.stderr.must_equal '' end + describe 'verify file' do + let(:file) { conn.file('C:\\train_test_file') } + + it 'exists' do + file.exist?.must_equal(true) + end + + it 'is a file' do + file.file?.must_equal(true) + end + + it 'has type :file' do + file.type.must_equal(:file) + end + + it 'has content' do + # TODO: this shouldn't include newlines that aren't in the original file + file.content.must_equal("hello world\r\n") + end + + it 'has owner name' do + file.owner.wont_be_nil + end + + it 'has no group name' do + file.group.must_be_nil + end + + it 'has no mode' do + file.mode.must_be_nil + end + + it 'has an md5sum' do + file.md5sum.wont_be_nil + end + + it 'has an sha256sum' do + file.sha256sum.wont_be_nil + end + + it 'has no modified time' do + file.mtime.must_be_nil + end + + it 'has no size' do + # TODO: this really ought to be implemented + file.size.must_be_nil + end + + it 'has no selinux label handling' do + file.selinux_label.must_be_nil + end + + it 'has product_version' do + file.product_version.wont_be_nil + end + + it 'has file_version' do + file.file_version.wont_be_nil + end + + it 'provides a json representation' do + j = file.to_json + j.must_be_kind_of Hash + j['type'].must_equal :file + end + end + after do # close the connection conn.close