train 0.12.1
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 +7 -0
- data/.rubocop.yml +71 -0
- data/CHANGELOG.md +308 -0
- data/Gemfile +30 -0
- data/LICENSE +201 -0
- data/README.md +156 -0
- data/Rakefile +148 -0
- data/lib/train.rb +117 -0
- data/lib/train/errors.rb +23 -0
- data/lib/train/extras.rb +17 -0
- data/lib/train/extras/command_wrapper.rb +148 -0
- data/lib/train/extras/file_aix.rb +20 -0
- data/lib/train/extras/file_common.rb +161 -0
- data/lib/train/extras/file_linux.rb +16 -0
- data/lib/train/extras/file_unix.rb +79 -0
- data/lib/train/extras/file_windows.rb +91 -0
- data/lib/train/extras/linux_lsb.rb +60 -0
- data/lib/train/extras/os_common.rb +136 -0
- data/lib/train/extras/os_detect_darwin.rb +32 -0
- data/lib/train/extras/os_detect_linux.rb +148 -0
- data/lib/train/extras/os_detect_unix.rb +99 -0
- data/lib/train/extras/os_detect_windows.rb +57 -0
- data/lib/train/extras/stat.rb +133 -0
- data/lib/train/options.rb +80 -0
- data/lib/train/plugins.rb +40 -0
- data/lib/train/plugins/base_connection.rb +86 -0
- data/lib/train/plugins/transport.rb +49 -0
- data/lib/train/transports/docker.rb +103 -0
- data/lib/train/transports/local.rb +52 -0
- data/lib/train/transports/local_file.rb +90 -0
- data/lib/train/transports/local_os.rb +51 -0
- data/lib/train/transports/mock.rb +147 -0
- data/lib/train/transports/ssh.rb +163 -0
- data/lib/train/transports/ssh_connection.rb +225 -0
- data/lib/train/transports/winrm.rb +184 -0
- data/lib/train/transports/winrm_connection.rb +194 -0
- data/lib/train/version.rb +7 -0
- data/test/integration/.kitchen.yml +43 -0
- data/test/integration/Berksfile +3 -0
- data/test/integration/bootstrap.sh +17 -0
- data/test/integration/chefignore +1 -0
- data/test/integration/cookbooks/test/metadata.rb +1 -0
- data/test/integration/cookbooks/test/recipes/default.rb +100 -0
- data/test/integration/cookbooks/test/recipes/prep_files.rb +47 -0
- data/test/integration/docker_run.rb +153 -0
- data/test/integration/docker_test.rb +24 -0
- data/test/integration/docker_test_container.rb +24 -0
- data/test/integration/helper.rb +61 -0
- data/test/integration/sudo/customcommand.rb +15 -0
- data/test/integration/sudo/nopasswd.rb +16 -0
- data/test/integration/sudo/passwd.rb +21 -0
- data/test/integration/sudo/reqtty.rb +17 -0
- data/test/integration/sudo/run_as.rb +12 -0
- data/test/integration/test-travis-1.yaml +13 -0
- data/test/integration/test-travis-2.yaml +13 -0
- data/test/integration/test_local.rb +19 -0
- data/test/integration/test_ssh.rb +39 -0
- data/test/integration/tests/path_block_device_test.rb +74 -0
- data/test/integration/tests/path_character_device_test.rb +74 -0
- data/test/integration/tests/path_file_test.rb +79 -0
- data/test/integration/tests/path_folder_test.rb +90 -0
- data/test/integration/tests/path_missing_test.rb +77 -0
- data/test/integration/tests/path_pipe_test.rb +78 -0
- data/test/integration/tests/path_symlink_test.rb +95 -0
- data/test/integration/tests/run_command_test.rb +28 -0
- data/test/unit/extras/command_wrapper_test.rb +78 -0
- data/test/unit/extras/file_common_test.rb +180 -0
- data/test/unit/extras/linux_file_test.rb +167 -0
- data/test/unit/extras/os_common_test.rb +269 -0
- data/test/unit/extras/os_detect_linux_test.rb +189 -0
- data/test/unit/extras/os_detect_windows_test.rb +99 -0
- data/test/unit/extras/stat_test.rb +148 -0
- data/test/unit/extras/windows_file_test.rb +44 -0
- data/test/unit/helper.rb +7 -0
- data/test/unit/plugins/connection_test.rb +44 -0
- data/test/unit/plugins/transport_test.rb +111 -0
- data/test/unit/plugins_test.rb +22 -0
- data/test/unit/train_test.rb +156 -0
- data/test/unit/transports/local_file_test.rb +184 -0
- data/test/unit/transports/local_test.rb +87 -0
- data/test/unit/transports/mock_test.rb +87 -0
- data/test/unit/transports/ssh_test.rb +109 -0
- data/test/unit/version_test.rb +8 -0
- data/test/windows/local_test.rb +46 -0
- data/test/windows/winrm_test.rb +52 -0
- data/train.gemspec +38 -0
- metadata +295 -0
@@ -0,0 +1,103 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Dominik Richter
|
4
|
+
# Author:: Christoph Hartmann
|
5
|
+
|
6
|
+
require 'docker'
|
7
|
+
|
8
|
+
module Train::Transports
|
9
|
+
class Docker < Train.plugin(1)
|
10
|
+
name 'docker'
|
11
|
+
|
12
|
+
include_options Train::Extras::CommandWrapper
|
13
|
+
option :host, required: true
|
14
|
+
|
15
|
+
def connection(state = {}, &block)
|
16
|
+
opts = merge_options(options, state || {})
|
17
|
+
validate_options(opts)
|
18
|
+
|
19
|
+
if @connection && @connection_options == opts
|
20
|
+
reuse_connection(&block)
|
21
|
+
else
|
22
|
+
create_new_connection(opts, &block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Creates a new Docker connection instance and save it for potential future
|
29
|
+
# reuse.
|
30
|
+
#
|
31
|
+
# @param options [Hash] connection options
|
32
|
+
# @return [Docker::Connection] a Docker connection instance
|
33
|
+
# @api private
|
34
|
+
def create_new_connection(options, &block)
|
35
|
+
if @connection
|
36
|
+
logger.debug("[Docker] shutting previous connection #{@connection}")
|
37
|
+
@connection.close
|
38
|
+
end
|
39
|
+
|
40
|
+
@connection_options = options
|
41
|
+
@connection = Connection.new(options, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return the last saved Docker connection instance.
|
45
|
+
#
|
46
|
+
# @return [Docker::Connection] a Docker connection instance
|
47
|
+
# @api private
|
48
|
+
def reuse_connection
|
49
|
+
logger.debug("[Docker] reusing existing connection #{@connection}")
|
50
|
+
yield @connection if block_given?
|
51
|
+
@connection
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Train::Transports::Docker
|
57
|
+
class Connection < BaseConnection
|
58
|
+
def initialize(conf)
|
59
|
+
super(conf)
|
60
|
+
@id = options[:host]
|
61
|
+
@container = ::Docker::Container.get(@id) ||
|
62
|
+
fail("Can't find Docker container #{@id}")
|
63
|
+
@files = {}
|
64
|
+
@cmd_wrapper = nil
|
65
|
+
@cmd_wrapper = CommandWrapper.load(self, @options)
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def close
|
70
|
+
# nothing to do at the moment
|
71
|
+
end
|
72
|
+
|
73
|
+
def os
|
74
|
+
@os ||= OS.new(self)
|
75
|
+
end
|
76
|
+
|
77
|
+
def file(path)
|
78
|
+
@files[path] ||= LinuxFile.new(self, path)
|
79
|
+
end
|
80
|
+
|
81
|
+
def run_command(cmd)
|
82
|
+
cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?
|
83
|
+
stdout, stderr, exit_status = @container.exec(
|
84
|
+
[
|
85
|
+
'/bin/sh', '-c', cmd
|
86
|
+
])
|
87
|
+
CommandResult.new(stdout.join, stderr.join, exit_status)
|
88
|
+
rescue ::Docker::Error::DockerError => _
|
89
|
+
raise
|
90
|
+
rescue => _
|
91
|
+
# @TODO: differentiate any other error
|
92
|
+
raise
|
93
|
+
end
|
94
|
+
|
95
|
+
class OS < OSCommon
|
96
|
+
def initialize(backend)
|
97
|
+
# hardcoded to unix/linux for now, until other operating systems
|
98
|
+
# are supported
|
99
|
+
super(backend, { family: 'unix' })
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# author: Dominik Richter
|
4
|
+
# author: Christoph Hartmann
|
5
|
+
|
6
|
+
require 'train/plugins'
|
7
|
+
require 'mixlib/shellout'
|
8
|
+
|
9
|
+
module Train::Transports
|
10
|
+
class Local < Train.plugin(1)
|
11
|
+
name 'local'
|
12
|
+
|
13
|
+
include_options Train::Extras::CommandWrapper
|
14
|
+
|
15
|
+
autoload :File, 'train/transports/local_file'
|
16
|
+
autoload :OS, 'train/transports/local_os'
|
17
|
+
|
18
|
+
def connection(_ = nil)
|
19
|
+
@connection ||= Connection.new(@options)
|
20
|
+
end
|
21
|
+
|
22
|
+
class Connection < BaseConnection
|
23
|
+
def initialize(options)
|
24
|
+
super(options)
|
25
|
+
@files = {}
|
26
|
+
@cmd_wrapper = nil
|
27
|
+
@cmd_wrapper = CommandWrapper.load(self, options)
|
28
|
+
end
|
29
|
+
|
30
|
+
def run_command(cmd)
|
31
|
+
cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?
|
32
|
+
res = Mixlib::ShellOut.new(cmd)
|
33
|
+
res.run_command
|
34
|
+
CommandResult.new(res.stdout, res.stderr, res.exitstatus)
|
35
|
+
rescue Errno::ENOENT => _
|
36
|
+
CommandResult.new('', '', 1)
|
37
|
+
end
|
38
|
+
|
39
|
+
def os
|
40
|
+
@os ||= OS.new(self)
|
41
|
+
end
|
42
|
+
|
43
|
+
def file(path)
|
44
|
+
@files[path] ||= File.new(self, path)
|
45
|
+
end
|
46
|
+
|
47
|
+
def login_command
|
48
|
+
nil # none, open your shell
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# author: Dominik Richter
|
4
|
+
# author: Christoph Hartmann
|
5
|
+
|
6
|
+
require 'train/extras'
|
7
|
+
|
8
|
+
class Train::Transports::Local::Connection
|
9
|
+
class File < LinuxFile
|
10
|
+
def content
|
11
|
+
@content ||= ::File.read(@path, encoding: 'UTF-8')
|
12
|
+
rescue StandardError => _
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
%w{
|
17
|
+
exist? file? socket? directory? symlink? pipe?
|
18
|
+
}.each do |m|
|
19
|
+
define_method m.to_sym do
|
20
|
+
::File.method(m.to_sym).call(@path)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def link_path
|
25
|
+
return nil unless symlink?
|
26
|
+
begin
|
27
|
+
@link_path ||= ::File.realpath(@path)
|
28
|
+
rescue Errno::ELOOP => _
|
29
|
+
# Leave it blank on symbolic loop, same as readlink
|
30
|
+
@link_path = ''
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def block_device?
|
35
|
+
::File.blockdev?(@path)
|
36
|
+
end
|
37
|
+
|
38
|
+
def character_device?
|
39
|
+
::File.chardev?(@path)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def pw_username(uid)
|
45
|
+
Etc.getpwuid(uid).name
|
46
|
+
rescue ArgumentError => _
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def pw_groupname(gid)
|
51
|
+
Etc.getgrgid(gid).name
|
52
|
+
rescue ArgumentError => _
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def stat
|
57
|
+
return @stat if defined? @stat
|
58
|
+
|
59
|
+
begin
|
60
|
+
file_stat =
|
61
|
+
if @follow_symlink
|
62
|
+
::File.stat(@path)
|
63
|
+
else
|
64
|
+
::File.lstat(@path)
|
65
|
+
end
|
66
|
+
rescue StandardError => _err
|
67
|
+
return @stat = {}
|
68
|
+
end
|
69
|
+
|
70
|
+
@stat = {
|
71
|
+
type: Train::Extras::Stat.find_type(file_stat.mode),
|
72
|
+
mode: file_stat.mode & 00777,
|
73
|
+
mtime: file_stat.mtime.to_i,
|
74
|
+
size: file_stat.size,
|
75
|
+
owner: pw_username(file_stat.uid),
|
76
|
+
uid: file_stat.uid,
|
77
|
+
group: pw_groupname(file_stat.gid),
|
78
|
+
gid: file_stat.gid,
|
79
|
+
}
|
80
|
+
|
81
|
+
lstat = @follow_symlink ? ' -L' : ''
|
82
|
+
res = @backend.run_command("stat#{lstat} #{@spath} 2>/dev/null --printf '%C'")
|
83
|
+
if res.exit_status == 0 && !res.stdout.empty? && res.stdout != '?'
|
84
|
+
@stat[:selinux_label] = res.stdout.strip
|
85
|
+
end
|
86
|
+
|
87
|
+
@stat
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Dominik Richter
|
3
|
+
# author: Christoph Hartmann
|
4
|
+
#
|
5
|
+
# This is heavily based on:
|
6
|
+
#
|
7
|
+
# OHAI https://github.com/chef/ohai
|
8
|
+
# by Adam Jacob, Chef Software Inc
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'rbconfig'
|
12
|
+
|
13
|
+
class Train::Transports::Local
|
14
|
+
class OS < OSCommon
|
15
|
+
def initialize(backend)
|
16
|
+
super(backend, { family: detect_local_os })
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def detect_local_os
|
22
|
+
case ::RbConfig::CONFIG['host_os']
|
23
|
+
when /aix(.+)$/
|
24
|
+
return 'aix'
|
25
|
+
when /darwin(.+)$/
|
26
|
+
return 'darwin'
|
27
|
+
when /hpux(.+)$/
|
28
|
+
return 'hpux'
|
29
|
+
when /linux/
|
30
|
+
return 'linux'
|
31
|
+
when /freebsd(.+)$/
|
32
|
+
return 'freebsd'
|
33
|
+
when /openbsd(.+)$/
|
34
|
+
return 'openbsd'
|
35
|
+
when /netbsd(.*)$/
|
36
|
+
return 'netbsd'
|
37
|
+
when /solaris2/
|
38
|
+
return 'solaris2'
|
39
|
+
when /mswin|mingw32|windows/
|
40
|
+
# After long discussion in IRC the "powers that be" have come to a consensus
|
41
|
+
# that no Windows platform exists that was not based on the
|
42
|
+
# Windows_NT kernel, so we herby decree that "windows" will refer to all
|
43
|
+
# platforms built upon the Windows_NT kernel and have access to win32 or win64
|
44
|
+
# subsystems.
|
45
|
+
return 'windows'
|
46
|
+
else
|
47
|
+
return ::RbConfig::CONFIG['host_os']
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# author: Dominik Richter
|
4
|
+
# author: Christoph Hartmann
|
5
|
+
|
6
|
+
require 'train/plugins'
|
7
|
+
require 'digest'
|
8
|
+
|
9
|
+
module Train::Transports
|
10
|
+
class Mock < Train.plugin(1)
|
11
|
+
name 'mock'
|
12
|
+
|
13
|
+
def initialize(conf = nil)
|
14
|
+
@conf = conf || {}
|
15
|
+
trace_calls if @conf[:trace]
|
16
|
+
end
|
17
|
+
|
18
|
+
def connection
|
19
|
+
@connection ||= Connection.new(@conf)
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
'Mock Transport'
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def trace_calls
|
29
|
+
interface_methods = {
|
30
|
+
'Train::Transports::Mock' =>
|
31
|
+
Train::Transports::Mock.instance_methods(false),
|
32
|
+
'Train::Transports::Mock::Connection' =>
|
33
|
+
Connection.instance_methods(false),
|
34
|
+
'Train::Transports::Mock::Connection::File' =>
|
35
|
+
Connection::FileCommon.instance_methods(false),
|
36
|
+
'Train::Transports::Mock::Connection::OS' =>
|
37
|
+
Connection::OSCommon.instance_methods(false),
|
38
|
+
}
|
39
|
+
|
40
|
+
# rubocop:disable Metrics/ParameterLists
|
41
|
+
# rubocop:disable Lint/Eval
|
42
|
+
set_trace_func proc { |event, _file, _line, id, binding, classname|
|
43
|
+
unless classname.to_s.start_with?('Train::Transports::Mock') and
|
44
|
+
event == 'call' and
|
45
|
+
interface_methods[classname.to_s].include?(id)
|
46
|
+
next
|
47
|
+
end
|
48
|
+
# kindly borrowed from the wonderful simple-tracer by matugm
|
49
|
+
arg_names = eval(
|
50
|
+
'method(__method__).parameters.map { |arg| arg[1].to_s }',
|
51
|
+
binding)
|
52
|
+
args = eval("#{arg_names}.map { |arg| eval(arg) }", binding).join(', ')
|
53
|
+
prefix = '-' * (classname.to_s.count(':') - 2) + '> '
|
54
|
+
puts("#{prefix}#{id} #{args}")
|
55
|
+
}
|
56
|
+
# rubocop:enable all
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Train::Transports::Mock
|
62
|
+
class Connection < BaseConnection
|
63
|
+
attr_accessor :files, :commands
|
64
|
+
attr_reader :os
|
65
|
+
|
66
|
+
def initialize(conf = nil)
|
67
|
+
@conf = conf || {}
|
68
|
+
@files = {}
|
69
|
+
@os = OS.new(self, family: 'unknown')
|
70
|
+
@commands = {}
|
71
|
+
end
|
72
|
+
|
73
|
+
def mock_os(value)
|
74
|
+
@os = OS.new(self, value)
|
75
|
+
end
|
76
|
+
|
77
|
+
def mock_command(cmd, stdout = nil, stderr = nil, exit_status = 0)
|
78
|
+
@commands[cmd] = Command.new(stdout || '', stderr || '', exit_status)
|
79
|
+
end
|
80
|
+
|
81
|
+
def command_not_found(cmd)
|
82
|
+
if @conf[:verbose]
|
83
|
+
STDERR.puts('Command not mocked:')
|
84
|
+
STDERR.puts(' '+cmd.to_s.split("\n").join("\n "))
|
85
|
+
STDERR.puts(' SHA: ' + Digest::SHA256.hexdigest(cmd.to_s))
|
86
|
+
end
|
87
|
+
mock_command(cmd)
|
88
|
+
end
|
89
|
+
|
90
|
+
def run_command(cmd)
|
91
|
+
@commands[cmd] ||
|
92
|
+
@commands[Digest::SHA256.hexdigest cmd.to_s] ||
|
93
|
+
command_not_found(cmd)
|
94
|
+
end
|
95
|
+
|
96
|
+
def file_not_found(path)
|
97
|
+
STDERR.puts('File not mocked: '+path.to_s) if @conf[:verbose]
|
98
|
+
File.new(self, path)
|
99
|
+
end
|
100
|
+
|
101
|
+
def file(path)
|
102
|
+
@files[path] ||= file_not_found(path)
|
103
|
+
end
|
104
|
+
|
105
|
+
def to_s
|
106
|
+
'Mock Connection'
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class Train::Transports::Mock::Connection
|
112
|
+
Command = Struct.new(:stdout, :stderr, :exit_status)
|
113
|
+
end
|
114
|
+
|
115
|
+
class Train::Transports::Mock::Connection
|
116
|
+
class OS < OSCommon
|
117
|
+
def initialize(backend, desc)
|
118
|
+
super(backend, desc)
|
119
|
+
end
|
120
|
+
|
121
|
+
def detect_family
|
122
|
+
# no op, we do not need to detect the os
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class Train::Transports::Mock::Connection
|
128
|
+
class File < FileCommon
|
129
|
+
%w{
|
130
|
+
exist? mode owner group link_path content mtime size
|
131
|
+
selinux_label product_version file_version path type
|
132
|
+
}.each do |m|
|
133
|
+
attr_accessor m.tr('?', '').to_sym
|
134
|
+
end
|
135
|
+
|
136
|
+
def initialize(backend, path, follow_symlink = true)
|
137
|
+
super(backend, path, follow_symlink)
|
138
|
+
@type = :unknown
|
139
|
+
@exist = false
|
140
|
+
end
|
141
|
+
|
142
|
+
def mounted
|
143
|
+
@mounted ||=
|
144
|
+
@backend.run_command("mount | grep -- ' on #{@path}'")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
4
|
+
# Author:: Dominik Richter (<dominik.richter@gmail.com>)
|
5
|
+
# Author:: Christoph Hartmann (<chris@lollyrock.com>)
|
6
|
+
#
|
7
|
+
# Copyright (C) 2014, Fletcher Nichol
|
8
|
+
#
|
9
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
10
|
+
# you may not use this file except in compliance with the License.
|
11
|
+
# You may obtain a copy of the License at
|
12
|
+
#
|
13
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
14
|
+
#
|
15
|
+
# Unless required by applicable law or agreed to in writing, software
|
16
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
17
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
18
|
+
# See the License for the specific language governing permissions and
|
19
|
+
# limitations under the License.
|
20
|
+
|
21
|
+
require 'net/ssh'
|
22
|
+
require 'net/scp'
|
23
|
+
require 'train/errors'
|
24
|
+
|
25
|
+
module Train::Transports
|
26
|
+
# Wrapped exception for any internally raised SSH-related errors.
|
27
|
+
#
|
28
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
29
|
+
class SSHFailed < Train::TransportError; end
|
30
|
+
|
31
|
+
# A Transport which uses the SSH protocol to execute commands and transfer
|
32
|
+
# files.
|
33
|
+
#
|
34
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
35
|
+
class SSH < Train.plugin(1)
|
36
|
+
name 'ssh'
|
37
|
+
|
38
|
+
autoload :Connection, 'train/transports/ssh_connection'
|
39
|
+
|
40
|
+
# add options for submodules
|
41
|
+
include_options Train::Extras::CommandWrapper
|
42
|
+
|
43
|
+
# common target configuration
|
44
|
+
option :host, required: true
|
45
|
+
option :port, default: 22, required: true
|
46
|
+
option :user, default: 'root', required: true
|
47
|
+
option :key_files, default: nil
|
48
|
+
option :password, default: nil
|
49
|
+
|
50
|
+
# additional ssh options
|
51
|
+
option :keepalive, default: true
|
52
|
+
option :keepalive_interval, default: 60
|
53
|
+
option :connection_timeout, default: 15
|
54
|
+
option :connection_retries, default: 5
|
55
|
+
option :connection_retry_sleep, default: 1
|
56
|
+
option :max_wait_until_ready, default: 600
|
57
|
+
option :compression, default: false
|
58
|
+
|
59
|
+
option :compression_level do |opts|
|
60
|
+
# on nil or false: set compression level to 0
|
61
|
+
opts[:compression] ? 6 : 0
|
62
|
+
end
|
63
|
+
|
64
|
+
# (see Base#connection)
|
65
|
+
def connection(state = {}, &block)
|
66
|
+
opts = merge_options(options, state || {})
|
67
|
+
validate_options(opts)
|
68
|
+
conn_opts = connection_options(opts)
|
69
|
+
|
70
|
+
if defined?(@connection) && @connection_options == conn_opts
|
71
|
+
reuse_connection(&block)
|
72
|
+
else
|
73
|
+
create_new_connection(conn_opts, &block)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def validate_options(options)
|
80
|
+
super(options)
|
81
|
+
|
82
|
+
key_files = Array(options[:key_files])
|
83
|
+
if key_files.empty? and options[:password].nil?
|
84
|
+
fail Train::ClientError,
|
85
|
+
'You must configure at least one authentication method for SSH:'\
|
86
|
+
' Password or key.'
|
87
|
+
end
|
88
|
+
|
89
|
+
options[:auth_methods] ||= ['none']
|
90
|
+
|
91
|
+
unless key_files.empty?
|
92
|
+
options[:auth_methods].push('publickey')
|
93
|
+
options[:keys_only] = true if options[:password].nil?
|
94
|
+
options[:key_files] = key_files
|
95
|
+
end
|
96
|
+
|
97
|
+
unless options[:password].nil?
|
98
|
+
options[:auth_methods].push('password')
|
99
|
+
end
|
100
|
+
|
101
|
+
super
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
105
|
+
# Builds the hash of options needed by the Connection object on
|
106
|
+
# construction.
|
107
|
+
#
|
108
|
+
# @param opts [Hash] merged configuration and mutable state data
|
109
|
+
# @return [Hash] hash of connection options
|
110
|
+
# @api private
|
111
|
+
def connection_options(opts)
|
112
|
+
{
|
113
|
+
logger: logger,
|
114
|
+
user_known_hosts_file: '/dev/null',
|
115
|
+
paranoid: false,
|
116
|
+
hostname: opts[:host],
|
117
|
+
port: opts[:port],
|
118
|
+
username: opts[:user],
|
119
|
+
compression: opts[:compression],
|
120
|
+
compression_level: opts[:compression_level],
|
121
|
+
keepalive: opts[:keepalive],
|
122
|
+
keepalive_interval: opts[:keepalive_interval],
|
123
|
+
timeout: opts[:connection_timeout],
|
124
|
+
connection_retries: opts[:connection_retries],
|
125
|
+
connection_retry_sleep: opts[:connection_retry_sleep],
|
126
|
+
max_wait_until_ready: opts[:max_wait_until_ready],
|
127
|
+
auth_methods: opts[:auth_methods],
|
128
|
+
keys_only: opts[:keys_only],
|
129
|
+
keys: opts[:key_files],
|
130
|
+
password: opts[:password],
|
131
|
+
forward_agent: opts[:forward_agent],
|
132
|
+
transport_options: opts,
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
# Creates a new SSH Connection instance and save it for potential future
|
137
|
+
# reuse.
|
138
|
+
#
|
139
|
+
# @param options [Hash] conneciton options
|
140
|
+
# @return [Ssh::Connection] an SSH Connection instance
|
141
|
+
# @api private
|
142
|
+
def create_new_connection(options, &block)
|
143
|
+
if defined?(@connection)
|
144
|
+
logger.debug("[SSH] shutting previous connection #{@connection}")
|
145
|
+
@connection.close
|
146
|
+
end
|
147
|
+
|
148
|
+
@connection_options = options
|
149
|
+
conn = Connection.new(options, &block)
|
150
|
+
@connection = conn unless conn.nil?
|
151
|
+
end
|
152
|
+
|
153
|
+
# Return the last saved SSH connection instance.
|
154
|
+
#
|
155
|
+
# @return [Ssh::Connection] an SSH Connection instance
|
156
|
+
# @api private
|
157
|
+
def reuse_connection
|
158
|
+
logger.debug("[SSH] reusing existing connection #{@connection}")
|
159
|
+
yield @connection if block_given?
|
160
|
+
@connection
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|