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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -2
- data/lib/train/extras.rb +0 -5
- data/lib/train/extras/os_common.rb +1 -1
- data/lib/train/extras/os_detect_unix.rb +6 -0
- data/lib/train/{extras/file_common.rb → file.rb} +65 -92
- data/lib/train/file/local.rb +70 -0
- data/lib/train/file/local/unix.rb +77 -0
- data/lib/train/file/local/windows.rb +63 -0
- data/lib/train/file/remote.rb +28 -0
- data/lib/train/file/remote/aix.rb +21 -0
- data/lib/train/file/remote/linux.rb +19 -0
- data/lib/train/file/remote/qnx.rb +41 -0
- data/lib/train/file/remote/unix.rb +110 -0
- data/lib/train/file/remote/windows.rb +94 -0
- data/lib/train/plugins/base_connection.rb +2 -1
- data/lib/train/transports/docker.rb +8 -1
- data/lib/train/transports/local.rb +6 -2
- data/lib/train/transports/mock.rb +7 -6
- data/lib/train/transports/ssh.rb +1 -2
- data/lib/train/transports/ssh_connection.rb +24 -4
- data/lib/train/transports/winrm_connection.rb +11 -5
- data/lib/train/version.rb +1 -1
- data/test/integration/tests/path_block_device_test.rb +2 -2
- data/test/integration/tests/path_character_device_test.rb +2 -2
- data/test/integration/tests/path_file_test.rb +2 -2
- data/test/integration/tests/path_folder_test.rb +5 -5
- data/test/integration/tests/path_missing_test.rb +0 -1
- data/test/integration/tests/path_pipe_test.rb +2 -3
- data/test/integration/tests/path_symlink_test.rb +2 -2
- data/test/unit/extras/os_detect_linux_test.rb +3 -3
- data/test/unit/extras/os_detect_windows_test.rb +1 -1
- data/test/unit/file/local/unix_test.rb +112 -0
- data/test/unit/file/local/windows_test.rb +41 -0
- data/test/unit/file/local_test.rb +110 -0
- data/test/unit/{extras/linux_file_test.rb → file/remote/linux_test.rb} +7 -7
- data/test/unit/file/remote/unix_test.rb +44 -0
- data/test/unit/file/remote_test.rb +62 -0
- data/test/unit/file_test.rb +156 -0
- data/test/unit/plugins/transport_test.rb +1 -1
- data/test/unit/transports/mock_test.rb +3 -3
- data/test/windows/local_test.rb +106 -0
- data/test/windows/winrm_test.rb +125 -0
- metadata +26 -16
- data/lib/train/extras/file_aix.rb +0 -20
- data/lib/train/extras/file_linux.rb +0 -16
- data/lib/train/extras/file_unix.rb +0 -79
- data/lib/train/extras/file_windows.rb +0 -100
- data/lib/train/transports/local_file.rb +0 -98
- data/test/unit/extras/file_common_test.rb +0 -180
- data/test/unit/extras/windows_file_test.rb +0 -44
- 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 #
|
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]
|
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] ||=
|
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
|
-
|
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
|
-
|
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 <
|
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::
|
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::
|
146
|
+
Train::File::DATA_FIELDS.each do |m|
|
146
147
|
attr_accessor m.tr('?', '').to_sym
|
147
148
|
next unless m.include?('?')
|
148
149
|
|
data/lib/train/transports/ssh.rb
CHANGED
@@ -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
|
-
'
|
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
|
-
|
65
|
+
Train::File::Remote::Aix.new(self, path)
|
65
66
|
elsif os.solaris?
|
66
|
-
|
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
|
-
|
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
|
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
|