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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +71 -0
  3. data/CHANGELOG.md +308 -0
  4. data/Gemfile +30 -0
  5. data/LICENSE +201 -0
  6. data/README.md +156 -0
  7. data/Rakefile +148 -0
  8. data/lib/train.rb +117 -0
  9. data/lib/train/errors.rb +23 -0
  10. data/lib/train/extras.rb +17 -0
  11. data/lib/train/extras/command_wrapper.rb +148 -0
  12. data/lib/train/extras/file_aix.rb +20 -0
  13. data/lib/train/extras/file_common.rb +161 -0
  14. data/lib/train/extras/file_linux.rb +16 -0
  15. data/lib/train/extras/file_unix.rb +79 -0
  16. data/lib/train/extras/file_windows.rb +91 -0
  17. data/lib/train/extras/linux_lsb.rb +60 -0
  18. data/lib/train/extras/os_common.rb +136 -0
  19. data/lib/train/extras/os_detect_darwin.rb +32 -0
  20. data/lib/train/extras/os_detect_linux.rb +148 -0
  21. data/lib/train/extras/os_detect_unix.rb +99 -0
  22. data/lib/train/extras/os_detect_windows.rb +57 -0
  23. data/lib/train/extras/stat.rb +133 -0
  24. data/lib/train/options.rb +80 -0
  25. data/lib/train/plugins.rb +40 -0
  26. data/lib/train/plugins/base_connection.rb +86 -0
  27. data/lib/train/plugins/transport.rb +49 -0
  28. data/lib/train/transports/docker.rb +103 -0
  29. data/lib/train/transports/local.rb +52 -0
  30. data/lib/train/transports/local_file.rb +90 -0
  31. data/lib/train/transports/local_os.rb +51 -0
  32. data/lib/train/transports/mock.rb +147 -0
  33. data/lib/train/transports/ssh.rb +163 -0
  34. data/lib/train/transports/ssh_connection.rb +225 -0
  35. data/lib/train/transports/winrm.rb +184 -0
  36. data/lib/train/transports/winrm_connection.rb +194 -0
  37. data/lib/train/version.rb +7 -0
  38. data/test/integration/.kitchen.yml +43 -0
  39. data/test/integration/Berksfile +3 -0
  40. data/test/integration/bootstrap.sh +17 -0
  41. data/test/integration/chefignore +1 -0
  42. data/test/integration/cookbooks/test/metadata.rb +1 -0
  43. data/test/integration/cookbooks/test/recipes/default.rb +100 -0
  44. data/test/integration/cookbooks/test/recipes/prep_files.rb +47 -0
  45. data/test/integration/docker_run.rb +153 -0
  46. data/test/integration/docker_test.rb +24 -0
  47. data/test/integration/docker_test_container.rb +24 -0
  48. data/test/integration/helper.rb +61 -0
  49. data/test/integration/sudo/customcommand.rb +15 -0
  50. data/test/integration/sudo/nopasswd.rb +16 -0
  51. data/test/integration/sudo/passwd.rb +21 -0
  52. data/test/integration/sudo/reqtty.rb +17 -0
  53. data/test/integration/sudo/run_as.rb +12 -0
  54. data/test/integration/test-travis-1.yaml +13 -0
  55. data/test/integration/test-travis-2.yaml +13 -0
  56. data/test/integration/test_local.rb +19 -0
  57. data/test/integration/test_ssh.rb +39 -0
  58. data/test/integration/tests/path_block_device_test.rb +74 -0
  59. data/test/integration/tests/path_character_device_test.rb +74 -0
  60. data/test/integration/tests/path_file_test.rb +79 -0
  61. data/test/integration/tests/path_folder_test.rb +90 -0
  62. data/test/integration/tests/path_missing_test.rb +77 -0
  63. data/test/integration/tests/path_pipe_test.rb +78 -0
  64. data/test/integration/tests/path_symlink_test.rb +95 -0
  65. data/test/integration/tests/run_command_test.rb +28 -0
  66. data/test/unit/extras/command_wrapper_test.rb +78 -0
  67. data/test/unit/extras/file_common_test.rb +180 -0
  68. data/test/unit/extras/linux_file_test.rb +167 -0
  69. data/test/unit/extras/os_common_test.rb +269 -0
  70. data/test/unit/extras/os_detect_linux_test.rb +189 -0
  71. data/test/unit/extras/os_detect_windows_test.rb +99 -0
  72. data/test/unit/extras/stat_test.rb +148 -0
  73. data/test/unit/extras/windows_file_test.rb +44 -0
  74. data/test/unit/helper.rb +7 -0
  75. data/test/unit/plugins/connection_test.rb +44 -0
  76. data/test/unit/plugins/transport_test.rb +111 -0
  77. data/test/unit/plugins_test.rb +22 -0
  78. data/test/unit/train_test.rb +156 -0
  79. data/test/unit/transports/local_file_test.rb +184 -0
  80. data/test/unit/transports/local_test.rb +87 -0
  81. data/test/unit/transports/mock_test.rb +87 -0
  82. data/test/unit/transports/ssh_test.rb +109 -0
  83. data/test/unit/version_test.rb +8 -0
  84. data/test/windows/local_test.rb +46 -0
  85. data/test/windows/winrm_test.rb +52 -0
  86. data/train.gemspec +38 -0
  87. 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