test-kitchen 1.6.0 → 1.7.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.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +8 -7
  3. data/.github/ISSUE_TEMPLATE.md +56 -0
  4. data/.gitignore +28 -27
  5. data/.kitchen.ci.yml +23 -0
  6. data/.kitchen.proxy.yml +27 -0
  7. data/.rubocop.yml +3 -3
  8. data/.travis.yml +70 -53
  9. data/.yardopts +3 -3
  10. data/Berksfile +3 -0
  11. data/CHANGELOG.md +1083 -1051
  12. data/CONTRIBUTING.md +14 -14
  13. data/Gemfile +19 -14
  14. data/Gemfile.proxy_tests +4 -5
  15. data/Guardfile +42 -42
  16. data/LICENSE +15 -15
  17. data/MAINTAINERS.md +23 -24
  18. data/README.md +135 -135
  19. data/Rakefile +61 -76
  20. data/appveyor.yml +44 -34
  21. data/features/kitchen_action_commands.feature +164 -164
  22. data/features/kitchen_command.feature +16 -16
  23. data/features/kitchen_console_command.feature +34 -34
  24. data/features/kitchen_defaults.feature +38 -38
  25. data/features/kitchen_diagnose_command.feature +96 -96
  26. data/features/kitchen_driver_create_command.feature +64 -64
  27. data/features/kitchen_driver_discover_command.feature +25 -25
  28. data/features/kitchen_help_command.feature +16 -16
  29. data/features/kitchen_init_command.feature +274 -274
  30. data/features/kitchen_list_command.feature +104 -104
  31. data/features/kitchen_login_command.feature +62 -62
  32. data/features/kitchen_sink_command.feature +30 -30
  33. data/features/kitchen_test_command.feature +88 -88
  34. data/features/step_definitions/gem_steps.rb +36 -36
  35. data/features/step_definitions/git_steps.rb +5 -5
  36. data/features/step_definitions/output_steps.rb +5 -5
  37. data/features/support/env.rb +75 -75
  38. data/lib/kitchen.rb +150 -150
  39. data/lib/kitchen/base64_stream.rb +55 -55
  40. data/lib/kitchen/cli.rb +419 -419
  41. data/lib/kitchen/collection.rb +55 -55
  42. data/lib/kitchen/color.rb +65 -65
  43. data/lib/kitchen/command.rb +185 -185
  44. data/lib/kitchen/command/action.rb +45 -45
  45. data/lib/kitchen/command/console.rb +58 -58
  46. data/lib/kitchen/command/diagnose.rb +92 -92
  47. data/lib/kitchen/command/driver_discover.rb +105 -105
  48. data/lib/kitchen/command/exec.rb +41 -41
  49. data/lib/kitchen/command/list.rb +119 -119
  50. data/lib/kitchen/command/login.rb +43 -43
  51. data/lib/kitchen/command/sink.rb +54 -54
  52. data/lib/kitchen/command/test.rb +51 -51
  53. data/lib/kitchen/config.rb +322 -322
  54. data/lib/kitchen/configurable.rb +529 -529
  55. data/lib/kitchen/data_munger.rb +959 -960
  56. data/lib/kitchen/diagnostic.rb +141 -141
  57. data/lib/kitchen/driver.rb +56 -56
  58. data/lib/kitchen/driver/base.rb +134 -134
  59. data/lib/kitchen/driver/dummy.rb +108 -108
  60. data/lib/kitchen/driver/proxy.rb +72 -72
  61. data/lib/kitchen/driver/ssh_base.rb +357 -357
  62. data/lib/kitchen/errors.rb +229 -229
  63. data/lib/kitchen/generator/driver_create.rb +177 -177
  64. data/lib/kitchen/generator/init.rb +296 -296
  65. data/lib/kitchen/instance.rb +662 -662
  66. data/lib/kitchen/lazy_hash.rb +142 -142
  67. data/lib/kitchen/loader/yaml.rb +349 -349
  68. data/lib/kitchen/logger.rb +423 -423
  69. data/lib/kitchen/logging.rb +56 -56
  70. data/lib/kitchen/login_command.rb +52 -52
  71. data/lib/kitchen/metadata_chopper.rb +52 -52
  72. data/lib/kitchen/platform.rb +67 -67
  73. data/lib/kitchen/provisioner.rb +54 -54
  74. data/lib/kitchen/provisioner/base.rb +236 -236
  75. data/lib/kitchen/provisioner/chef/berkshelf.rb +114 -114
  76. data/lib/kitchen/provisioner/chef/common_sandbox.rb +322 -322
  77. data/lib/kitchen/provisioner/chef/librarian.rb +112 -112
  78. data/lib/kitchen/provisioner/chef_apply.rb +124 -125
  79. data/lib/kitchen/provisioner/chef_base.rb +341 -294
  80. data/lib/kitchen/provisioner/chef_solo.rb +88 -89
  81. data/lib/kitchen/provisioner/chef_zero.rb +245 -245
  82. data/lib/kitchen/provisioner/dummy.rb +79 -79
  83. data/lib/kitchen/provisioner/shell.rb +138 -138
  84. data/lib/kitchen/rake_tasks.rb +63 -63
  85. data/lib/kitchen/shell_out.rb +93 -93
  86. data/lib/kitchen/ssh.rb +276 -276
  87. data/lib/kitchen/state_file.rb +120 -120
  88. data/lib/kitchen/suite.rb +51 -51
  89. data/lib/kitchen/thor_tasks.rb +66 -66
  90. data/lib/kitchen/transport.rb +54 -54
  91. data/lib/kitchen/transport/base.rb +176 -176
  92. data/lib/kitchen/transport/dummy.rb +79 -79
  93. data/lib/kitchen/transport/ssh.rb +364 -364
  94. data/lib/kitchen/transport/winrm.rb +486 -486
  95. data/lib/kitchen/util.rb +147 -147
  96. data/lib/kitchen/verifier.rb +55 -55
  97. data/lib/kitchen/verifier/base.rb +235 -235
  98. data/lib/kitchen/verifier/busser.rb +277 -277
  99. data/lib/kitchen/verifier/dummy.rb +79 -79
  100. data/lib/kitchen/verifier/shell.rb +101 -101
  101. data/lib/kitchen/version.rb +21 -21
  102. data/lib/vendor/hash_recursive_merge.rb +82 -82
  103. data/spec/kitchen/base64_stream_spec.rb +77 -77
  104. data/spec/kitchen/cli_spec.rb +56 -56
  105. data/spec/kitchen/collection_spec.rb +80 -80
  106. data/spec/kitchen/color_spec.rb +54 -54
  107. data/spec/kitchen/config_spec.rb +408 -408
  108. data/spec/kitchen/configurable_spec.rb +1095 -1062
  109. data/spec/kitchen/data_munger_spec.rb +2694 -2383
  110. data/spec/kitchen/diagnostic_spec.rb +129 -129
  111. data/spec/kitchen/driver/base_spec.rb +121 -121
  112. data/spec/kitchen/driver/dummy_spec.rb +199 -199
  113. data/spec/kitchen/driver/proxy_spec.rb +138 -138
  114. data/spec/kitchen/driver/ssh_base_spec.rb +1115 -1115
  115. data/spec/kitchen/driver_spec.rb +112 -112
  116. data/spec/kitchen/errors_spec.rb +309 -309
  117. data/spec/kitchen/instance_spec.rb +1419 -1419
  118. data/spec/kitchen/lazy_hash_spec.rb +117 -117
  119. data/spec/kitchen/loader/yaml_spec.rb +774 -774
  120. data/spec/kitchen/logger_spec.rb +429 -429
  121. data/spec/kitchen/logging_spec.rb +59 -59
  122. data/spec/kitchen/login_command_spec.rb +68 -68
  123. data/spec/kitchen/metadata_chopper_spec.rb +82 -82
  124. data/spec/kitchen/platform_spec.rb +89 -89
  125. data/spec/kitchen/provisioner/base_spec.rb +386 -386
  126. data/spec/kitchen/provisioner/chef_apply_spec.rb +136 -136
  127. data/spec/kitchen/provisioner/chef_base_spec.rb +1161 -1067
  128. data/spec/kitchen/provisioner/chef_solo_spec.rb +557 -557
  129. data/spec/kitchen/provisioner/chef_zero_spec.rb +1001 -1001
  130. data/spec/kitchen/provisioner/dummy_spec.rb +99 -99
  131. data/spec/kitchen/provisioner/shell_spec.rb +566 -566
  132. data/spec/kitchen/provisioner_spec.rb +107 -107
  133. data/spec/kitchen/shell_out_spec.rb +150 -150
  134. data/spec/kitchen/ssh_spec.rb +693 -693
  135. data/spec/kitchen/state_file_spec.rb +129 -129
  136. data/spec/kitchen/suite_spec.rb +62 -62
  137. data/spec/kitchen/transport/base_spec.rb +89 -89
  138. data/spec/kitchen/transport/ssh_spec.rb +1255 -1255
  139. data/spec/kitchen/transport/winrm_spec.rb +1143 -1143
  140. data/spec/kitchen/transport_spec.rb +112 -112
  141. data/spec/kitchen/util_spec.rb +165 -165
  142. data/spec/kitchen/verifier/base_spec.rb +362 -362
  143. data/spec/kitchen/verifier/busser_spec.rb +610 -610
  144. data/spec/kitchen/verifier/dummy_spec.rb +99 -99
  145. data/spec/kitchen/verifier/shell_spec.rb +160 -158
  146. data/spec/kitchen/verifier_spec.rb +120 -120
  147. data/spec/kitchen_spec.rb +114 -114
  148. data/spec/spec_helper.rb +85 -85
  149. data/spec/support/powershell_max_size_spec.rb +40 -40
  150. data/support/busser_install_command.ps1 +14 -14
  151. data/support/busser_install_command.sh +14 -14
  152. data/support/chef-client-zero.rb +77 -77
  153. data/support/chef_base_init_command.ps1 +18 -18
  154. data/support/chef_base_init_command.sh +2 -2
  155. data/support/chef_base_install_command.ps1 +85 -85
  156. data/support/chef_base_install_command.sh +229 -229
  157. data/support/chef_zero_prepare_command_legacy.ps1 +9 -9
  158. data/support/chef_zero_prepare_command_legacy.sh +10 -10
  159. data/support/download_helpers.sh +109 -109
  160. data/support/dummy-validation.pem +27 -27
  161. data/templates/driver/CHANGELOG.md.erb +3 -3
  162. data/templates/driver/Gemfile.erb +3 -3
  163. data/templates/driver/README.md.erb +64 -64
  164. data/templates/driver/Rakefile.erb +21 -21
  165. data/templates/driver/driver.rb.erb +23 -23
  166. data/templates/driver/gemspec.erb +29 -29
  167. data/templates/driver/gitignore.erb +17 -17
  168. data/templates/driver/license_apachev2.erb +15 -15
  169. data/templates/driver/license_lgplv3.erb +16 -16
  170. data/templates/driver/license_mit.erb +22 -22
  171. data/templates/driver/license_reserved.erb +5 -5
  172. data/templates/driver/tailor.erb +4 -4
  173. data/templates/driver/travis.yml.erb +11 -11
  174. data/templates/driver/version.rb.erb +12 -12
  175. data/templates/init/chefignore.erb +1 -1
  176. data/templates/init/kitchen.yml.erb +18 -18
  177. data/test-kitchen.gemspec +62 -62
  178. data/test/integration/default/default_spec.rb +3 -0
  179. data/testing_windows.md +37 -37
  180. metadata +23 -11
@@ -1,93 +1,93 @@
1
- # -*- encoding: utf-8 -*-
2
- #
3
- # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
- #
5
- # Copyright (C) 2012, Fletcher Nichol
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
-
19
- require "mixlib/shellout"
20
-
21
- module Kitchen
22
-
23
- # Mixin that wraps a command shell out invocation, providing a #run_command
24
- # method.
25
- #
26
- # @author Fletcher Nichol <fnichol@nichol.ca>
27
- module ShellOut
28
-
29
- # Wrapped exception for any interally raised shell out commands.
30
- class ShellCommandFailed < TransientFailure; end
31
-
32
- # Executes a command in a subshell on the local running system.
33
- #
34
- # @param cmd [String] command to be executed locally
35
- # @param options [Hash] additional configuration of command
36
- # @option options [TrueClass, FalseClass] :use_sudo whether or not to use
37
- # sudo
38
- # @option options [String] :sudo_command custom sudo command to use.
39
- # Default is "sudo -E".
40
- # @option options [String] :log_subject used in the output or log header
41
- # for clarity and context. Default is "local".
42
- # @option options [String] :cwd the directory to chdir to before running
43
- # the command
44
- # @option options [Hash] :environment a Hash of environment variables to
45
- # set before the command is run. By default, the environment will
46
- # *always* be set to `'LC_ALL' => 'C'` to prevent issues with multibyte
47
- # characters in Ruby 1.8. To avoid this, use :environment => nil for
48
- # *no* extra environment settings, or
49
- # `:environment => {'LC_ALL'=>nil, ...}` to set other environment
50
- # settings without changing the locale.
51
- # @option options [Integer] :timeout Numeric value for the number of
52
- # seconds to wait on the child process before raising an Exception.
53
- # This is calculated as the total amount of time that ShellOut waited on
54
- # the child process without receiving any output (i.e., IO.select
55
- # returned nil). Default is 60000 seconds. Note: the stdlib Timeout
56
- # library is not used.
57
- # @return [String] the standard output of the command as a String
58
- # @raise [ShellCommandFailed] if the command fails
59
- # @raise [Error] for all other unexpected exceptions
60
- def run_command(cmd, options = {})
61
- if options.fetch(:use_sudo, false)
62
- cmd = "#{options.fetch(:sudo_command, "sudo -E")} #{cmd}"
63
- end
64
- subject = "[#{options.fetch(:log_subject, "local")} command]"
65
-
66
- debug("#{subject} BEGIN (#{cmd})")
67
- sh = Mixlib::ShellOut.new(cmd, shell_opts(options))
68
- sh.run_command
69
- debug("#{subject} END #{Util.duration(sh.execution_time)}")
70
- sh.error!
71
- sh.stdout
72
- rescue Mixlib::ShellOut::ShellCommandFailed => ex
73
- raise ShellCommandFailed, ex.message
74
- rescue Exception => error # rubocop:disable Lint/RescueException
75
- error.extend(Kitchen::Error)
76
- raise
77
- end
78
-
79
- private
80
-
81
- # Returns a hash of MixLib::ShellOut options for the command.
82
- #
83
- # @param options [Hash] a Hash of options
84
- # @return [Hash] a new Hash of options, filterd and merged with defaults
85
- # @api private
86
- def shell_opts(options)
87
- filtered_opts = options.reject do |key, _value|
88
- [:use_sudo, :sudo_command, :log_subject, :quiet].include?(key)
89
- end
90
- { :live_stream => logger, :timeout => 60000 }.merge(filtered_opts)
91
- end
92
- end
93
- end
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2012, Fletcher Nichol
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require "mixlib/shellout"
20
+
21
+ module Kitchen
22
+
23
+ # Mixin that wraps a command shell out invocation, providing a #run_command
24
+ # method.
25
+ #
26
+ # @author Fletcher Nichol <fnichol@nichol.ca>
27
+ module ShellOut
28
+
29
+ # Wrapped exception for any interally raised shell out commands.
30
+ class ShellCommandFailed < TransientFailure; end
31
+
32
+ # Executes a command in a subshell on the local running system.
33
+ #
34
+ # @param cmd [String] command to be executed locally
35
+ # @param options [Hash] additional configuration of command
36
+ # @option options [TrueClass, FalseClass] :use_sudo whether or not to use
37
+ # sudo
38
+ # @option options [String] :sudo_command custom sudo command to use.
39
+ # Default is "sudo -E".
40
+ # @option options [String] :log_subject used in the output or log header
41
+ # for clarity and context. Default is "local".
42
+ # @option options [String] :cwd the directory to chdir to before running
43
+ # the command
44
+ # @option options [Hash] :environment a Hash of environment variables to
45
+ # set before the command is run. By default, the environment will
46
+ # *always* be set to `'LC_ALL' => 'C'` to prevent issues with multibyte
47
+ # characters in Ruby 1.8. To avoid this, use :environment => nil for
48
+ # *no* extra environment settings, or
49
+ # `:environment => {'LC_ALL'=>nil, ...}` to set other environment
50
+ # settings without changing the locale.
51
+ # @option options [Integer] :timeout Numeric value for the number of
52
+ # seconds to wait on the child process before raising an Exception.
53
+ # This is calculated as the total amount of time that ShellOut waited on
54
+ # the child process without receiving any output (i.e., IO.select
55
+ # returned nil). Default is 60000 seconds. Note: the stdlib Timeout
56
+ # library is not used.
57
+ # @return [String] the standard output of the command as a String
58
+ # @raise [ShellCommandFailed] if the command fails
59
+ # @raise [Error] for all other unexpected exceptions
60
+ def run_command(cmd, options = {})
61
+ if options.fetch(:use_sudo, false)
62
+ cmd = "#{options.fetch(:sudo_command, "sudo -E")} #{cmd}"
63
+ end
64
+ subject = "[#{options.fetch(:log_subject, "local")} command]"
65
+
66
+ debug("#{subject} BEGIN (#{cmd})")
67
+ sh = Mixlib::ShellOut.new(cmd, shell_opts(options))
68
+ sh.run_command
69
+ debug("#{subject} END #{Util.duration(sh.execution_time)}")
70
+ sh.error!
71
+ sh.stdout
72
+ rescue Mixlib::ShellOut::ShellCommandFailed => ex
73
+ raise ShellCommandFailed, ex.message
74
+ rescue Exception => error # rubocop:disable Lint/RescueException
75
+ error.extend(Kitchen::Error)
76
+ raise
77
+ end
78
+
79
+ private
80
+
81
+ # Returns a hash of MixLib::ShellOut options for the command.
82
+ #
83
+ # @param options [Hash] a Hash of options
84
+ # @return [Hash] a new Hash of options, filterd and merged with defaults
85
+ # @api private
86
+ def shell_opts(options)
87
+ filtered_opts = options.reject do |key, _value|
88
+ [:use_sudo, :sudo_command, :log_subject, :quiet].include?(key)
89
+ end
90
+ { :live_stream => logger, :timeout => 60000 }.merge(filtered_opts)
91
+ end
92
+ end
93
+ end
data/lib/kitchen/ssh.rb CHANGED
@@ -1,276 +1,276 @@
1
- # -*- encoding: utf-8 -*-
2
- #
3
- # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
- #
5
- # Copyright (C) 2013, Fletcher Nichol
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
-
19
- require "logger"
20
- require "net/ssh"
21
- require "net/scp"
22
- require "socket"
23
-
24
- require "kitchen/errors"
25
- require "kitchen/login_command"
26
-
27
- module Kitchen
28
-
29
- # Wrapped exception for any internally raised SSH-related errors.
30
- #
31
- # @author Fletcher Nichol <fnichol@nichol.ca>
32
- class SSHFailed < TransientFailure; end
33
-
34
- # Class to help establish SSH connections, issue remote commands, and
35
- # transfer files between a local system and remote node.
36
- #
37
- # @author Fletcher Nichol <fnichol@nichol.ca>
38
- class SSH
39
-
40
- # Constructs a new SSH object.
41
- #
42
- # @example basic usage
43
- #
44
- # ssh = Kitchen::SSH.new("remote.example.com", "root")
45
- # ssh.exec("sudo apt-get update")
46
- # ssh.upload!("/tmp/data.txt", "/var/lib/data.txt")
47
- # ssh.shutdown
48
- #
49
- # @example block usage
50
- #
51
- # Kitchen::SSH.new("remote.example.com", "root") do |ssh|
52
- # ssh.exec("sudo apt-get update")
53
- # ssh.upload!("/tmp/data.txt", "/var/lib/data.txt")
54
- # end
55
- #
56
- # @param hostname [String] the remote hostname (IP address, FQDN, etc.)
57
- # @param username [String] the username for the remote host
58
- # @param options [Hash] configuration options
59
- # @option options [Logger] :logger the logger to use
60
- # (default: `::Logger.new(STDOUT)`)
61
- # @yield [self] if a block is given then the constructed object yields
62
- # itself and calls `#shutdown` at the end, closing the remote connection
63
- def initialize(hostname, username, options = {})
64
- @hostname = hostname
65
- @username = username
66
- @options = options.dup
67
- @logger = @options.delete(:logger) || ::Logger.new(STDOUT)
68
-
69
- if block_given?
70
- yield self
71
- shutdown
72
- end
73
- end
74
-
75
- # Execute a command on the remote host.
76
- #
77
- # @param cmd [String] command string to execute
78
- # @raise [SSHFailed] if the command does not exit with a 0 code
79
- def exec(cmd)
80
- logger.debug("[SSH] #{self} (#{cmd})")
81
- exit_code = exec_with_exit(cmd)
82
-
83
- if exit_code != 0
84
- raise SSHFailed, "SSH exited (#{exit_code}) for command: [#{cmd}]"
85
- end
86
- end
87
-
88
- # Uploads a local file to remote host.
89
- #
90
- # @param local [String] path to local file
91
- # @param remote [String] path to remote file destination
92
- # @param options [Hash] configuration options that are passed to
93
- # `Net::SCP.upload`
94
- # @see http://net-ssh.github.io/net-scp/classes/Net/SCP.html#method-i-upload
95
- def upload!(local, remote, options = {}, &progress)
96
- if progress.nil?
97
- progress = lambda { |_ch, name, sent, total|
98
- if sent == total
99
- logger.debug("Uploaded #{name} (#{total} bytes)")
100
- end
101
- }
102
- end
103
-
104
- session.scp.upload!(local, remote, options, &progress)
105
- end
106
-
107
- # Uploads a recursive directory to remote host.
108
- #
109
- # @param local [String] path to local file or directory
110
- # @param remote [String] path to remote file destination
111
- # @param options [Hash] configuration options that are passed to
112
- # `Net::SCP.upload`
113
- # @option options [true,false] :recursive recursive copy (default: `true`)
114
- # @see http://net-ssh.github.io/net-scp/classes/Net/SCP.html#method-i-upload
115
- def upload_path!(local, remote, options = {}, &progress)
116
- options = { :recursive => true }.merge(options)
117
-
118
- upload!(local, remote, options, &progress)
119
- end
120
-
121
- # Shuts down the session connection, if it is still active.
122
- def shutdown
123
- return if @session.nil?
124
-
125
- logger.debug("[SSH] closing connection to #{self}")
126
- session.shutdown!
127
- ensure
128
- @session = nil
129
- end
130
-
131
- # Blocks until the remote host's SSH TCP port is listening.
132
- def wait
133
- logger.info("Waiting for #{hostname}:#{port}...") until test_ssh
134
- end
135
-
136
- # Builds a LoginCommand which can be used to open an interactive session
137
- # on the remote host.
138
- #
139
- # @return [LoginCommand] the login command
140
- def login_command
141
- args = %W[ -o UserKnownHostsFile=/dev/null ]
142
- args += %W[ -o StrictHostKeyChecking=no ]
143
- args += %W[ -o IdentitiesOnly=yes ] if options[:keys]
144
- args += %W[ -o LogLevel=#{logger.debug? ? "VERBOSE" : "ERROR"} ]
145
- if options.key?(:forward_agent)
146
- args += %W[ -o ForwardAgent=#{options[:forward_agent] ? "yes" : "no"} ]
147
- end
148
- Array(options[:keys]).each { |ssh_key| args += %W[ -i #{ssh_key} ] }
149
- args += %W[ -p #{port} ]
150
- args += %W[ #{username}@#{hostname} ]
151
-
152
- LoginCommand.new("ssh", args)
153
- end
154
-
155
- private
156
-
157
- # TCP socket exceptions
158
- SOCKET_EXCEPTIONS = [
159
- SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
160
- Errno::ENETUNREACH, IOError
161
- ]
162
-
163
- # @return [String] the remote hostname
164
- # @api private
165
- attr_reader :hostname
166
-
167
- # @return [String] the username for the remote host
168
- # @api private
169
- attr_reader :username
170
-
171
- # @return [Hash] SSH options, passed to `Net::SSH.start`
172
- attr_reader :options
173
-
174
- # @return [Logger] the logger to use
175
- # @api private
176
- attr_reader :logger
177
-
178
- # Builds the Net::SSH session connection or returns the existing one if
179
- # built.
180
- #
181
- # @return [Net::SSH::Connection::Session] the SSH connection session
182
- # @api private
183
- def session
184
- @session ||= establish_connection
185
- end
186
-
187
- # Establish a connection session to the remote host.
188
- #
189
- # @return [Net::SSH::Connection::Session] the SSH connection session
190
- # @api private
191
- def establish_connection
192
- rescue_exceptions = [
193
- Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
194
- Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
195
- Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout
196
- ]
197
- retries = options[:ssh_retries] || 3
198
-
199
- begin
200
- logger.debug("[SSH] opening connection to #{self}")
201
- Net::SSH.start(hostname, username, options)
202
- rescue *rescue_exceptions => e
203
- retries -= 1
204
- if retries > 0
205
- logger.info("[SSH] connection failed, retrying (#{e.inspect})")
206
- sleep options[:ssh_timeout] || 1
207
- retry
208
- else
209
- logger.warn("[SSH] connection failed, terminating (#{e.inspect})")
210
- raise
211
- end
212
- end
213
- end
214
-
215
- # String representation of object, reporting its connection details and
216
- # configuration.
217
- #
218
- # @api private
219
- def to_s
220
- "#{username}@#{hostname}:#{port}<#{options.inspect}>"
221
- end
222
-
223
- # @return [Integer] SSH port (default: 22)
224
- # @api private
225
- def port
226
- options.fetch(:port, 22)
227
- end
228
-
229
- # Execute a remote command and return the command's exit code.
230
- #
231
- # @param cmd [String] command string to execute
232
- # @return [Integer] the exit code of the command
233
- # @api private
234
- def exec_with_exit(cmd)
235
- exit_code = nil
236
- session.open_channel do |channel|
237
-
238
- channel.request_pty
239
-
240
- channel.exec(cmd) do |_ch, _success|
241
-
242
- channel.on_data do |_ch, data|
243
- logger << data
244
- end
245
-
246
- channel.on_extended_data do |_ch, _type, data|
247
- logger << data
248
- end
249
-
250
- channel.on_request("exit-status") do |_ch, data|
251
- exit_code = data.read_long
252
- end
253
- end
254
- end
255
- session.loop
256
- exit_code
257
- end
258
-
259
- # Test a remote TCP socket (presumably SSH) for connectivity.
260
- #
261
- # @return [true,false] a truthy value if the socket is ready and false
262
- # otherwise
263
- # @api private
264
- def test_ssh
265
- socket = TCPSocket.new(hostname, port)
266
- IO.select([socket], nil, nil, 5)
267
- rescue *SOCKET_EXCEPTIONS
268
- sleep options[:ssh_timeout] || 2
269
- false
270
- rescue Errno::EPERM, Errno::ETIMEDOUT
271
- false
272
- ensure
273
- socket && socket.close
274
- end
275
- end
276
- end
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2013, Fletcher Nichol
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require "logger"
20
+ require "net/ssh"
21
+ require "net/scp"
22
+ require "socket"
23
+
24
+ require "kitchen/errors"
25
+ require "kitchen/login_command"
26
+
27
+ module Kitchen
28
+
29
+ # Wrapped exception for any internally raised SSH-related errors.
30
+ #
31
+ # @author Fletcher Nichol <fnichol@nichol.ca>
32
+ class SSHFailed < TransientFailure; end
33
+
34
+ # Class to help establish SSH connections, issue remote commands, and
35
+ # transfer files between a local system and remote node.
36
+ #
37
+ # @author Fletcher Nichol <fnichol@nichol.ca>
38
+ class SSH
39
+
40
+ # Constructs a new SSH object.
41
+ #
42
+ # @example basic usage
43
+ #
44
+ # ssh = Kitchen::SSH.new("remote.example.com", "root")
45
+ # ssh.exec("sudo apt-get update")
46
+ # ssh.upload!("/tmp/data.txt", "/var/lib/data.txt")
47
+ # ssh.shutdown
48
+ #
49
+ # @example block usage
50
+ #
51
+ # Kitchen::SSH.new("remote.example.com", "root") do |ssh|
52
+ # ssh.exec("sudo apt-get update")
53
+ # ssh.upload!("/tmp/data.txt", "/var/lib/data.txt")
54
+ # end
55
+ #
56
+ # @param hostname [String] the remote hostname (IP address, FQDN, etc.)
57
+ # @param username [String] the username for the remote host
58
+ # @param options [Hash] configuration options
59
+ # @option options [Logger] :logger the logger to use
60
+ # (default: `::Logger.new(STDOUT)`)
61
+ # @yield [self] if a block is given then the constructed object yields
62
+ # itself and calls `#shutdown` at the end, closing the remote connection
63
+ def initialize(hostname, username, options = {})
64
+ @hostname = hostname
65
+ @username = username
66
+ @options = options.dup
67
+ @logger = @options.delete(:logger) || ::Logger.new(STDOUT)
68
+
69
+ if block_given?
70
+ yield self
71
+ shutdown
72
+ end
73
+ end
74
+
75
+ # Execute a command on the remote host.
76
+ #
77
+ # @param cmd [String] command string to execute
78
+ # @raise [SSHFailed] if the command does not exit with a 0 code
79
+ def exec(cmd)
80
+ logger.debug("[SSH] #{self} (#{cmd})")
81
+ exit_code = exec_with_exit(cmd)
82
+
83
+ if exit_code != 0
84
+ raise SSHFailed, "SSH exited (#{exit_code}) for command: [#{cmd}]"
85
+ end
86
+ end
87
+
88
+ # Uploads a local file to remote host.
89
+ #
90
+ # @param local [String] path to local file
91
+ # @param remote [String] path to remote file destination
92
+ # @param options [Hash] configuration options that are passed to
93
+ # `Net::SCP.upload`
94
+ # @see http://net-ssh.github.io/net-scp/classes/Net/SCP.html#method-i-upload
95
+ def upload!(local, remote, options = {}, &progress)
96
+ if progress.nil?
97
+ progress = lambda { |_ch, name, sent, total|
98
+ if sent == total
99
+ logger.debug("Uploaded #{name} (#{total} bytes)")
100
+ end
101
+ }
102
+ end
103
+
104
+ session.scp.upload!(local, remote, options, &progress)
105
+ end
106
+
107
+ # Uploads a recursive directory to remote host.
108
+ #
109
+ # @param local [String] path to local file or directory
110
+ # @param remote [String] path to remote file destination
111
+ # @param options [Hash] configuration options that are passed to
112
+ # `Net::SCP.upload`
113
+ # @option options [true,false] :recursive recursive copy (default: `true`)
114
+ # @see http://net-ssh.github.io/net-scp/classes/Net/SCP.html#method-i-upload
115
+ def upload_path!(local, remote, options = {}, &progress)
116
+ options = { :recursive => true }.merge(options)
117
+
118
+ upload!(local, remote, options, &progress)
119
+ end
120
+
121
+ # Shuts down the session connection, if it is still active.
122
+ def shutdown
123
+ return if @session.nil?
124
+
125
+ logger.debug("[SSH] closing connection to #{self}")
126
+ session.shutdown!
127
+ ensure
128
+ @session = nil
129
+ end
130
+
131
+ # Blocks until the remote host's SSH TCP port is listening.
132
+ def wait
133
+ logger.info("Waiting for #{hostname}:#{port}...") until test_ssh
134
+ end
135
+
136
+ # Builds a LoginCommand which can be used to open an interactive session
137
+ # on the remote host.
138
+ #
139
+ # @return [LoginCommand] the login command
140
+ def login_command
141
+ args = %W[ -o UserKnownHostsFile=/dev/null ]
142
+ args += %W[ -o StrictHostKeyChecking=no ]
143
+ args += %W[ -o IdentitiesOnly=yes ] if options[:keys]
144
+ args += %W[ -o LogLevel=#{logger.debug? ? "VERBOSE" : "ERROR"} ]
145
+ if options.key?(:forward_agent)
146
+ args += %W[ -o ForwardAgent=#{options[:forward_agent] ? "yes" : "no"} ]
147
+ end
148
+ Array(options[:keys]).each { |ssh_key| args += %W[ -i #{ssh_key} ] }
149
+ args += %W[ -p #{port} ]
150
+ args += %W[ #{username}@#{hostname} ]
151
+
152
+ LoginCommand.new("ssh", args)
153
+ end
154
+
155
+ private
156
+
157
+ # TCP socket exceptions
158
+ SOCKET_EXCEPTIONS = [
159
+ SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
160
+ Errno::ENETUNREACH, IOError
161
+ ]
162
+
163
+ # @return [String] the remote hostname
164
+ # @api private
165
+ attr_reader :hostname
166
+
167
+ # @return [String] the username for the remote host
168
+ # @api private
169
+ attr_reader :username
170
+
171
+ # @return [Hash] SSH options, passed to `Net::SSH.start`
172
+ attr_reader :options
173
+
174
+ # @return [Logger] the logger to use
175
+ # @api private
176
+ attr_reader :logger
177
+
178
+ # Builds the Net::SSH session connection or returns the existing one if
179
+ # built.
180
+ #
181
+ # @return [Net::SSH::Connection::Session] the SSH connection session
182
+ # @api private
183
+ def session
184
+ @session ||= establish_connection
185
+ end
186
+
187
+ # Establish a connection session to the remote host.
188
+ #
189
+ # @return [Net::SSH::Connection::Session] the SSH connection session
190
+ # @api private
191
+ def establish_connection
192
+ rescue_exceptions = [
193
+ Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
194
+ Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
195
+ Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout
196
+ ]
197
+ retries = options[:ssh_retries] || 3
198
+
199
+ begin
200
+ logger.debug("[SSH] opening connection to #{self}")
201
+ Net::SSH.start(hostname, username, options)
202
+ rescue *rescue_exceptions => e
203
+ retries -= 1
204
+ if retries > 0
205
+ logger.info("[SSH] connection failed, retrying (#{e.inspect})")
206
+ sleep options[:ssh_timeout] || 1
207
+ retry
208
+ else
209
+ logger.warn("[SSH] connection failed, terminating (#{e.inspect})")
210
+ raise
211
+ end
212
+ end
213
+ end
214
+
215
+ # String representation of object, reporting its connection details and
216
+ # configuration.
217
+ #
218
+ # @api private
219
+ def to_s
220
+ "#{username}@#{hostname}:#{port}<#{options.inspect}>"
221
+ end
222
+
223
+ # @return [Integer] SSH port (default: 22)
224
+ # @api private
225
+ def port
226
+ options.fetch(:port, 22)
227
+ end
228
+
229
+ # Execute a remote command and return the command's exit code.
230
+ #
231
+ # @param cmd [String] command string to execute
232
+ # @return [Integer] the exit code of the command
233
+ # @api private
234
+ def exec_with_exit(cmd)
235
+ exit_code = nil
236
+ session.open_channel do |channel|
237
+
238
+ channel.request_pty
239
+
240
+ channel.exec(cmd) do |_ch, _success|
241
+
242
+ channel.on_data do |_ch, data|
243
+ logger << data
244
+ end
245
+
246
+ channel.on_extended_data do |_ch, _type, data|
247
+ logger << data
248
+ end
249
+
250
+ channel.on_request("exit-status") do |_ch, data|
251
+ exit_code = data.read_long
252
+ end
253
+ end
254
+ end
255
+ session.loop
256
+ exit_code
257
+ end
258
+
259
+ # Test a remote TCP socket (presumably SSH) for connectivity.
260
+ #
261
+ # @return [true,false] a truthy value if the socket is ready and false
262
+ # otherwise
263
+ # @api private
264
+ def test_ssh
265
+ socket = TCPSocket.new(hostname, port)
266
+ IO.select([socket], nil, nil, 5)
267
+ rescue *SOCKET_EXCEPTIONS
268
+ sleep options[:ssh_timeout] || 2
269
+ false
270
+ rescue Errno::EPERM, Errno::ETIMEDOUT
271
+ false
272
+ ensure
273
+ socket && socket.close
274
+ end
275
+ end
276
+ end