test-kitchen 1.6.0 → 1.7.0

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