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