train 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -2
  3. data/lib/train/extras.rb +0 -5
  4. data/lib/train/extras/os_common.rb +1 -1
  5. data/lib/train/extras/os_detect_unix.rb +6 -0
  6. data/lib/train/{extras/file_common.rb → file.rb} +65 -92
  7. data/lib/train/file/local.rb +70 -0
  8. data/lib/train/file/local/unix.rb +77 -0
  9. data/lib/train/file/local/windows.rb +63 -0
  10. data/lib/train/file/remote.rb +28 -0
  11. data/lib/train/file/remote/aix.rb +21 -0
  12. data/lib/train/file/remote/linux.rb +19 -0
  13. data/lib/train/file/remote/qnx.rb +41 -0
  14. data/lib/train/file/remote/unix.rb +110 -0
  15. data/lib/train/file/remote/windows.rb +94 -0
  16. data/lib/train/plugins/base_connection.rb +2 -1
  17. data/lib/train/transports/docker.rb +8 -1
  18. data/lib/train/transports/local.rb +6 -2
  19. data/lib/train/transports/mock.rb +7 -6
  20. data/lib/train/transports/ssh.rb +1 -2
  21. data/lib/train/transports/ssh_connection.rb +24 -4
  22. data/lib/train/transports/winrm_connection.rb +11 -5
  23. data/lib/train/version.rb +1 -1
  24. data/test/integration/tests/path_block_device_test.rb +2 -2
  25. data/test/integration/tests/path_character_device_test.rb +2 -2
  26. data/test/integration/tests/path_file_test.rb +2 -2
  27. data/test/integration/tests/path_folder_test.rb +5 -5
  28. data/test/integration/tests/path_missing_test.rb +0 -1
  29. data/test/integration/tests/path_pipe_test.rb +2 -3
  30. data/test/integration/tests/path_symlink_test.rb +2 -2
  31. data/test/unit/extras/os_detect_linux_test.rb +3 -3
  32. data/test/unit/extras/os_detect_windows_test.rb +1 -1
  33. data/test/unit/file/local/unix_test.rb +112 -0
  34. data/test/unit/file/local/windows_test.rb +41 -0
  35. data/test/unit/file/local_test.rb +110 -0
  36. data/test/unit/{extras/linux_file_test.rb → file/remote/linux_test.rb} +7 -7
  37. data/test/unit/file/remote/unix_test.rb +44 -0
  38. data/test/unit/file/remote_test.rb +62 -0
  39. data/test/unit/file_test.rb +156 -0
  40. data/test/unit/plugins/transport_test.rb +1 -1
  41. data/test/unit/transports/mock_test.rb +3 -3
  42. data/test/windows/local_test.rb +106 -0
  43. data/test/windows/winrm_test.rb +125 -0
  44. metadata +26 -16
  45. data/lib/train/extras/file_aix.rb +0 -20
  46. data/lib/train/extras/file_linux.rb +0 -16
  47. data/lib/train/extras/file_unix.rb +0 -79
  48. data/lib/train/extras/file_windows.rb +0 -100
  49. data/lib/train/transports/local_file.rb +0 -98
  50. data/test/unit/extras/file_common_test.rb +0 -180
  51. data/test/unit/extras/windows_file_test.rb +0 -44
  52. data/test/unit/transports/local_file_test.rb +0 -202
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+
3
+ module Train
4
+ class File
5
+ class Remote < Train::File
6
+ def basename(suffix = nil, sep = '/')
7
+ fail 'Not yet supported: Suffix in file.basename' unless suffix.nil?
8
+ @basename ||= detect_filename(path, sep || '/')
9
+ end
10
+
11
+ def stat
12
+ return @stat if defined?(@stat)
13
+ @stat = Train::Extras::Stat.stat(@spath, @backend, @follow_symlink)
14
+ end
15
+
16
+ # helper methods provided to any implementing class
17
+ private
18
+
19
+ def detect_filename(path, sep)
20
+ idx = path.rindex(sep)
21
+ return path if idx.nil?
22
+ idx += 1
23
+ return detect_filename(path[0..-2], sep) if idx == path.length
24
+ path[idx..-1]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ require 'train/file/remote/unix'
4
+
5
+ module Train
6
+ class File
7
+ class Remote
8
+ class Aix < Train::File::Remote::Unix
9
+ def link_path
10
+ return nil unless symlink?
11
+ @link_path ||=
12
+ @backend.run_command("perl -e 'print readlink shift' #{@spath}").stdout.chomp
13
+ end
14
+
15
+ def mounted
16
+ @mounted ||= @backend.run_command("lsfs -c #{@spath}")
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ require 'train/file/remote/unix'
4
+
5
+ module Train
6
+ class File
7
+ class Remote
8
+ class Linux < Train::File::Remote::Unix
9
+ def content
10
+ return @content if defined?(@content)
11
+ @content = @backend.run_command("cat #{@path} || echo -n").stdout
12
+ return @content unless @content.empty?
13
+ @content = nil if directory? or size.nil? or size > 0
14
+ @content
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+ #
3
+ # author: Christoph Hartmann
4
+ # author: Dominik Richter
5
+
6
+ require 'train/file/remote/unix'
7
+
8
+ module Train
9
+ class File
10
+ class Remote
11
+ class Aix < Train::File::Remote::Unix
12
+ def content
13
+ cat = 'cat'
14
+ cat = '/proc/boot/cat' if @backend.os[:release].to_i >= 7
15
+ @content ||= case
16
+ when !exist?
17
+ nil
18
+ else
19
+ @backend.run_command("#{cat} #{@spath}").stdout || ''
20
+ end
21
+ end
22
+
23
+ def type
24
+ if @backend.run_command("file #{@spath}").stdout.include?('directory')
25
+ return :directory
26
+ else
27
+ return :file
28
+ end
29
+ end
30
+
31
+ %w{
32
+ mode owner group uid gid mtime size selinux_label link_path mounted stat
33
+ }.each do |field|
34
+ define_method field.to_sym do
35
+ fail NotImplementedError, "QNX does not implement the #{m}() method yet."
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,110 @@
1
+ # encoding: utf-8
2
+
3
+ require 'shellwords'
4
+
5
+ module Train
6
+ class File
7
+ class Remote
8
+ class Unix < Train::File::Remote
9
+ attr_reader :path
10
+
11
+ def sanitize_filename(path)
12
+ @spath = Shellwords.escape(path) || @path
13
+ end
14
+
15
+ def content
16
+ @content ||=
17
+ if !exist? || directory?
18
+ nil
19
+ elsif size.nil? || size.zero?
20
+ ''
21
+ else
22
+ @backend.run_command("cat #{@spath}").stdout || ''
23
+ end
24
+ end
25
+
26
+ def exist?
27
+ @exist ||= (
28
+ f = @follow_symlink ? '' : " || test -L #{@spath}"
29
+ @backend.run_command("test -e #{@spath}"+f)
30
+ .exit_status == 0
31
+ )
32
+ end
33
+
34
+ def mounted?
35
+ !mounted.nil? && !mounted.stdout.nil? && !mounted.stdout.empty?
36
+ end
37
+
38
+ def mounted
39
+ @mounted ||=
40
+ @backend.run_command("mount | grep -- ' on #{@spath} '")
41
+ end
42
+
43
+ %w{
44
+ type mode owner group uid gid mtime size selinux_label
45
+ }.each do |field|
46
+ define_method field.to_sym do
47
+ stat[field.to_sym]
48
+ end
49
+ end
50
+
51
+ def mode?(sth)
52
+ mode == sth
53
+ end
54
+
55
+ def grouped_into?(sth)
56
+ group == sth
57
+ end
58
+
59
+ def linked_to?(dst)
60
+ link_path == dst
61
+ end
62
+
63
+ def link_path
64
+ symlink? ? path : nil
65
+ end
66
+
67
+ def unix_mode_mask(owner, type)
68
+ o = UNIX_MODE_OWNERS[owner.to_sym]
69
+ return nil if o.nil?
70
+
71
+ t = UNIX_MODE_TYPES[type.to_sym]
72
+ return nil if t.nil?
73
+
74
+ t & o
75
+ end
76
+
77
+ def path
78
+ return @path unless @follow_symlink && symlink?
79
+ @link_path ||= read_target_path
80
+ end
81
+
82
+ private
83
+
84
+ # Returns full path of a symlink target(real dest) or '' on symlink loop
85
+ def read_target_path
86
+ full_path = @backend.run_command("readlink -n #{@spath} -f").stdout
87
+ # Needed for some OSes like OSX that returns relative path
88
+ # when the link and target are in the same directory
89
+ if !full_path.start_with?('/') && full_path != ''
90
+ full_path = ::File.expand_path("../#{full_path}", @spath)
91
+ end
92
+ full_path
93
+ end
94
+
95
+ UNIX_MODE_OWNERS = {
96
+ all: 00777,
97
+ owner: 00700,
98
+ group: 00070,
99
+ other: 00007,
100
+ }.freeze
101
+
102
+ UNIX_MODE_TYPES = {
103
+ r: 00444,
104
+ w: 00222,
105
+ x: 00111,
106
+ }.freeze
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,94 @@
1
+ # encoding: utf-8
2
+
3
+ module Train
4
+ class File
5
+ class Remote
6
+ class Windows < Train::File::Remote
7
+ attr_reader :path
8
+ # Ensures we do not use invalid characters for file names
9
+ # @see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#naming_conventions
10
+ def sanitize_filename(path)
11
+ return if path.nil?
12
+ # we do not filter :, backslash and forward slash, since they are part of the path
13
+ @spath = path.gsub(/[<>"|?*]/, '')
14
+ end
15
+
16
+ def basename(suffix = nil, sep = '\\')
17
+ super(suffix, sep)
18
+ end
19
+
20
+ def content
21
+ return @content if defined?(@content)
22
+ @content = @backend.run_command("Get-Content(\"#{@spath}\") | Out-String").stdout
23
+ return @content unless @content.empty?
24
+ @content = nil if directory? # or size.nil? or size > 0
25
+ @content
26
+ end
27
+
28
+ def exist?
29
+ return @exist if defined?(@exist)
30
+ @exist = @backend.run_command(
31
+ "(Test-Path -Path \"#{@spath}\").ToString()").stdout.chomp == 'True'
32
+ end
33
+
34
+ def owner
35
+ owner = @backend.run_command(
36
+ "Get-Acl \"#{@spath}\" | select -expand Owner").stdout.strip
37
+ return if owner.empty?
38
+ owner
39
+ end
40
+
41
+ def type
42
+ if attributes.include?('Archive')
43
+ return :file
44
+ elsif attributes.include?('ReparsePoint')
45
+ return :symlink
46
+ elsif attributes.include?('Directory')
47
+ return :directory
48
+ end
49
+ :unknown
50
+ end
51
+
52
+ def size
53
+ if file?
54
+ @backend.run_command("((Get-Item '#{@spath}').Length)").stdout.strip.to_i
55
+ end
56
+ end
57
+
58
+ def product_version
59
+ @product_version ||= @backend.run_command(
60
+ "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"#{@spath}\").ProductVersion").stdout.chomp
61
+ end
62
+
63
+ def file_version
64
+ @file_version ||= @backend.run_command(
65
+ "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"#{@spath}\").FileVersion").stdout.chomp
66
+ end
67
+
68
+ %w{
69
+ mode group uid gid mtime selinux_label
70
+ }.each do |field|
71
+ define_method field.to_sym do
72
+ nil
73
+ end
74
+ end
75
+
76
+ def link_path
77
+ nil
78
+ end
79
+
80
+ def mounted
81
+ nil
82
+ end
83
+
84
+ private
85
+
86
+ def attributes
87
+ return @attributes if defined?(@attributes)
88
+ @attributes = @backend.run_command(
89
+ "(Get-ItemProperty -Path \"#{@spath}\").attributes.ToString()").stdout.chomp.split(/\s*,\s*/)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -6,6 +6,7 @@
6
6
 
7
7
  require 'train/errors'
8
8
  require 'train/extras'
9
+ require 'train/file'
9
10
  require 'logger'
10
11
 
11
12
  class Train::Plugins::Transport
@@ -78,7 +79,7 @@ class Train::Plugins::Transport
78
79
  #
79
80
  # @return [LoginCommand] array of command line tokens
80
81
  def login_command
81
- fail Train::ClientError, "#{self.class} does not implement #run_command()"
82
+ fail Train::ClientError, "#{self.class} does not implement #login_command()"
82
83
  end
83
84
 
84
85
  # Block and return only when the remote host is prepared and ready to
@@ -74,7 +74,14 @@ class Train::Transports::Docker
74
74
  end
75
75
 
76
76
  def file(path)
77
- @files[path] ||= LinuxFile.new(self, path)
77
+ @files[path] ||=\
78
+ if os.aix?
79
+ Train::File::Remote::Aix.new(self, path)
80
+ elsif os.solaris?
81
+ Train::File::Remote::Unix.new(self, path)
82
+ else
83
+ Train::File::Remote::Linux.new(self, path)
84
+ end
78
85
  end
79
86
 
80
87
  def run_command(cmd)
@@ -17,7 +17,6 @@ module Train::Transports
17
17
  end
18
18
 
19
19
  class Connection < BaseConnection
20
- require 'train/transports/local_file'
21
20
  require 'train/transports/local_os'
22
21
 
23
22
  def initialize(options)
@@ -40,7 +39,12 @@ module Train::Transports
40
39
  end
41
40
 
42
41
  def file(path)
43
- @files[path] ||= File.new(self, path)
42
+ @files[path] ||= \
43
+ if os.windows?
44
+ Train::File::Local::Windows.new(self, path)
45
+ else
46
+ Train::File::Local::Unix.new(self, path)
47
+ end
44
48
  end
45
49
 
46
50
  def login_command
@@ -65,7 +65,7 @@ class Train::Transports::Mock
65
65
 
66
66
  def initialize(conf = nil)
67
67
  super(conf)
68
- @os = OS.new(self, family: 'unknown')
68
+ mock_os
69
69
  @commands = {}
70
70
  end
71
71
 
@@ -73,8 +73,9 @@ class Train::Transports::Mock
73
73
  'mock://'
74
74
  end
75
75
 
76
- def mock_os(value)
77
- @os = OS.new(self, value)
76
+ def mock_os(value = {})
77
+ os_params = { name: 'unknown', family: 'unknown', release: 'unknown', arch: 'unknown' }.merge(value)
78
+ @os = OS.new(self, os_params)
78
79
  end
79
80
 
80
81
  def mock_command(cmd, stdout = nil, stderr = nil, exit_status = 0)
@@ -129,20 +130,20 @@ class Train::Transports::Mock::Connection
129
130
  end
130
131
 
131
132
  class Train::Transports::Mock::Connection
132
- class File < FileCommon
133
+ class File < Train::File
133
134
  def self.from_json(json)
134
135
  res = new(json['backend'],
135
136
  json['path'],
136
137
  json['follow_symlink'])
137
138
  res.type = json['type']
138
- Train::Extras::FileCommon::DATA_FIELDS.each do |f|
139
+ Train::File::DATA_FIELDS.each do |f|
139
140
  m = (f.tr('?', '') + '=').to_sym
140
141
  res.method(m).call(json[f])
141
142
  end
142
143
  res
143
144
  end
144
145
 
145
- Train::Extras::FileCommon::DATA_FIELDS.each do |m|
146
+ Train::File::DATA_FIELDS.each do |m|
146
147
  attr_accessor m.tr('?', '').to_sym
147
148
  next unless m.include?('?')
148
149
 
@@ -97,8 +97,7 @@ module Train::Transports
97
97
  if options[:auth_methods] == ['none']
98
98
  if ssh_known_identities.empty?
99
99
  fail Train::ClientError,
100
- 'You must configure at least one authentication method for SSH:'\
101
- ' Agent, Key or Password.'
100
+ 'Your SSH Agent has no keys added, and you have not specified a password or a key file'
102
101
  else
103
102
  logger.debug('[SSH] Using Agent keys as no password or key file have been specified')
104
103
  options[:auth_methods].push('publickey')
@@ -39,6 +39,7 @@ class Train::Transports::SSH
39
39
  @connection_retries = @options.delete(:connection_retries)
40
40
  @connection_retry_sleep = @options.delete(:connection_retry_sleep)
41
41
  @max_wait_until_ready = @options.delete(:max_wait_until_ready)
42
+ @max_ssh_sessions = @options.delete(:max_ssh_connections) { 9 }
42
43
  @session = nil
43
44
  @transport_options = @options.delete(:transport_options)
44
45
  @cmd_wrapper = nil
@@ -61,11 +62,13 @@ class Train::Transports::SSH
61
62
  def file(path)
62
63
  @files[path] ||= \
63
64
  if os.aix?
64
- AixFile.new(self, path)
65
+ Train::File::Remote::Aix.new(self, path)
65
66
  elsif os.solaris?
66
- UnixFile.new(self, path)
67
+ Train::File::Remote::Unix.new(self, path)
68
+ elsif os[:name] == 'qnx'
69
+ Train::File::Remote::Qnx.new(self, path)
67
70
  else
68
- LinuxFile.new(self, path)
71
+ Train::File::Remote::Linux.new(self, path)
69
72
  end
70
73
  end
71
74
 
@@ -134,17 +137,34 @@ class Train::Transports::SSH
134
137
 
135
138
  # (see Base::Connection#upload)
136
139
  def upload(locals, remote)
140
+ waits = []
137
141
  Array(locals).each do |local|
138
142
  opts = File.directory?(local) ? { recursive: true } : {}
139
143
 
140
- session.scp.upload!(local, remote, opts) do |_ch, name, sent, total|
144
+ waits.push session.scp.upload(local, remote, opts) do |_ch, name, sent, total|
141
145
  logger.debug("Uploaded #{name} (#{total} bytes)") if sent == total
142
146
  end
147
+ waits.shift.wait while waits.length >= @max_ssh_sessions
143
148
  end
149
+ waits.each(&:wait)
144
150
  rescue Net::SSH::Exception => ex
145
151
  raise Train::Transports::SSHFailed, "SCP upload failed (#{ex.message})"
146
152
  end
147
153
 
154
+ def download(remotes, local)
155
+ waits = []
156
+ Array(remotes).map do |remote|
157
+ opts = file(remote).directory? ? { recursive: true } : {}
158
+ waits.push session.scp.download(remote, local, opts) do |_ch, name, recv, total|
159
+ logger.debug("Downloaded #{name} (#{total} bytes)") if recv == total
160
+ end
161
+ waits.shift.wait while waits.length >= @max_ssh_sessions
162
+ end
163
+ waits.each(&:wait)
164
+ rescue Net::SSH::Exception => ex
165
+ raise Train::Transports::SSHFailed, "SCP download failed (#{ex.message})"
166
+ end
167
+
148
168
  # (see Base::Connection#wait_until_ready)
149
169
  def wait_until_ready
150
170
  delay = 3