furnish-ssh 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1cb22d00eeea3ce171e12a7dd969c507c9f995a9
4
+ data.tar.gz: 7a26d0d19e35979e24dff3d60c40c19457c6d517
5
+ SHA512:
6
+ metadata.gz: 093bf39088928118a7e1fcf3934731b9c6aa5474fd9bdd5803213e6471559b75836efdcf7873636d38a52656e2d1102345ea93e5c5d838b3bfe4c88275aec429
7
+ data.tar.gz: b3b0554ef1b98939deb781ccf660ff39d9a5a1544ffc1df4c29900bf2ffdd8c3015698fb70251b8041ddc57a62b53d9f23893ac2c768583b599e2039d12514e3
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ html
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in furnish-ssh.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ # vim: ft=ruby
2
+ guard 'minitest' do
3
+ # with Minitest::Unit
4
+ watch(%r!^test/(.*)\/?test_(.*)\.rb!)
5
+ watch(%r!^test/helper\.rb!) { "test" }
6
+ end
7
+
8
+ guard 'rake', :run_on_all => false, :task => 'rdoc_cov' do
9
+ watch(%r!^lib/(.*)([^/]+)\.rb!)
10
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Erik Hollensbe
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # furnish-ssh
2
+
3
+ Inject SSH remote commands into your provisioning pipeline. See
4
+ [furnish](https://github.com/chef-workflow/furnish/) for information on what
5
+ furnish is.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'furnish-ssh'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install furnish-ssh
20
+
21
+ ## Usage
22
+
23
+ SSH requires ip addresses, which implies machines and possibly something like
24
+ [furnish-ip](https://github.com/chef-workflow/furnish-ip) to manage a pool of
25
+ them. This example will use AutoIP and Vagrant from
26
+ [furnish-vagrant](https://github.com/chef-workflow/furnish-vagrant).
27
+
28
+ It doesn't have to be this complicated (and won't be in most cases), this is
29
+ just a complete example.
30
+
31
+ ```ruby
32
+ require 'furnish'
33
+ require 'furnish/ip'
34
+ require 'furnish/provisioners/ip'
35
+ require 'furnish/provisioners/vagrant'
36
+ require 'furnish/provisioners/ssh'
37
+
38
+ Furnish.init
39
+ sched = Furnish::Scheduler.new
40
+ # Furnish::IP is a database of allocated addresses and more will be allocated
41
+ # by the AutoIP provisioner.
42
+ ip = Furnish::IP.new('10.10.10.0/24')
43
+ # allocate gateway and network IPs so Vagrant can NAT properly
44
+ [0, 1].each { |x| ip.allocate("10.10.10.#{x}") }
45
+
46
+ # create our group - get an ip, hand it to vagrant which creates a machine,
47
+ # hand it to ssh which updates the box.
48
+ group = Furnish::ProvisionerGroup.new(
49
+ 'test',
50
+ [
51
+ Furnish::Provisioner::AutoIP.new(:ip => ip, :number_of_addresses => 1),
52
+ Furnish::Provisioner::Vagrant.new(:box => "precise64", :number_of_machines => 1),
53
+ Furnish::Provisioner::SSH.new(
54
+ :username => "vagrant",
55
+ :password => "vagrant",
56
+ :startup_command => "sudo apt-get update; sudo apt-get dist-upgrade -y"
57
+ )
58
+ ]
59
+ )
60
+
61
+ sched << group
62
+ sched.run
63
+ ```
64
+
65
+ ## Contributing
66
+
67
+ 1. Fork it
68
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
69
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
70
+ 4. Push to the branch (`git push origin my-new-feature`)
71
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+ require 'rdoc/task'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << "test"
7
+ files = FileList["test/test_*.rb"]
8
+
9
+ if ENV["TEST_SMALL"]
10
+ files.reject! { |f| File.basename(f) =~ /^test_multi/ }
11
+ end
12
+
13
+ t.test_files = files
14
+ t.verbose = true
15
+ end
16
+
17
+ RDoc::Task.new do |rdoc|
18
+ rdoc.title = "Run SSH commands as a Furnish Provisioner"
19
+ rdoc.main = "README.md"
20
+ rdoc.rdoc_files.include("README.md", "lib/**/*.rb")
21
+ rdoc.rdoc_files -= ["lib/furnish/ssh/version.rb"]
22
+ if ENV["RDOC_COVER"]
23
+ rdoc.options << "-C"
24
+ end
25
+ end
26
+
27
+ desc "run tests with coverage report"
28
+ task "test:coverage" do
29
+ ENV["COVERAGE"] = "1"
30
+ Rake::Task["test"].invoke
31
+ end
32
+
33
+ desc "run rdoc with coverage report"
34
+ task :rdoc_cov do
35
+ # ugh
36
+ ENV["RDOC_COVER"] = "1"
37
+ ruby "-S rake rerdoc"
38
+ end
39
+
40
+ namespace :test do
41
+ desc "Run a shorter test suite that's not as exhaustive."
42
+ task :small do
43
+ ENV["TEST_SMALL"] = "1"
44
+ ruby "-S rake test"
45
+ end
46
+ end
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'furnish/ssh/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "furnish-ssh"
8
+ spec.version = Furnish::SSH::VERSION
9
+ spec.authors = ["Erik Hollensbe"]
10
+ spec.email = ["erik+github@hollensbe.org"]
11
+ spec.description = %q{Run SSH commands as a Furnish Provisioner}
12
+ spec.summary = %q{Run SSH commands as a Furnish Provisioner}
13
+ spec.homepage = "https://github.com/chef-workflow/furnish-ssh"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'furnish', '~> 0.1.1'
22
+ spec.add_dependency 'net-ssh', '~> 2.0'
23
+
24
+ spec.add_development_dependency 'furnish-vagrant', '~> 0.1.0'
25
+ spec.add_development_dependency 'furnish-ip', '~> 0.1.0'
26
+ spec.add_development_dependency 'bundler', '~> 1.3'
27
+ spec.add_development_dependency 'rake'
28
+ spec.add_development_dependency 'minitest'
29
+ spec.add_development_dependency 'guard-minitest'
30
+ spec.add_development_dependency 'guard-rake', '~> 0.0.8'
31
+ spec.add_development_dependency 'rdoc', '~> 4.0'
32
+ spec.add_development_dependency 'rb-fsevent'
33
+ spec.add_development_dependency 'simplecov'
34
+ end
@@ -0,0 +1,482 @@
1
+ require 'furnish/provisioners/api'
2
+ require 'furnish/logger'
3
+ require 'net/ssh'
4
+ require 'net/ssh/verifiers/no_save_strict' # XXX this is provided by this gem
5
+ require 'timeout'
6
+ require 'set'
7
+
8
+ module Furnish # :nodoc:
9
+ module Provisioner # :nodoc:
10
+
11
+ #
12
+ # Provisioner to execute SSH commands on remote targets during startup and
13
+ # shutdown. See ::new for construction requirements.
14
+ #
15
+ # Please see Net::SSH::Verifiers::NoSaveStrict and #paranoid about how host
16
+ # keys are handled in a surprising way.
17
+ #
18
+ # Startup::
19
+ # * requires:
20
+ # * ips (Set<String>):: list of IP addresses to target
21
+ # * yields:
22
+ # * ssh_exit_statuses (Hash<String, Integer>):: IP -> exit status map
23
+ # * ssh_output (Hash<String, String>):: IP -> command output map
24
+ #
25
+ # Shutdown::
26
+ # * accepts:
27
+ # * ips (Set<String>):: list of IP addresses to target
28
+ # * yields:
29
+ # * ssh_exit_statuses (Hash<String, Integer>):: IP -> exit status map
30
+ # * ssh_output (Hash<String, String>):: IP -> command output map
31
+ #
32
+ class SSH < API
33
+
34
+ include Furnish::Logger::Mixins
35
+
36
+ configure_startup do
37
+ requires :ips,
38
+ "String IP addresses to connect to",
39
+ Set
40
+
41
+ yields :ssh_exit_statuses,
42
+ "Exit codes, mapped IP<String> -> Code<Integer>",
43
+ Hash
44
+
45
+ # FIXME mute output option
46
+ yields :ssh_output,
47
+ "output of SSH, mapped IP<String> -> Output<String>",
48
+ Hash
49
+ end
50
+
51
+ configure_shutdown do
52
+ accepts_from_any true
53
+
54
+ accepts :ips,
55
+ "String IP addresses to connect to",
56
+ Set
57
+
58
+ yields :ssh_exit_statuses,
59
+ "Exit codes, mapped IP<String> -> Code<Integer>",
60
+ Hash
61
+
62
+ # FIXME mute output option
63
+ yields :ssh_output,
64
+ "output of SSH, mapped IP<String> -> Output<String>",
65
+ Hash
66
+ end
67
+
68
+ ##
69
+ # :attr: provision_wait
70
+ #
71
+ # How long to wait before giving up on SSH returning. Default is 300
72
+ # seconds. Fractional values OK.
73
+ #
74
+ furnish_property :provision_wait,
75
+ "How long to wait before giving up on SSH returning. Default is 300 seconds. Fractional values OK.",
76
+ Numeric
77
+
78
+ ##
79
+ # :attr: username
80
+ #
81
+ # Username which to SSH in as. Required, no default.
82
+ #
83
+ furnish_property :username,
84
+ "Username which to SSH in as. Required, no default.",
85
+ String
86
+
87
+ ##
88
+ # :attr: password
89
+ #
90
+ # Password to use when authenticating. Optional, but this or #private_key
91
+ # or #private_key_path must be provided.
92
+ #
93
+ furnish_property :password,
94
+ "Password to use when authenticating. Optional, but this or private_key or private_key_path must be provided.",
95
+ String
96
+
97
+ ##
98
+ # :attr: private_key
99
+ #
100
+ # Private key to use when authenticating. Optional, but this or #password
101
+ # or #private_key_path must be provided.
102
+ #
103
+ furnish_property :private_key,
104
+ "Private key to use when authenticating. Optional, but this or password or private_key_path must be provided.",
105
+ String
106
+
107
+ ##
108
+ # :attr: private_key_path
109
+ #
110
+ # Path to file on disk containing the private key to use when
111
+ # authenticating. Optional, but this or #password or #private_key must be
112
+ # provided.
113
+ #
114
+ furnish_property :private_key_path,
115
+ "Path to file on disk containing the private key to use when authenticating. Optional, but this or password or private_key must be provided.",
116
+ String
117
+
118
+ ##
119
+ # :attr: startup_command
120
+ #
121
+ # The command to run on each remote host when this provisioner runs
122
+ # startup. Either #startup_command or #shutdown_command must be provided.
123
+ #
124
+ furnish_property :startup_command,
125
+ "The command to run on each remote host when this provisioner runs startup. Either startup_command or shutdown_command must be provided.",
126
+ String
127
+
128
+ ##
129
+ # :attr: shutdown_command
130
+ #
131
+ # The command to run on each remote host when this provisioner runs
132
+ # shutdown. Either #startup_command or #shutdown_command must be provided.
133
+ #
134
+ furnish_property :shutdown_command,
135
+ "The command to run on each remote host when this provisioner runs shutdown. Either startup_command or shutdown_command must be provided.",
136
+ String
137
+
138
+ ##
139
+ # :attr: require_pty
140
+ #
141
+ # If true, attempts to allocate a pty after connecting. If this fails,
142
+ # fails the provision. Default is false. Cannot be used with #stdin.
143
+ #
144
+ furnish_property :require_pty,
145
+ "If true, attempts to allocate a pty after connecting. If this fails, fails the provision. Default is false. Cannot be used with stdin."
146
+
147
+ ##
148
+ # :attr: stdin
149
+ #
150
+ # If a string is provided, will be provided to the executing command as
151
+ # standard input. Cannot be used with #require_pty.
152
+ #
153
+ furnish_property :stdin,
154
+ "If a string is provided, will be provided to the executing command as standard input. Cannot be used with require_pty.",
155
+ String
156
+
157
+ ##
158
+ # :attr: merge_output
159
+ #
160
+ # If true, will merge stdout and stderr for purposes of output.
161
+ #
162
+ furnish_property :merge_output,
163
+ "If true, will merge stdout and stderr for purposes of output."
164
+
165
+ ##
166
+ # :attr: log_output
167
+ #
168
+ # If true, will send all output to the furnish logger. Use #merge_output
169
+ # to get stderr as well.
170
+ #
171
+ furnish_property :log_output,
172
+ "If true, will send all output to the furnish logger. Use merge_output to get stderr as well."
173
+
174
+ ##
175
+ # :attr: paranoid
176
+ #
177
+ # Maps to Net::SSH.start's :paranoid option. If :no_save_strict is
178
+ # assigned (the default), will use our
179
+ # Net::SSH::Verifiers::NoSaveStrict verifier which will not attempt to
180
+ # save any host keys that we do not recognize. Any that do exist
181
+ # however will be checked appropriately.
182
+ #
183
+ furnish_property :paranoid,
184
+ "Maps to Net::SSH.start's :paranoid option, used for host key validation. Use :no_save_strict (the default) to get something similar to :strict that doesn't save on an unknown key."
185
+
186
+ ##
187
+ # :attr: mute_output
188
+ #
189
+ # If true, output will not be stored or relayed. Useful for commands
190
+ # which will perform lots of output. log_output is not affected."
191
+ #
192
+ furnish_property :mute_output,
193
+ "If true, output will not be stored or relayed. Useful for commands which will perform lots of output. log_output is not affected."
194
+
195
+ ##
196
+ # :attr: success
197
+ #
198
+ # If non-nil, exit statuses that are in the set will be considered
199
+ # successes. Default is to only treat 0 as a success.
200
+ #
201
+ furnish_property :success,
202
+ "If non-nil, exit statuses that are in the set will be considered successes.",
203
+ Set
204
+
205
+ # a stored list of the IPs dealt with by this provisioner
206
+ attr_reader :ips
207
+
208
+ # a hash of ip -> output, accessible after provision. overwritten on both
209
+ # startup and shutdown.
210
+ attr_reader :output
211
+
212
+ #--
213
+ # TODO host key verifier, allowable exit codes (other than zero of
214
+ # course), attr_reader for output
215
+ #++
216
+
217
+ #
218
+ # Construct the SSH provisioner.
219
+ #
220
+ # Requirements::
221
+ # * #username must be provided.
222
+ # * #password, #private_key, or #private_key_path must be provided, but only one of them.
223
+ # * #startup_command or #shutdown_command must be provided. You may provide both.
224
+ # * #stdin and #require_pty cannot be provided together.
225
+ #
226
+ def initialize(args)
227
+ super
228
+ check_auth_args
229
+ check_command_args
230
+ check_stdin_pty
231
+
232
+ @paranoid = args.has_key?(:paranoid) ? args[:paranoid] : :no_save_strict
233
+ @provision_wait ||= 300
234
+ @success ||= Set[0]
235
+ end
236
+
237
+ #
238
+ # Predicate for determining requirements for ::new.
239
+ #
240
+ def check_stdin_pty
241
+ if stdin and require_pty
242
+ raise ArgumentError, "stdin and require_pty are incompatible -- if used together, will hang the provision."
243
+ end
244
+ end
245
+
246
+ #
247
+ # Predicate for determining requirements for ::new.
248
+ #
249
+ def check_command_args
250
+ unless startup_command or shutdown_command
251
+ raise ArgumentError, "startup_command or shutdown_command must be provided at minimum."
252
+ end
253
+ end
254
+
255
+ #
256
+ # Predicate for determining requirements for ::new.
257
+ #
258
+ def check_auth_args
259
+ unless username
260
+ raise ArgumentError, "username must be provided"
261
+ end
262
+
263
+ unless password or private_key or private_key_path
264
+ raise ArgumentError, "password, private_key, or private_key_path must be provided"
265
+ end
266
+
267
+ if [password, private_key, private_key_path].compact.count > 1
268
+ raise ArgumentError, "You may only supply one of password, private_key, or private_key_path."
269
+ end
270
+ end
271
+
272
+ #
273
+ # Checks #log_output and logs the output with the host if set.
274
+ #
275
+ def log(host, output)
276
+ if log_output
277
+ if_debug do
278
+ print "[#{host}] #{output}"
279
+ flush
280
+ end
281
+ end
282
+ end
283
+
284
+ #
285
+ # Constructs the proper hash for Net::SSH.start options.
286
+ #
287
+ def ssh_options
288
+ opts = {
289
+ :config => false,
290
+ :keys_only => private_key_path || private_key
291
+ }
292
+
293
+ if password
294
+ opts[:password] = password
295
+ elsif private_key
296
+ opts[:key_data] = [private_key]
297
+ elsif private_key_path
298
+ opts[:keys] = private_key_path
299
+ end
300
+
301
+ opts[:paranoid] = paranoid
302
+
303
+ if opts[:paranoid] == :no_save_strict
304
+ opts[:paranoid] = Net::SSH::Verifiers::NoSaveStrict.new
305
+ end
306
+
307
+ return opts
308
+ end
309
+
310
+ #
311
+ # Performs the actual connection and execution.
312
+ #
313
+ def ssh(host, cmd)
314
+ ret = {
315
+ :exit_status => 0,
316
+ :stdout => "",
317
+ :stderr => ""
318
+ }
319
+
320
+ Net::SSH.start(host, username, ssh_options) do |ssh|
321
+ ssh.open_channel do |ch|
322
+ if stdin
323
+ ch.send_data(stdin)
324
+ ch.eof!
325
+ end
326
+
327
+ if require_pty
328
+ ch.request_pty do |ch, success|
329
+ unless success
330
+ raise "The use_sudo setting requires a PTY, and your SSH is rejecting our attempt to get one."
331
+ end
332
+ end
333
+ end
334
+
335
+ ch.on_open_failed do |ch, code, desc|
336
+ raise "Connection Error to #{username}@#{host}: #{desc}"
337
+ end
338
+
339
+ ch.exec(cmd) do |ch, success|
340
+ unless success
341
+ raise "Could not execute command '#{cmd}' on #{username}@#{host}"
342
+ end
343
+
344
+ if merge_output
345
+ ch.on_data do |ch, data|
346
+ log(host, data)
347
+ ret[:stdout] << data
348
+ end
349
+
350
+ ch.on_extended_data do |ch, type, data|
351
+ if type == 1
352
+ log(host, data)
353
+ ret[:stdout] << data
354
+ end
355
+ end
356
+ else
357
+ ch.on_data do |ch, data|
358
+ log(host, data)
359
+ ret[:stdout] << data
360
+ end
361
+
362
+ ch.on_extended_data do |ch, type, data|
363
+ ret[:stderr] << data if type == 1
364
+ end
365
+ end
366
+
367
+ ch.on_request("exit-status") do |ch, data|
368
+ ret[:exit_status] = data.read_long
369
+ end
370
+ end
371
+ end
372
+
373
+ ssh.loop
374
+ end
375
+
376
+ return ret
377
+ end
378
+
379
+ #
380
+ # Runs multiple ssh commands in threads, monitors those threads and
381
+ # stuffs status information. Will return #noop unless a command is
382
+ # provided. Called by #startup and #shutdown.
383
+ #
384
+ def run_ssh_provision(provision_command)
385
+ unless provision_command
386
+ return noop
387
+ end
388
+
389
+ #
390
+ # XXX sorry for the ugly. creates a IP => Thread map for tracking return values.
391
+ #
392
+ thread_map = Hash[
393
+ ips.map do |ip|
394
+ [
395
+ ip,
396
+ Thread.new do
397
+ ssh(ip, provision_command)
398
+ end
399
+ ]
400
+ end
401
+ ]
402
+
403
+ # FIXME see TODO about output handling
404
+ exit_statuses = { }
405
+ @output = { }
406
+
407
+ begin
408
+ Timeout.timeout(provision_wait) do
409
+ thread_map.each do |ip, thr|
410
+ result = thr.value # exception will happen here.
411
+
412
+ output[ip] = mute_output ? "" : result[:stdout]
413
+ exit_statuses[ip] = result[:exit_status]
414
+ end
415
+ end
416
+ rescue TimeoutError
417
+ thread_map.values.each { |t| t.kill if t.alive? }
418
+ raise "timeout reached waiting for hosts '#{ips.join(', ')}'"
419
+ end
420
+
421
+ if exit_statuses.values.all? { |x| success.any? { |c| x == c } }
422
+ return({ :ssh_exit_statuses => exit_statuses, :ssh_output => output })
423
+ else
424
+ # FIXME log
425
+ return false
426
+ end
427
+ end
428
+
429
+ #
430
+ # What happens when we can't execute something (e.g., because of a
431
+ # missing command). Just some boilerplate values.
432
+ #
433
+ def noop
434
+ exit_statuses = Hash[ips.map { |ip| [ip, 0] }]
435
+ @output = Hash[ips.map { |ip| [ip, ""] }]
436
+
437
+ return({ :ssh_exit_statuses => exit_statuses, :ssh_output => output })
438
+ end
439
+
440
+ #
441
+ # Provision: run the command on all hosts and return the status from
442
+ # #run_ssh_provision. Will stuff the ips if passed regardless, so they
443
+ # can be used for #shutdown when nothing is expected to run in #startup.
444
+ #
445
+ def startup(args={})
446
+ @ips = args[:ips]
447
+ run_ssh_provision(startup_command)
448
+ end
449
+
450
+ #
451
+ # Deprovision: run the command. If no ips are provided from a previous
452
+ # provisioner, use the IPs gathered during startup.
453
+ #
454
+ def shutdown(args={})
455
+ # XXX use the IPs we got during startup if we didn't get a new set.
456
+ if args[:ips] and !args[:ips].empty?
457
+ @ips = args[:ips]
458
+ end
459
+
460
+ return false if !ips or ips.empty?
461
+ run_ssh_provision(shutdown_command)
462
+ end
463
+
464
+ #
465
+ # Outputs the commands if they exist.
466
+ #
467
+ def report
468
+ a = []
469
+
470
+ if startup_command
471
+ a.push("startup: '#{startup_command}'")
472
+ end
473
+
474
+ if shutdown_command
475
+ a.push("shutdown: '#{shutdown_command}'")
476
+ end
477
+
478
+ return a
479
+ end
480
+ end
481
+ end
482
+ end
@@ -0,0 +1,6 @@
1
+ module Furnish # :nodoc:
2
+ module SSH # :nodoc:
3
+ # the version of furnish-ssh
4
+ VERSION = "0.0.1"
5
+ end
6
+ end
@@ -0,0 +1,34 @@
1
+ require 'net/ssh/errors'
2
+ require 'net/ssh/known_hosts'
3
+ require 'net/ssh/verifiers/secure'
4
+
5
+
6
+ module Net #:nodoc:
7
+ module SSH #:nodoc:
8
+ module Verifiers #:nodoc:
9
+ #
10
+ # This is similar to Net::SSH::Verifiers::Strict, but does not save the
11
+ # host key if it does not already exist in the known hosts file(s),
12
+ # making it ideal for repeat provisions of varied VMs that re-use IP
13
+ # addresses.
14
+ #
15
+ # It still goes through normal verification for those keys that do exist
16
+ # in the known hosts files, however.
17
+ #
18
+ #--
19
+ # XXX the inheritance here is correct. Strict inherits from secure, but saves
20
+ # instead. We're just like strict but we don't save.
21
+ #++
22
+ class NoSaveStrict < Secure
23
+ #
24
+ # Verify the connection. See NoSaveStrict.
25
+ #
26
+ def verify(arguments)
27
+ super
28
+ rescue HostKeyUnknown
29
+ return true
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
data/test/data/vagrant ADDED
@@ -0,0 +1,27 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIIEogIBAAKCAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzI
3
+ w+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoP
4
+ kcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2
5
+ hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NO
6
+ Td0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcW
7
+ yLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQIBIwKCAQEA4iqWPJXtzZA68mKd
8
+ ELs4jJsdyky+ewdZeNds5tjcnHU5zUYE25K+ffJED9qUWICcLZDc81TGWjHyAqD1
9
+ Bw7XpgUwFgeUJwUlzQurAv+/ySnxiwuaGJfhFM1CaQHzfXphgVml+fZUvnJUTvzf
10
+ TK2Lg6EdbUE9TarUlBf/xPfuEhMSlIE5keb/Zz3/LUlRg8yDqz5w+QWVJ4utnKnK
11
+ iqwZN0mwpwU7YSyJhlT4YV1F3n4YjLswM5wJs2oqm0jssQu/BT0tyEXNDYBLEF4A
12
+ sClaWuSJ2kjq7KhrrYXzagqhnSei9ODYFShJu8UWVec3Ihb5ZXlzO6vdNQ1J9Xsf
13
+ 4m+2ywKBgQD6qFxx/Rv9CNN96l/4rb14HKirC2o/orApiHmHDsURs5rUKDx0f9iP
14
+ cXN7S1uePXuJRK/5hsubaOCx3Owd2u9gD6Oq0CsMkE4CUSiJcYrMANtx54cGH7Rk
15
+ EjFZxK8xAv1ldELEyxrFqkbE4BKd8QOt414qjvTGyAK+OLD3M2QdCQKBgQDtx8pN
16
+ CAxR7yhHbIWT1AH66+XWN8bXq7l3RO/ukeaci98JfkbkxURZhtxV/HHuvUhnPLdX
17
+ 3TwygPBYZFNo4pzVEhzWoTtnEtrFueKxyc3+LjZpuo+mBlQ6ORtfgkr9gBVphXZG
18
+ YEzkCD3lVdl8L4cw9BVpKrJCs1c5taGjDgdInQKBgHm/fVvv96bJxc9x1tffXAcj
19
+ 3OVdUN0UgXNCSaf/3A/phbeBQe9xS+3mpc4r6qvx+iy69mNBeNZ0xOitIjpjBo2+
20
+ dBEjSBwLk5q5tJqHmy/jKMJL4n9ROlx93XS+njxgibTvU6Fp9w+NOFD/HvxB3Tcz
21
+ 6+jJF85D5BNAG3DBMKBjAoGBAOAxZvgsKN+JuENXsST7F89Tck2iTcQIT8g5rwWC
22
+ P9Vt74yboe2kDT531w8+egz7nAmRBKNM751U/95P9t88EDacDI/Z2OwnuFQHCPDF
23
+ llYOUI+SpLJ6/vURRbHSnnn8a/XG+nzedGH5JGqEJNQsz+xT2axM0/W/CRknmGaJ
24
+ kda/AoGANWrLCz708y7VYgAtW2Uf1DPOIYMdvo6fxIB5i9ZfISgcJ/bbCUkFrhoH
25
+ +vq/5CIWxCPp0f85R4qxxQ5ihxJ0YDQT9Jpx4TMss4PSavPaBH3RXow5Ohe+bYoQ
26
+ NE5OgEXk2wVfZczCZpigBKbKZHNYcelXtTt/nP3rsCuGcM4h53s=
27
+ -----END RSA PRIVATE KEY-----
data/test/helper.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'bundler/setup'
2
+
3
+ if ENV["COVERAGE"]
4
+ require 'simplecov'
5
+ SimpleCov.start do
6
+ add_filter '/test/'
7
+ end
8
+ end
9
+
10
+ unless ENV["FURNISH_DEBUG"]
11
+ $stderr.puts <<-EOF
12
+
13
+ These tests are *SLOW* and take some time to run with very little feedback.
14
+ If you want output of what is going on in the test suite, set FURNISH_DEBUG
15
+ in your environment.
16
+
17
+ EOF
18
+ end
19
+
20
+ require 'furnish/test'
21
+ require 'furnish/provisioners/auto_ip'
22
+ require 'furnish/provisioners/vagrant'
23
+ require 'furnish/provisioners/ssh'
24
+ require 'tc'
25
+ require 'minitest/autorun'
data/test/tc.rb ADDED
@@ -0,0 +1,40 @@
1
+ #
2
+ # This is a specialized test case for orchestrating against furnish-ssh
3
+ # with a vagrant box.
4
+ #
5
+ class Furnish::SSHTestCase < Furnish::SchedulerTestCase
6
+ BOX_URL = "http://files.vagrantup.com/precise64.box"
7
+ PRIVATE_KEY_PATH = "test/data/vagrant" # path to private keys that work on vagrant
8
+
9
+ def setup
10
+ super
11
+ @machine_count = 1
12
+ @ip = nil
13
+ @klass = Furnish::Provisioner::SSH
14
+ sched.serial = true
15
+ end
16
+
17
+ def teardown
18
+ sched.teardown
19
+ super
20
+ end
21
+
22
+ def ssh_provisioner(extra_args={ :startup_command => "ls", :log_output => true })
23
+ args = { :username => "vagrant", :password => "vagrant" }
24
+ @klass.new(args.merge(extra_args))
25
+ end
26
+
27
+ def ip
28
+ @ip ||= Furnish::IP.new("10.10.10.0/24")
29
+ @ip.allocate("10.10.10.0")
30
+ @ip.allocate("10.10.10.1")
31
+ @ip
32
+ end
33
+
34
+ def machine_provisioner(num=@machine_count)
35
+ [
36
+ Furnish::Provisioner::AutoIP.new(:ip => ip, :number_of_addresses => num),
37
+ Furnish::Provisioner::Vagrant.new(:box_url => BOX_URL, :number_of_servers => num)
38
+ ]
39
+ end
40
+ end
data/test/test_api.rb ADDED
@@ -0,0 +1,69 @@
1
+ require 'helper'
2
+
3
+ class TestAPI < Furnish::SSHTestCase
4
+ def test_protocol
5
+ sp = @klass.startup_protocol
6
+ assert_nil(sp[:accepts_from_any])
7
+ assert_includes(sp[:requires], :ips)
8
+ assert_equal(Set, sp[:requires][:ips][:type])
9
+ assert_includes(sp[:yields], :ssh_exit_statuses)
10
+ assert_equal(Hash, sp[:yields][:ssh_exit_statuses][:type])
11
+
12
+ sp = @klass.shutdown_protocol
13
+ assert_includes(sp[:yields], :ssh_exit_statuses)
14
+ assert_equal(Hash, sp[:yields][:ssh_exit_statuses][:type])
15
+ assert(sp[:accepts_from_any])
16
+ end
17
+
18
+ def test_properties
19
+ fp = @klass.furnish_properties
20
+ {
21
+ :username => String,
22
+ :password => String,
23
+ :private_key => String,
24
+ :private_key_path => String,
25
+ :startup_command => String,
26
+ :shutdown_command => String,
27
+ :provision_wait => Numeric
28
+ }.each do |key, value|
29
+ assert_includes(fp, key)
30
+ assert_equal(value, fp[key][:type])
31
+ end
32
+ end
33
+
34
+ def test_constructor
35
+ passing_auth_opts = { :username => "foo", :password => "quux" }
36
+ passing_cmd_opts = { :startup_command => "ls" }
37
+
38
+ assert_raises(ArgumentError) { @klass.new }
39
+ assert_raises(ArgumentError) { @klass.new(:username => "foo") }
40
+ assert_raises(ArgumentError) { @klass.new(:password => "bar") }
41
+
42
+ # XXX permutations of auth options
43
+ assert_raises(ArgumentError) { @klass.new(passing_cmd_opts.merge(:username => "foo", :password => "quux", :private_key_path => "frob")) }
44
+ assert_raises(ArgumentError) { @klass.new(passing_cmd_opts.merge(:username => "foo", :password => "quux", :private_key => "frob")) }
45
+ assert_raises(ArgumentError) { @klass.new(passing_cmd_opts.merge(:username => "foo", :private_key_path => "quux", :private_key => "frob")) }
46
+ assert_raises(ArgumentError) { @klass.new(passing_cmd_opts.merge(:username => "foo", :private_key_path => "quux", :private_key => "frob", :password => "heyooo")) }
47
+
48
+ # same as above, just with valid arguments
49
+ %w[password private_key private_key_path].each do |arg|
50
+ @klass.new(passing_cmd_opts.merge(:username => "foo", arg.to_sym => "quux"))
51
+ end
52
+
53
+ assert_raises(ArgumentError) { @klass.new(passing_auth_opts) }
54
+
55
+ cmd_opts = %w[startup_command shutdown_command]
56
+
57
+ cmd_opts.each do |arg|
58
+ @klass.new(passing_auth_opts.merge(arg.to_sym => "ls"))
59
+ end
60
+
61
+ @klass.new(passing_auth_opts.merge(Hash[cmd_opts.map { |arg| [arg.to_sym, "ls"] }]))
62
+
63
+ obj = @klass.new(passing_auth_opts.merge(passing_cmd_opts))
64
+ assert_equal(300, obj.provision_wait)
65
+
66
+ obj = @klass.new(passing_auth_opts.merge(passing_cmd_opts.merge(:provision_wait => 10)))
67
+ assert_equal(10, obj.provision_wait)
68
+ end
69
+ end
@@ -0,0 +1,107 @@
1
+ require 'helper'
2
+
3
+ class TestBasic < Furnish::SSHTestCase
4
+ def test_startup
5
+ sched.s("test1-startup", machine_provisioner)
6
+ sched.run
7
+
8
+ prov = ssh_provisioner
9
+ assert_equal(["startup: 'ls'"], prov.report)
10
+
11
+ target_ips = ip.group_ips("test1-startup")
12
+ assert_equal(@machine_count, target_ips.count)
13
+
14
+ ret = prov.startup(:ips => target_ips)
15
+ refute_nil(ret)
16
+ assert_equal(Hash[target_ips.map { |ip| [ip, 0] }], ret[:ssh_exit_statuses])
17
+
18
+ target_ips.each do |ip|
19
+ assert_kind_of(String, ret[:ssh_output][ip])
20
+ refute_empty(ret[:ssh_output][ip])
21
+ refute_empty(prov.output[ip])
22
+ end
23
+
24
+ ret = prov.shutdown
25
+ refute_nil(ret)
26
+ assert_equal(Hash[target_ips.map { |ip| [ip, 0] }], ret[:ssh_exit_statuses])
27
+
28
+ target_ips.each do |ip|
29
+ assert_kind_of(String, ret[:ssh_output][ip])
30
+ assert_empty(ret[:ssh_output][ip])
31
+ assert_empty(prov.output[ip])
32
+ end
33
+
34
+ prov = ssh_provisioner(:startup_command => 'exit 1')
35
+ refute(prov.startup(:ips => target_ips))
36
+ assert(prov.shutdown)
37
+
38
+ prov = ssh_provisioner(:shutdown_command => 'exit 1')
39
+ assert(prov.startup(:ips => target_ips))
40
+ refute(prov.shutdown)
41
+
42
+ prov = ssh_provisioner(:startup_command => 'exit 1', :success => Set[0,1])
43
+ assert(prov.startup(:ips => target_ips))
44
+ end
45
+
46
+ def test_shutdown
47
+ sched.s("test1-shutdown", machine_provisioner)
48
+ sched.run
49
+
50
+ prov = ssh_provisioner(:shutdown_command => "ls")
51
+ assert_equal(["shutdown: 'ls'"], prov.report)
52
+
53
+ refute(prov.shutdown)
54
+
55
+ target_ips = ip.group_ips("test1-shutdown")
56
+ assert_equal(@machine_count, target_ips.count)
57
+
58
+ ret = prov.startup(:ips => Set[])
59
+ refute_nil(ret)
60
+ assert_empty(ret[:ssh_exit_statuses])
61
+ assert_empty(ret[:ssh_output])
62
+ assert_empty(prov.output)
63
+
64
+ refute(prov.shutdown(:ips => Set[]))
65
+ refute(prov.shutdown)
66
+
67
+ ret = prov.shutdown(:ips => target_ips)
68
+ refute_nil(ret)
69
+ assert_equal(Hash[target_ips.map { |ip| [ip, 0] }], ret[:ssh_exit_statuses])
70
+
71
+ target_ips.each do |ip|
72
+ assert_kind_of(String, ret[:ssh_output][ip])
73
+ refute_empty(ret[:ssh_output][ip])
74
+ refute_empty(prov.output[ip])
75
+ end
76
+
77
+ prov = ssh_provisioner(:shutdown_command => 'exit 1')
78
+ assert(prov.startup(:ips => Set[]))
79
+ refute(prov.shutdown(:ips => target_ips))
80
+
81
+ prov = ssh_provisioner(:shutdown_command => 'exit 1', :success => Set[0,1])
82
+ assert(prov.startup(:ips => target_ips))
83
+ assert(prov.shutdown)
84
+ end
85
+
86
+ def test_basic_provision
87
+ sched.s("test1-basic", machine_provisioner + [ssh_provisioner, ssh_provisioner(:shutdown_command => "ls")])
88
+ # these next two will raise if something's not working.
89
+ sched.run
90
+ sched.down("test1-basic")
91
+ end
92
+
93
+ def test_failing_provision
94
+ sched.s("test1-failing", machine_provisioner + [ssh_provisioner(:startup_command => 'exit 1'), ssh_provisioner(:shutdown_command => "ls")])
95
+ assert_raises(RuntimeError) { sched.run }
96
+ sched.force_deprovision = true
97
+ sched.down("test1-failing")
98
+ sched.force_deprovision = false
99
+ sched.s("test2-failing", machine_provisioner + [ssh_provisioner, ssh_provisioner(:shutdown_command => "exit 1")])
100
+ sched.run
101
+ assert_raises(RuntimeError) { sched.down("test2-failing") }
102
+ sched.force_deprovision = true
103
+ sched.down("test2-failing")
104
+ ensure
105
+ sched.force_deprovision = false
106
+ end
107
+ end
@@ -0,0 +1,66 @@
1
+ require 'helper'
2
+
3
+ class TestFeatures < Furnish::SSHTestCase
4
+ def test_stdin_and_log_and_merge_output
5
+ if ENV["FURNISH_DEBUG"]
6
+ $stderr.puts "Log tests are running"
7
+ end
8
+
9
+ io = StringIO.new('', 'w')
10
+
11
+ Furnish.logger = Furnish::Logger.new(io, 3)
12
+
13
+ sched.s("test1-stdin", machine_provisioner)
14
+ sched.run
15
+
16
+ prov = ssh_provisioner(:startup_command => "cat", :stdin => "fart\n", :log_output => true)
17
+ target_ips = ip.group_ips("test1-stdin")
18
+ ret = prov.startup(:ips => target_ips)
19
+ assert(ret)
20
+
21
+ target_ips.each do |ip|
22
+ assert_kind_of(String, ret[:ssh_output][ip])
23
+ assert_equal("fart\n", ret[:ssh_output][ip])
24
+ assert_equal("fart\n", prov.output[ip])
25
+ assert_match(/^\[#{Regexp.quote(ip)}\] fart$/, io.string)
26
+ end
27
+
28
+ io.string = ''
29
+
30
+ prov = ssh_provisioner(:startup_command => "cat >/dev/fd/2", :stdin => "fart\n", :log_output => true)
31
+ target_ips = ip.group_ips("test1-stdin")
32
+ ret = prov.startup(:ips => target_ips)
33
+ assert(ret)
34
+
35
+ target_ips.each do |ip|
36
+ assert_kind_of(String, ret[:ssh_output][ip])
37
+ assert_empty(ret[:ssh_output][ip])
38
+ assert_empty(prov.output[ip])
39
+ assert_empty(io.string)
40
+ end
41
+
42
+ prov = ssh_provisioner(:startup_command => "cat >/dev/fd/2", :stdin => "fart\n", :merge_output => true, :log_output => true)
43
+ target_ips = ip.group_ips("test1-stdin")
44
+ ret = prov.startup(:ips => target_ips)
45
+ assert(ret)
46
+
47
+ target_ips.each do |ip|
48
+ assert_kind_of(String, ret[:ssh_output][ip])
49
+ assert_equal("fart\n", ret[:ssh_output][ip])
50
+ assert_equal("fart\n", prov.output[ip])
51
+ assert_match(/^\[#{Regexp.quote(ip)}\] fart$/, io.string)
52
+ end
53
+
54
+ prov = ssh_provisioner(:startup_command => "cat >/dev/fd/2", :stdin => "fart\n", :merge_output => true, :log_output => true, :mute_output => true)
55
+ target_ips = ip.group_ips("test1-stdin")
56
+ ret = prov.startup(:ips => target_ips)
57
+ assert(ret)
58
+
59
+ target_ips.each do |ip|
60
+ assert_kind_of(String, ret[:ssh_output][ip])
61
+ assert_empty(ret[:ssh_output][ip])
62
+ assert_empty(prov.output[ip])
63
+ assert_match(/^\[#{Regexp.quote(ip)}\] fart$/, io.string)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,9 @@
1
+ require 'helper'
2
+ require 'test_basic'
3
+
4
+ class TestMultiBasic < TestBasic
5
+ def setup
6
+ super
7
+ @machine_count = 2
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require 'helper'
2
+ require 'test_features'
3
+
4
+ class TestMultiFeatures < TestFeatures
5
+ def setup
6
+ super
7
+ @machine_count = 2
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,238 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: furnish-ssh
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Erik Hollensbe
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-04-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: furnish
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-ssh
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: furnish-vagrant
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 0.1.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.1.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: furnish-ip
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 0.1.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: 0.1.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '1.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '1.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: minitest
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: guard-minitest
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: guard-rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ~>
130
+ - !ruby/object:Gem::Version
131
+ version: 0.0.8
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ~>
137
+ - !ruby/object:Gem::Version
138
+ version: 0.0.8
139
+ - !ruby/object:Gem::Dependency
140
+ name: rdoc
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ~>
144
+ - !ruby/object:Gem::Version
145
+ version: '4.0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ~>
151
+ - !ruby/object:Gem::Version
152
+ version: '4.0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rb-fsevent
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - '>='
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - '>='
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: simplecov
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - '>='
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description: Run SSH commands as a Furnish Provisioner
182
+ email:
183
+ - erik+github@hollensbe.org
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - .gitignore
189
+ - Gemfile
190
+ - Guardfile
191
+ - LICENSE.txt
192
+ - README.md
193
+ - Rakefile
194
+ - furnish-ssh.gemspec
195
+ - lib/furnish/provisioners/ssh.rb
196
+ - lib/furnish/ssh/version.rb
197
+ - lib/net/ssh/verifiers/no_save_strict.rb
198
+ - test/data/vagrant
199
+ - test/helper.rb
200
+ - test/tc.rb
201
+ - test/test_api.rb
202
+ - test/test_basic.rb
203
+ - test/test_features.rb
204
+ - test/test_multi_basic.rb
205
+ - test/test_multi_features.rb
206
+ homepage: https://github.com/chef-workflow/furnish-ssh
207
+ licenses:
208
+ - MIT
209
+ metadata: {}
210
+ post_install_message:
211
+ rdoc_options: []
212
+ require_paths:
213
+ - lib
214
+ required_ruby_version: !ruby/object:Gem::Requirement
215
+ requirements:
216
+ - - '>='
217
+ - !ruby/object:Gem::Version
218
+ version: '0'
219
+ required_rubygems_version: !ruby/object:Gem::Requirement
220
+ requirements:
221
+ - - '>='
222
+ - !ruby/object:Gem::Version
223
+ version: '0'
224
+ requirements: []
225
+ rubyforge_project:
226
+ rubygems_version: 2.0.3
227
+ signing_key:
228
+ specification_version: 4
229
+ summary: Run SSH commands as a Furnish Provisioner
230
+ test_files:
231
+ - test/data/vagrant
232
+ - test/helper.rb
233
+ - test/tc.rb
234
+ - test/test_api.rb
235
+ - test/test_basic.rb
236
+ - test/test_features.rb
237
+ - test/test_multi_basic.rb
238
+ - test/test_multi_features.rb