train 0.28.0 → 0.29.0

Sign up to get free protection for your applications and to get access to all the features.
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