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 +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/Guardfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +71 -0
- data/Rakefile +46 -0
- data/furnish-ssh.gemspec +34 -0
- data/lib/furnish/provisioners/ssh.rb +482 -0
- data/lib/furnish/ssh/version.rb +6 -0
- data/lib/net/ssh/verifiers/no_save_strict.rb +34 -0
- data/test/data/vagrant +27 -0
- data/test/helper.rb +25 -0
- data/test/tc.rb +40 -0
- data/test/test_api.rb +69 -0
- data/test/test_basic.rb +107 -0
- data/test/test_features.rb +66 -0
- data/test/test_multi_basic.rb +9 -0
- data/test/test_multi_features.rb +9 -0
- metadata +238 -0
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
data/Gemfile
ADDED
data/Guardfile
ADDED
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
|
data/furnish-ssh.gemspec
ADDED
@@ -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,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
|
data/test/test_basic.rb
ADDED
@@ -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
|
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
|