furnish-ssh 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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