test-kitchen 1.7.0 → 1.7.1.dev

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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +8 -8
  3. data/.gitattributes +3 -0
  4. data/.github/ISSUE_TEMPLATE.md +55 -55
  5. data/.gitignore +28 -28
  6. data/.kitchen.ci.yml +23 -23
  7. data/.kitchen.proxy.yml +27 -27
  8. data/.rubocop.yml +3 -3
  9. data/.travis.yml +70 -70
  10. data/.yardopts +3 -3
  11. data/Berksfile +3 -3
  12. data/CHANGELOG.md +1090 -1083
  13. data/CONTRIBUTING.md +14 -14
  14. data/Gemfile +19 -19
  15. data/Gemfile.proxy_tests +4 -4
  16. data/Guardfile +42 -42
  17. data/LICENSE +15 -15
  18. data/MAINTAINERS.md +23 -23
  19. data/README.md +135 -135
  20. data/Rakefile +61 -61
  21. data/appveyor.yml +44 -44
  22. data/features/kitchen_action_commands.feature +164 -164
  23. data/features/kitchen_command.feature +16 -16
  24. data/features/kitchen_console_command.feature +34 -34
  25. data/features/kitchen_defaults.feature +38 -38
  26. data/features/kitchen_diagnose_command.feature +96 -96
  27. data/features/kitchen_driver_create_command.feature +64 -64
  28. data/features/kitchen_driver_discover_command.feature +25 -25
  29. data/features/kitchen_help_command.feature +16 -16
  30. data/features/kitchen_init_command.feature +274 -274
  31. data/features/kitchen_list_command.feature +104 -104
  32. data/features/kitchen_login_command.feature +62 -62
  33. data/features/kitchen_sink_command.feature +30 -30
  34. data/features/kitchen_test_command.feature +88 -88
  35. data/features/step_definitions/gem_steps.rb +36 -36
  36. data/features/step_definitions/git_steps.rb +5 -5
  37. data/features/step_definitions/output_steps.rb +5 -5
  38. data/features/support/env.rb +75 -75
  39. data/lib/kitchen.rb +150 -150
  40. data/lib/kitchen/base64_stream.rb +55 -55
  41. data/lib/kitchen/cli.rb +419 -419
  42. data/lib/kitchen/collection.rb +55 -55
  43. data/lib/kitchen/color.rb +65 -65
  44. data/lib/kitchen/command.rb +185 -185
  45. data/lib/kitchen/command/action.rb +45 -45
  46. data/lib/kitchen/command/console.rb +58 -58
  47. data/lib/kitchen/command/diagnose.rb +92 -92
  48. data/lib/kitchen/command/driver_discover.rb +105 -105
  49. data/lib/kitchen/command/exec.rb +41 -41
  50. data/lib/kitchen/command/list.rb +119 -119
  51. data/lib/kitchen/command/login.rb +43 -43
  52. data/lib/kitchen/command/sink.rb +54 -54
  53. data/lib/kitchen/command/test.rb +51 -51
  54. data/lib/kitchen/config.rb +322 -322
  55. data/lib/kitchen/configurable.rb +529 -529
  56. data/lib/kitchen/data_munger.rb +959 -959
  57. data/lib/kitchen/diagnostic.rb +141 -141
  58. data/lib/kitchen/driver.rb +56 -56
  59. data/lib/kitchen/driver/base.rb +134 -134
  60. data/lib/kitchen/driver/dummy.rb +108 -108
  61. data/lib/kitchen/driver/proxy.rb +72 -72
  62. data/lib/kitchen/driver/ssh_base.rb +357 -357
  63. data/lib/kitchen/errors.rb +229 -229
  64. data/lib/kitchen/generator/driver_create.rb +177 -177
  65. data/lib/kitchen/generator/init.rb +296 -296
  66. data/lib/kitchen/instance.rb +662 -662
  67. data/lib/kitchen/lazy_hash.rb +142 -142
  68. data/lib/kitchen/loader/yaml.rb +349 -349
  69. data/lib/kitchen/logger.rb +423 -423
  70. data/lib/kitchen/logging.rb +56 -56
  71. data/lib/kitchen/login_command.rb +52 -52
  72. data/lib/kitchen/metadata_chopper.rb +52 -52
  73. data/lib/kitchen/platform.rb +67 -67
  74. data/lib/kitchen/provisioner.rb +54 -54
  75. data/lib/kitchen/provisioner/base.rb +236 -236
  76. data/lib/kitchen/provisioner/chef/berkshelf.rb +114 -114
  77. data/lib/kitchen/provisioner/chef/common_sandbox.rb +322 -322
  78. data/lib/kitchen/provisioner/chef/librarian.rb +112 -112
  79. data/lib/kitchen/provisioner/chef_apply.rb +124 -124
  80. data/lib/kitchen/provisioner/chef_base.rb +341 -341
  81. data/lib/kitchen/provisioner/chef_solo.rb +88 -88
  82. data/lib/kitchen/provisioner/chef_zero.rb +245 -245
  83. data/lib/kitchen/provisioner/dummy.rb +79 -79
  84. data/lib/kitchen/provisioner/shell.rb +138 -138
  85. data/lib/kitchen/rake_tasks.rb +63 -63
  86. data/lib/kitchen/shell_out.rb +93 -93
  87. data/lib/kitchen/ssh.rb +276 -276
  88. data/lib/kitchen/state_file.rb +120 -120
  89. data/lib/kitchen/suite.rb +51 -51
  90. data/lib/kitchen/thor_tasks.rb +66 -66
  91. data/lib/kitchen/transport.rb +54 -54
  92. data/lib/kitchen/transport/base.rb +176 -176
  93. data/lib/kitchen/transport/dummy.rb +79 -79
  94. data/lib/kitchen/transport/ssh.rb +364 -364
  95. data/lib/kitchen/transport/winrm.rb +486 -486
  96. data/lib/kitchen/util.rb +147 -147
  97. data/lib/kitchen/verifier.rb +55 -55
  98. data/lib/kitchen/verifier/base.rb +235 -235
  99. data/lib/kitchen/verifier/busser.rb +277 -277
  100. data/lib/kitchen/verifier/dummy.rb +79 -79
  101. data/lib/kitchen/verifier/shell.rb +101 -101
  102. data/lib/kitchen/version.rb +21 -21
  103. data/lib/vendor/hash_recursive_merge.rb +82 -82
  104. data/spec/kitchen/base64_stream_spec.rb +77 -77
  105. data/spec/kitchen/cli_spec.rb +56 -56
  106. data/spec/kitchen/collection_spec.rb +80 -80
  107. data/spec/kitchen/color_spec.rb +54 -54
  108. data/spec/kitchen/config_spec.rb +408 -408
  109. data/spec/kitchen/configurable_spec.rb +1095 -1095
  110. data/spec/kitchen/data_munger_spec.rb +2694 -2694
  111. data/spec/kitchen/diagnostic_spec.rb +129 -129
  112. data/spec/kitchen/driver/base_spec.rb +121 -121
  113. data/spec/kitchen/driver/dummy_spec.rb +199 -199
  114. data/spec/kitchen/driver/proxy_spec.rb +138 -138
  115. data/spec/kitchen/driver/ssh_base_spec.rb +1115 -1115
  116. data/spec/kitchen/driver_spec.rb +112 -112
  117. data/spec/kitchen/errors_spec.rb +309 -309
  118. data/spec/kitchen/instance_spec.rb +1419 -1419
  119. data/spec/kitchen/lazy_hash_spec.rb +117 -117
  120. data/spec/kitchen/loader/yaml_spec.rb +774 -774
  121. data/spec/kitchen/logger_spec.rb +429 -429
  122. data/spec/kitchen/logging_spec.rb +59 -59
  123. data/spec/kitchen/login_command_spec.rb +68 -68
  124. data/spec/kitchen/metadata_chopper_spec.rb +82 -82
  125. data/spec/kitchen/platform_spec.rb +89 -89
  126. data/spec/kitchen/provisioner/base_spec.rb +386 -386
  127. data/spec/kitchen/provisioner/chef_apply_spec.rb +136 -136
  128. data/spec/kitchen/provisioner/chef_base_spec.rb +1161 -1161
  129. data/spec/kitchen/provisioner/chef_solo_spec.rb +557 -557
  130. data/spec/kitchen/provisioner/chef_zero_spec.rb +1001 -1001
  131. data/spec/kitchen/provisioner/dummy_spec.rb +99 -99
  132. data/spec/kitchen/provisioner/shell_spec.rb +566 -566
  133. data/spec/kitchen/provisioner_spec.rb +107 -107
  134. data/spec/kitchen/shell_out_spec.rb +150 -150
  135. data/spec/kitchen/ssh_spec.rb +693 -693
  136. data/spec/kitchen/state_file_spec.rb +129 -129
  137. data/spec/kitchen/suite_spec.rb +62 -62
  138. data/spec/kitchen/transport/base_spec.rb +89 -89
  139. data/spec/kitchen/transport/ssh_spec.rb +1255 -1255
  140. data/spec/kitchen/transport/winrm_spec.rb +1143 -1143
  141. data/spec/kitchen/transport_spec.rb +112 -112
  142. data/spec/kitchen/util_spec.rb +165 -165
  143. data/spec/kitchen/verifier/base_spec.rb +362 -362
  144. data/spec/kitchen/verifier/busser_spec.rb +610 -610
  145. data/spec/kitchen/verifier/dummy_spec.rb +99 -99
  146. data/spec/kitchen/verifier/shell_spec.rb +160 -160
  147. data/spec/kitchen/verifier_spec.rb +120 -120
  148. data/spec/kitchen_spec.rb +114 -114
  149. data/spec/spec_helper.rb +85 -85
  150. data/spec/support/powershell_max_size_spec.rb +40 -40
  151. data/support/busser_install_command.ps1 +14 -14
  152. data/support/busser_install_command.sh +14 -14
  153. data/support/chef-client-zero.rb +77 -77
  154. data/support/chef_base_init_command.ps1 +18 -18
  155. data/support/chef_base_init_command.sh +2 -2
  156. data/support/chef_base_install_command.ps1 +85 -85
  157. data/support/chef_base_install_command.sh +229 -229
  158. data/support/chef_zero_prepare_command_legacy.ps1 +9 -9
  159. data/support/chef_zero_prepare_command_legacy.sh +10 -10
  160. data/support/download_helpers.sh +109 -109
  161. data/support/dummy-validation.pem +27 -27
  162. data/templates/driver/CHANGELOG.md.erb +3 -3
  163. data/templates/driver/Gemfile.erb +3 -3
  164. data/templates/driver/README.md.erb +64 -64
  165. data/templates/driver/Rakefile.erb +21 -21
  166. data/templates/driver/driver.rb.erb +23 -23
  167. data/templates/driver/gemspec.erb +29 -29
  168. data/templates/driver/gitignore.erb +17 -17
  169. data/templates/driver/license_apachev2.erb +15 -15
  170. data/templates/driver/license_lgplv3.erb +16 -16
  171. data/templates/driver/license_mit.erb +22 -22
  172. data/templates/driver/license_reserved.erb +5 -5
  173. data/templates/driver/tailor.erb +4 -4
  174. data/templates/driver/travis.yml.erb +11 -11
  175. data/templates/driver/version.rb.erb +12 -12
  176. data/templates/init/chefignore.erb +1 -1
  177. data/templates/init/kitchen.yml.erb +18 -18
  178. data/test-kitchen.gemspec +62 -62
  179. data/test/integration/default/default_spec.rb +3 -3
  180. data/testing_windows.md +37 -37
  181. metadata +5 -4
@@ -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