rouster 0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/LICENSE +9 -0
- data/README.md +175 -0
- data/Rakefile +65 -0
- data/Vagrantfile +23 -0
- data/examples/bootstrap.rb +113 -0
- data/examples/demo.rb +71 -0
- data/examples/error.rb +30 -0
- data/lib/rouster.rb +737 -0
- data/lib/rouster/deltas.rb +481 -0
- data/lib/rouster/puppet.rb +398 -0
- data/lib/rouster/testing.rb +743 -0
- data/lib/rouster/tests.rb +596 -0
- data/path_helper.rb +21 -0
- data/rouster.gemspec +30 -0
- data/test/basic.rb +10 -0
- data/test/functional/deltas/test_get_crontab.rb +99 -0
- data/test/functional/deltas/test_get_groups.rb +48 -0
- data/test/functional/deltas/test_get_packages.rb +71 -0
- data/test/functional/deltas/test_get_ports.rb +119 -0
- data/test/functional/deltas/test_get_services.rb +43 -0
- data/test/functional/deltas/test_get_users.rb +45 -0
- data/test/functional/puppet/test_facter.rb +59 -0
- data/test/functional/test_caching.rb +124 -0
- data/test/functional/test_destroy.rb +51 -0
- data/test/functional/test_dirs.rb +88 -0
- data/test/functional/test_files.rb +64 -0
- data/test/functional/test_get.rb +76 -0
- data/test/functional/test_inspect.rb +31 -0
- data/test/functional/test_is_dir.rb +118 -0
- data/test/functional/test_is_file.rb +119 -0
- data/test/functional/test_new.rb +92 -0
- data/test/functional/test_put.rb +81 -0
- data/test/functional/test_rebuild.rb +49 -0
- data/test/functional/test_restart.rb +44 -0
- data/test/functional/test_run.rb +77 -0
- data/test/functional/test_status.rb +38 -0
- data/test/functional/test_suspend.rb +31 -0
- data/test/functional/test_up.rb +27 -0
- data/test/functional/test_validate_file.rb +30 -0
- data/test/puppet/manifests/default.pp +9 -0
- data/test/puppet/manifests/hiera.yaml +12 -0
- data/test/puppet/manifests/hieradata/common.json +3 -0
- data/test/puppet/manifests/hieradata/vagrant.json +3 -0
- data/test/puppet/manifests/manifest.pp +78 -0
- data/test/puppet/modules/role/manifests/ui.pp +5 -0
- data/test/puppet/test_apply.rb +149 -0
- data/test/puppet/test_roles.rb +186 -0
- data/test/tunnel_vs_scp.rb +41 -0
- data/test/unit/puppet/test_get_puppet_star.rb +68 -0
- data/test/unit/test_generate_unique_mac.rb +43 -0
- data/test/unit/test_new.rb +31 -0
- data/test/unit/test_parse_ls_string.rb +334 -0
- data/test/unit/test_traverse_up.rb +43 -0
- data/test/unit/testing/test_meets_constraint.rb +55 -0
- data/test/unit/testing/test_validate_file.rb +112 -0
- data/test/unit/testing/test_validate_group.rb +72 -0
- data/test/unit/testing/test_validate_package.rb +69 -0
- data/test/unit/testing/test_validate_port.rb +98 -0
- data/test/unit/testing/test_validate_service.rb +73 -0
- data/test/unit/testing/test_validate_user.rb +92 -0
- metadata +203 -0
data/examples/error.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require sprintf('%s/../%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
|
2
|
+
require 'rouster'
|
3
|
+
|
4
|
+
begin
|
5
|
+
badkey = Rouster.new(:name => 'whatever', :verbosity => 4, :sshkey => __FILE__)
|
6
|
+
rescue Rouster::InternalError => e
|
7
|
+
p "caught #{e.class}: #{e.message}"
|
8
|
+
rescue => e
|
9
|
+
p "caught unexpected #{e.class}: #{e.message}"
|
10
|
+
end
|
11
|
+
|
12
|
+
p badkey
|
13
|
+
|
14
|
+
begin
|
15
|
+
badvagrantfile = Rouster.new(:name => 'likehesaid', :vagrantfile => 'dne')
|
16
|
+
rescue Rouster::InternalError => e
|
17
|
+
p "caught #{e.class}: #{e.message}"
|
18
|
+
rescue => e
|
19
|
+
p "caught unexpected #{e.class}: #{e.message}"
|
20
|
+
end
|
21
|
+
|
22
|
+
p badvagrantfile
|
23
|
+
|
24
|
+
begin
|
25
|
+
good = Rouster.new(:name => 'app', verbosity => 4)
|
26
|
+
rescue => e
|
27
|
+
p "caught unexpected exception #{e.class}: #{e.message}"
|
28
|
+
end
|
29
|
+
|
30
|
+
p good
|
data/lib/rouster.rb
ADDED
@@ -0,0 +1,737 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'log4r'
|
3
|
+
require 'json'
|
4
|
+
require 'net/scp'
|
5
|
+
require 'net/ssh'
|
6
|
+
|
7
|
+
require sprintf('%s/../%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
|
8
|
+
|
9
|
+
require 'rouster/tests'
|
10
|
+
|
11
|
+
class Rouster
|
12
|
+
|
13
|
+
# sporadically updated version number
|
14
|
+
VERSION = 0.50
|
15
|
+
|
16
|
+
# custom exceptions -- what else do we want them to include/do?
|
17
|
+
class ArgumentError < StandardError; end # thrown by methods that take parameters from users
|
18
|
+
class FileTransferError < StandardError; end # thrown by get() and put()
|
19
|
+
class InternalError < StandardError; end # thrown by most (if not all) Rouster methods
|
20
|
+
class ExternalError < StandardError; end # thrown when external dependencies do not respond as expected
|
21
|
+
class LocalExecutionError < StandardError; end # thrown by _run()
|
22
|
+
class RemoteExecutionError < StandardError; end # thrown by run()
|
23
|
+
class SSHConnectionError < StandardError; end # thrown by available_via_ssh() -- and potentially _run()
|
24
|
+
|
25
|
+
attr_accessor :facts, :sudo, :verbosity
|
26
|
+
attr_reader :cache, :cache_timeout, :deltas, :exitcode, :log, :name, :output, :passthrough, :sshkey, :unittest, :vagrantfile
|
27
|
+
|
28
|
+
##
|
29
|
+
# initialize - object instantiation
|
30
|
+
#
|
31
|
+
# parameters
|
32
|
+
# * <name> - the name of the VM as specified in the Vagrantfile
|
33
|
+
# * [cache_timeout] - integer specifying how long Rouster should cache status() and is_available_via_ssh?() results, default is false
|
34
|
+
# * [passthrough] - boolean of whether this is a VM or passthrough, default is false -- passthrough is not completely implemented
|
35
|
+
# * [sshkey] - the full or relative path to a SSH key used to auth to VM -- defaults to location Vagrant installs to (ENV[VAGRANT_HOME} or ]~/.vagrant.d/)
|
36
|
+
# * [sshtunnel] - boolean of whether or not to instantiate the SSH tunnel upon upping the VM, default is true
|
37
|
+
# * [sudo] - boolean of whether or not to prefix commands run in VM with 'sudo', default is true
|
38
|
+
# * [vagrantfile] - the full or relative path to the Vagrantfile to use, if not specified, will look for one in 5 directories above current location
|
39
|
+
# * [verbosity] - DEBUG (0) < INFO (1) < WARN (2) < ERROR (3) < FATAL (4)
|
40
|
+
def initialize(opts = nil)
|
41
|
+
@cache_timeout = opts[:cache_timeout].nil? ? false : opts[:cache_timeout]
|
42
|
+
@name = opts[:name]
|
43
|
+
@passthrough = opts[:passthrough].nil? ? false : opts[:passthrough]
|
44
|
+
@sshkey = opts[:sshkey]
|
45
|
+
@sshtunnel = opts[:sshtunnel].nil? ? true : opts[:sshtunnel]
|
46
|
+
@unittest = opts[:unittest].nil? ? false : opts[:unittest]
|
47
|
+
@vagrantfile = opts[:vagrantfile].nil? ? traverse_up(Dir.pwd, 'Vagrantfile', 5) : opts[:vagrantfile]
|
48
|
+
@verbosity = opts[:verbosity].is_a?(Integer) ? opts[:verbosity] : 4
|
49
|
+
|
50
|
+
if opts.has_key?(:sudo)
|
51
|
+
@sudo = opts[:sudo]
|
52
|
+
elsif @passthrough.eql?(true)
|
53
|
+
@sudo = false
|
54
|
+
else
|
55
|
+
@sudo = true
|
56
|
+
end
|
57
|
+
|
58
|
+
@ostype = nil
|
59
|
+
@output = Array.new
|
60
|
+
@cache = Hash.new
|
61
|
+
@deltas = Hash.new
|
62
|
+
|
63
|
+
@exitcode = nil
|
64
|
+
@ssh = nil # hash containing the SSH connection object
|
65
|
+
@ssh_info = nil # hash containing connection information
|
66
|
+
|
67
|
+
# set up logging
|
68
|
+
require 'log4r/config'
|
69
|
+
Log4r.define_levels(*Log4r::Log4rConfig::LogLevels)
|
70
|
+
|
71
|
+
@log = Log4r::Logger.new(sprintf('rouster:%s', @name))
|
72
|
+
@log.outputters = Log4r::Outputter.stderr
|
73
|
+
@log.level = @verbosity
|
74
|
+
|
75
|
+
@log.debug('Vagrantfile and VM name validation..')
|
76
|
+
unless File.file?(@vagrantfile)
|
77
|
+
raise InternalError.new(sprintf('specified Vagrantfile [%s] does not exist', @vagrantfile))
|
78
|
+
end
|
79
|
+
|
80
|
+
raise InternalError.new() if @name.nil?
|
81
|
+
|
82
|
+
return if opts[:unittest].eql?(true) # quick return if we're a unit test
|
83
|
+
|
84
|
+
# this is breaking test/functional/test_caching.rb test_ssh_caching (if the VM was not running when the test started)
|
85
|
+
# it slows down object instantiation, but is a good test to ensure the machine name is valid..
|
86
|
+
begin
|
87
|
+
self.status()
|
88
|
+
rescue Rouster::LocalExecutionError
|
89
|
+
raise InternalError.new()
|
90
|
+
end
|
91
|
+
|
92
|
+
begin
|
93
|
+
self._run('which vagrant')
|
94
|
+
rescue
|
95
|
+
raise ExternalError.new('vagrant not found in path')
|
96
|
+
end
|
97
|
+
|
98
|
+
@log.debug('SSH key discovery and viability tests..')
|
99
|
+
if @sshkey.nil?
|
100
|
+
if @passthrough.eql?(true)
|
101
|
+
raise InternalError.new('must specify sshkey when using a passthrough host')
|
102
|
+
else
|
103
|
+
# ref the key from the vagrant home dir if it's been overridden
|
104
|
+
@sshkey = sprintf('%s/insecure_private_key', ENV['VAGRANT_HOME']) if ENV['VAGRANT_HOME']
|
105
|
+
@sshkey = sprintf('%s/.vagrant.d/insecure_private_key', ENV['HOME']) unless ENV['VAGRANT_HOME']
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
begin
|
110
|
+
raise InternalError.new('ssh key not specified') if @sshkey.nil?
|
111
|
+
raise InternalError.new('ssh key does not exist') unless File.file?(@sshkey)
|
112
|
+
self.check_key_permissions(@sshkey)
|
113
|
+
rescue => e
|
114
|
+
raise InternalError.new("specified key [#{@sshkey}] has bad permissions. Vagrant exception: [#{e.message}]")
|
115
|
+
end
|
116
|
+
|
117
|
+
if @sshtunnel
|
118
|
+
self.up()
|
119
|
+
end
|
120
|
+
|
121
|
+
@log.info('Rouster object successfully instantiated')
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
##
|
126
|
+
# inspect
|
127
|
+
#
|
128
|
+
# overloaded method to return useful information about Rouster objects
|
129
|
+
def inspect
|
130
|
+
"name [#{@name}]:
|
131
|
+
is_available_via_ssh?[#{self.is_available_via_ssh?}],
|
132
|
+
passthrough[#{@passthrough}],
|
133
|
+
sshkey[#{@sshkey}],
|
134
|
+
status[#{self.status()}],
|
135
|
+
sudo[#{@sudo}],
|
136
|
+
vagrantfile[#{@vagrantfile}],
|
137
|
+
verbosity[#{@verbosity}]\n"
|
138
|
+
end
|
139
|
+
|
140
|
+
## Vagrant methods
|
141
|
+
|
142
|
+
##
|
143
|
+
# up
|
144
|
+
# runs `vagrant up` from the Vagrantfile path
|
145
|
+
# if :sshtunnel is passed to the object during instantiation, the tunnel is created here as well
|
146
|
+
def up
|
147
|
+
@log.info('up()')
|
148
|
+
self.vagrant(sprintf('up %s', @name))
|
149
|
+
|
150
|
+
@ssh_info = nil # in case the ssh-info has changed, a la destroy/rebuild
|
151
|
+
self.connect_ssh_tunnel() if @sshtunnel
|
152
|
+
end
|
153
|
+
|
154
|
+
##
|
155
|
+
# destroy
|
156
|
+
# runs `vagrant destroy <name>` from the Vagrantfile path
|
157
|
+
def destroy
|
158
|
+
@log.info('destroy()')
|
159
|
+
disconnect_ssh_tunnel
|
160
|
+
self.vagrant(sprintf('destroy -f %s', @name))
|
161
|
+
end
|
162
|
+
|
163
|
+
##
|
164
|
+
# status
|
165
|
+
#
|
166
|
+
# runs `vagrant status <name>` from the Vagrantfile path
|
167
|
+
# parses the status and provider out of output, but only status is returned
|
168
|
+
def status
|
169
|
+
status = nil
|
170
|
+
|
171
|
+
if @cache_timeout
|
172
|
+
if @cache.has_key?(:status)
|
173
|
+
if (Time.now.to_i - @cache[:status][:time]) < @cache_timeout
|
174
|
+
@log.debug(sprintf('using cached status[%s] from [%s]', @cache[:status][:status], @cache[:status][:time]))
|
175
|
+
return @cache[:status][:status]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
@log.info('status()')
|
181
|
+
self.vagrant(sprintf('status %s', @name))
|
182
|
+
|
183
|
+
# else case here is handled by non-0 exit code
|
184
|
+
if self.get_output().match(/^#{@name}\s*(.*\s?\w+)\s\((.+)\)$/)
|
185
|
+
# vagrant 1.2+, $1 = status, $2 = provider
|
186
|
+
status = $1
|
187
|
+
elsif self.get_output().match(/^#{@name}\s+(.+)$/)
|
188
|
+
# vagrant 1.2-, $1 = status
|
189
|
+
status = $1
|
190
|
+
end
|
191
|
+
|
192
|
+
if @cache_timeout
|
193
|
+
@cache[:status] = Hash.new unless @cache[:status].class.eql?(Hash)
|
194
|
+
@cache[:status][:time] = Time.now.to_i
|
195
|
+
@cache[:status][:status] = status
|
196
|
+
@log.debug(sprintf('caching status[%s] at [%s]', @cache[:status][:status], @cache[:status][:time]))
|
197
|
+
end
|
198
|
+
|
199
|
+
return status
|
200
|
+
end
|
201
|
+
|
202
|
+
##
|
203
|
+
# suspend
|
204
|
+
#
|
205
|
+
# runs `vagrant suspend <name>` from the Vagrantfile path
|
206
|
+
def suspend
|
207
|
+
@log.info('suspend()')
|
208
|
+
disconnect_ssh_tunnel()
|
209
|
+
self.vagrant(sprintf('suspend %s', @name))
|
210
|
+
end
|
211
|
+
|
212
|
+
## internal methods
|
213
|
+
#private -- commented out so that unit tests can pass, should probably use the 'make all private methods public' method discussed in issue #28
|
214
|
+
|
215
|
+
##
|
216
|
+
# run
|
217
|
+
#
|
218
|
+
# runs a command inside the Vagrant VM
|
219
|
+
#
|
220
|
+
# returns output (STDOUT and STDERR) from command run, sets @exitcode
|
221
|
+
# currently determines exitcode by tacking a 'echo $?' onto the command being run, which is then parsed out before returning
|
222
|
+
#
|
223
|
+
# parameters
|
224
|
+
# * <command> - the command to run (sudo will be prepended if specified in object instantiation)
|
225
|
+
# * [expected_exitcode] - allows for non-0 exit codes to be returned without requiring exception handling
|
226
|
+
def run(command, expected_exitcode=[0])
|
227
|
+
|
228
|
+
if @ssh.nil?
|
229
|
+
self.connect_ssh_tunnel
|
230
|
+
end
|
231
|
+
|
232
|
+
output = nil
|
233
|
+
expected_exitcode = [expected_exitcode] unless expected_exitcode.class.eql?(Array) # yuck, but 2.0 no longer coerces strings into single element arrays
|
234
|
+
|
235
|
+
cmd = sprintf('%s%s; echo ec[$?]', self.uses_sudo? ? 'sudo ' : '', command)
|
236
|
+
@log.info(sprintf('vm running: [%s]', cmd))
|
237
|
+
|
238
|
+
output = @ssh.exec!(cmd)
|
239
|
+
if output.match(/ec\[(\d+)\]/)
|
240
|
+
@exitcode = $1.to_i
|
241
|
+
output.gsub!(/ec\[(\d+)\]\n/, '')
|
242
|
+
else
|
243
|
+
@exitcode = 1
|
244
|
+
end
|
245
|
+
|
246
|
+
self.output.push(output)
|
247
|
+
@log.debug(sprintf('output: [%s]', output))
|
248
|
+
|
249
|
+
unless expected_exitcode.member?(@exitcode)
|
250
|
+
raise RemoteExecutionError.new("output[#{output}], exitcode[#{@exitcode}], expected[#{expected_exitcode}]")
|
251
|
+
end
|
252
|
+
|
253
|
+
@exitcode ||= 0
|
254
|
+
output
|
255
|
+
end
|
256
|
+
|
257
|
+
##
|
258
|
+
# is_available_via_ssh?
|
259
|
+
#
|
260
|
+
# returns true or false after:
|
261
|
+
# * attempting to establish SSH tunnel if it is not currently up/open
|
262
|
+
# * running a functional test of the tunnel
|
263
|
+
def is_available_via_ssh?
|
264
|
+
res = nil
|
265
|
+
|
266
|
+
if @cache_timeout
|
267
|
+
if @cache.has_key?(:is_available_via_ssh?)
|
268
|
+
if (Time.now.to_i - @cache[:is_available_via_ssh?][:time]) < @cache_timeout
|
269
|
+
@log.debug(sprintf('using cached is_available_via_ssh?[%s] from [%s]', @cache[:is_available_via_ssh?][:status], @cache[:is_available_via_ssh?][:time]))
|
270
|
+
return @cache[:is_available_via_ssh?][:status]
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
if @ssh.nil? or @ssh.closed?
|
276
|
+
begin
|
277
|
+
self.connect_ssh_tunnel()
|
278
|
+
rescue Rouster::InternalError, Net::SSH::Disconnect => e
|
279
|
+
res = false
|
280
|
+
end
|
281
|
+
|
282
|
+
end
|
283
|
+
|
284
|
+
if res.nil?
|
285
|
+
begin
|
286
|
+
self.run('echo functional test of SSH tunnel')
|
287
|
+
rescue
|
288
|
+
res = false
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
res = true if res.nil?
|
293
|
+
|
294
|
+
if @cache_timeout
|
295
|
+
@cache[:is_available_via_ssh?] = Hash.new unless @cache[:is_available_via_ssh?].class.eql?(Hash)
|
296
|
+
@cache[:is_available_via_ssh?][:time] = Time.now.to_i
|
297
|
+
@cache[:is_available_via_ssh?][:status] = res
|
298
|
+
@log.debug(sprintf('caching is_available_via_ssh?[%s] at [%s]', @cache[:is_available_via_ssh?][:status], @cache[:is_available_via_ssh?][:time]))
|
299
|
+
end
|
300
|
+
|
301
|
+
res
|
302
|
+
end
|
303
|
+
|
304
|
+
##
|
305
|
+
# sandbox_available?
|
306
|
+
#
|
307
|
+
# returns true or false after attempting to find out if the sandbox
|
308
|
+
# subcommand is available
|
309
|
+
def sandbox_available?
|
310
|
+
if @cache.has_key?(:sandbox_available?)
|
311
|
+
@log.debug(sprintf('using cached sandbox_available?[%s]', @cache[:sandbox_available?]))
|
312
|
+
return @cache[:sandbox_available?]
|
313
|
+
end
|
314
|
+
|
315
|
+
@log.info('sandbox_available()')
|
316
|
+
self._run(sprintf('cd %s; vagrant', File.dirname(@vagrantfile))) # calling 'vagrant' without parameters to determine available faces
|
317
|
+
|
318
|
+
sandbox_available = false
|
319
|
+
if self.get_output().match(/^\s+sandbox$/)
|
320
|
+
sandbox_available = true
|
321
|
+
end
|
322
|
+
|
323
|
+
@cache[:sandbox_available?] = sandbox_available
|
324
|
+
@log.debug(sprintf('caching sandbox_available?[%s]', @cache[:sandbox_available?]))
|
325
|
+
@log.error('sandbox support is not available, please install the "sahara" gem first, https://github.com/jedi4ever/sahara') unless sandbox_available
|
326
|
+
|
327
|
+
return sandbox_available
|
328
|
+
end
|
329
|
+
|
330
|
+
##
|
331
|
+
# sandbox_on
|
332
|
+
# runs `vagrant sandbox on` from the Vagrantfile path
|
333
|
+
def sandbox_on
|
334
|
+
# TODO should we raise() if sandbox is unavailable?
|
335
|
+
self.vagrant(sprintf('sandbox on %s', @name)) if self.sandbox_available?
|
336
|
+
end
|
337
|
+
|
338
|
+
##
|
339
|
+
# sandbox_off
|
340
|
+
# runs `vagrant sandbox off` from the Vagrantfile path
|
341
|
+
def sandbox_off
|
342
|
+
self.vagrant(sprintf('sandbox off %s', @name)) if self.sandbox_available?
|
343
|
+
end
|
344
|
+
|
345
|
+
##
|
346
|
+
# sandbox_rollback
|
347
|
+
# runs `vagrant sandbox rollback` from the Vagrantfile path
|
348
|
+
def sandbox_rollback
|
349
|
+
if self.sandbox_available?
|
350
|
+
self.disconnect_ssh_tunnel
|
351
|
+
self.vagrant(sprintf('sandbox rollback %s', @name))
|
352
|
+
self.connect_ssh_tunnel
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
##
|
357
|
+
# sandbox_commit
|
358
|
+
# runs `vagrant sandbox commit` from the Vagrantfile path
|
359
|
+
def sandbox_commit
|
360
|
+
if self.sandbox_available?
|
361
|
+
self.disconnect_ssh_tunnel
|
362
|
+
self.vagrant(sprintf('sandbox commit %s', @name))
|
363
|
+
self.connect_ssh_tunnel
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
##
|
368
|
+
# get_ssh_info
|
369
|
+
#
|
370
|
+
# runs `vagrant ssh-config <name>` from the Vagrantfile path
|
371
|
+
#
|
372
|
+
# returns a hash containing required data for opening an SSH connection to a VM, to be consumed by connect_ssh_tunnel()
|
373
|
+
def get_ssh_info
|
374
|
+
|
375
|
+
h = Hash.new()
|
376
|
+
|
377
|
+
if @ssh_info.class.eql?(Hash)
|
378
|
+
h = @ssh_info
|
379
|
+
else
|
380
|
+
|
381
|
+
res = self.vagrant(sprintf('ssh-config %s', @name))
|
382
|
+
|
383
|
+
res.split("\n").each do |line|
|
384
|
+
if line.match(/HostName (.*?)$/)
|
385
|
+
h[:hostname] = $1
|
386
|
+
elsif line.match(/User (\w*?)$/)
|
387
|
+
h[:user] = $1
|
388
|
+
elsif line.match(/Port (\d*?)$/)
|
389
|
+
h[:ssh_port] = $1
|
390
|
+
elsif line.match(/IdentityFile (.*?)$/)
|
391
|
+
# TODO what to do if the user has specified @sshkey ?
|
392
|
+
h[:identity_file] = $1
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
@ssh_info = h
|
397
|
+
end
|
398
|
+
|
399
|
+
h
|
400
|
+
end
|
401
|
+
|
402
|
+
##
|
403
|
+
# connect_ssh_tunnel
|
404
|
+
#
|
405
|
+
# instantiates a Net::SSH persistent connection to the Vagrant VM
|
406
|
+
#
|
407
|
+
# raises its own InternalError if the machine isn't running, otherwise returns Net::SSH connection object
|
408
|
+
def connect_ssh_tunnel
|
409
|
+
@log.debug('opening SSH tunnel..')
|
410
|
+
|
411
|
+
status = self.status()
|
412
|
+
if status.eql?('running')
|
413
|
+
self.get_ssh_info()
|
414
|
+
@ssh = Net::SSH.start(@ssh_info[:hostname], @ssh_info[:user], :port => @ssh_info[:ssh_port], :keys => [@sshkey], :paranoid => false)
|
415
|
+
else
|
416
|
+
raise InternalError.new(sprintf('VM is not running[%s], unable open SSH tunnel', status))
|
417
|
+
end
|
418
|
+
|
419
|
+
@ssh
|
420
|
+
end
|
421
|
+
|
422
|
+
##
|
423
|
+
# disconnect_ssh_tunnel
|
424
|
+
#
|
425
|
+
# shuts down the persistent Net::SSH tunnel
|
426
|
+
#
|
427
|
+
def disconnect_ssh_tunnel
|
428
|
+
@log.debug('closing SSH tunnel..')
|
429
|
+
|
430
|
+
@ssh.shutdown! unless @ssh.nil?
|
431
|
+
@ssh = nil
|
432
|
+
end
|
433
|
+
|
434
|
+
##
|
435
|
+
# os_type
|
436
|
+
#
|
437
|
+
# attempts to determine VM operating system based on `uname -a` output, supports OSX, Sun|Solaris, Ubuntu and Redhat
|
438
|
+
def os_type
|
439
|
+
|
440
|
+
if @ostype
|
441
|
+
return @ostype
|
442
|
+
end
|
443
|
+
|
444
|
+
res = nil
|
445
|
+
uname = self.run('uname -a')
|
446
|
+
|
447
|
+
case uname
|
448
|
+
when /Darwin/i
|
449
|
+
res = :osx
|
450
|
+
when /Sun|Solaris/i
|
451
|
+
res =:solaris
|
452
|
+
when /Ubuntu/i
|
453
|
+
res = :ubuntu
|
454
|
+
when /Debian/i
|
455
|
+
res = :debian
|
456
|
+
else
|
457
|
+
if self.is_file?('/etc/redhat-release')
|
458
|
+
res = :redhat
|
459
|
+
else
|
460
|
+
res = nil
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
@ostype = res
|
465
|
+
res
|
466
|
+
end
|
467
|
+
|
468
|
+
##
|
469
|
+
# get
|
470
|
+
#
|
471
|
+
# downloads a file from VM to host
|
472
|
+
#
|
473
|
+
# parameters
|
474
|
+
# * <remote_file> - full or relative path (based on ~vagrant) of file to download
|
475
|
+
# * [local_file] - full or relative path (based on $PWD) of file to download to
|
476
|
+
#
|
477
|
+
# if no local_file is specified, will be downloaded to $PWD with the same shortname as it had in the VM
|
478
|
+
#
|
479
|
+
# returns true on successful download, false if the file DNE and raises a FileTransferError.. well, you know
|
480
|
+
def get(remote_file, local_file=nil)
|
481
|
+
# TODO what happens when we pass a wildcard as remote_file?
|
482
|
+
|
483
|
+
local_file = local_file.nil? ? File.basename(remote_file) : local_file
|
484
|
+
@log.debug(sprintf('scp from VM[%s] to host[%s]', remote_file, local_file))
|
485
|
+
|
486
|
+
begin
|
487
|
+
@ssh.scp.download!(remote_file, local_file)
|
488
|
+
rescue => e
|
489
|
+
raise FileTransferError.new(sprintf('unable to get[%s], exception[%s]', remote_file, e.message()))
|
490
|
+
end
|
491
|
+
|
492
|
+
return true
|
493
|
+
end
|
494
|
+
|
495
|
+
##
|
496
|
+
# put
|
497
|
+
#
|
498
|
+
# uploads a file from host to VM
|
499
|
+
#
|
500
|
+
# parameters
|
501
|
+
# * <local_file> - full or relative path (based on $PWD) of file to upload
|
502
|
+
# * [remote_file] - full or relative path (based on ~vagrant) of filename to upload to
|
503
|
+
def put(local_file, remote_file=nil)
|
504
|
+
remote_file = remote_file.nil? ? File.basename(local_file) : remote_file
|
505
|
+
@log.debug(sprintf('scp from host[%s] to VM[%s]', local_file, remote_file))
|
506
|
+
|
507
|
+
raise FileTransferError.new(sprintf('unable to put[%s], local file does not exist', local_file)) unless File.file?(local_file)
|
508
|
+
|
509
|
+
begin
|
510
|
+
@ssh.scp.upload!(local_file, remote_file)
|
511
|
+
rescue => e
|
512
|
+
raise FileTransferError.new(sprintf('unable to put[%s], exception[%s]', local_file, e.message()))
|
513
|
+
end
|
514
|
+
|
515
|
+
end
|
516
|
+
|
517
|
+
##
|
518
|
+
# is_passthrough?
|
519
|
+
#
|
520
|
+
# convenience getter for @passthrough truthiness
|
521
|
+
def is_passthrough?
|
522
|
+
self.passthrough.eql?(true)
|
523
|
+
end
|
524
|
+
|
525
|
+
##
|
526
|
+
# uses_sudo?
|
527
|
+
#
|
528
|
+
# convenience getter for @sudo truthiness
|
529
|
+
def uses_sudo?
|
530
|
+
self.sudo.eql?(true)
|
531
|
+
end
|
532
|
+
|
533
|
+
##
|
534
|
+
# rebuild
|
535
|
+
#
|
536
|
+
# destroy and then up the machine in question
|
537
|
+
def rebuild
|
538
|
+
@log.debug('rebuild()')
|
539
|
+
self.destroy
|
540
|
+
self.up
|
541
|
+
end
|
542
|
+
|
543
|
+
##
|
544
|
+
# restart
|
545
|
+
#
|
546
|
+
# runs `shutdown -rf now` in the VM, optionally waits for machine to come back to life
|
547
|
+
#
|
548
|
+
# parameters
|
549
|
+
# * [wait] - number of seconds to wait until is_available_via_ssh?() returns true before assuming failure
|
550
|
+
def restart(wait=nil)
|
551
|
+
@log.debug('restart()')
|
552
|
+
|
553
|
+
if self.is_passthrough? and self.passthrough.eql?(local)
|
554
|
+
@log.warn(sprintf('intercepted [restart] sent to a local passthrough, no op'))
|
555
|
+
return nil
|
556
|
+
end
|
557
|
+
|
558
|
+
case os_type
|
559
|
+
when :osx
|
560
|
+
self.run('shutdown -r now')
|
561
|
+
when :redhat, :ubuntu, :debian
|
562
|
+
self.run('/sbin/shutdown -rf now')
|
563
|
+
when :solaris
|
564
|
+
self.run('shutdown -y -i5 -g0')
|
565
|
+
else
|
566
|
+
raise InternalError.new(sprintf('unsupported OS[%s]', @ostype))
|
567
|
+
end
|
568
|
+
|
569
|
+
@ssh, @ssh_info = nil # severing the SSH tunnel, getting ready in case this box is brought back up on a different port
|
570
|
+
|
571
|
+
if wait
|
572
|
+
inc = wait.to_i / 10
|
573
|
+
0..wait.each do |e|
|
574
|
+
@log.debug(sprintf('waiting for reboot: round[%s], step[%s], total[%s]', e, inc, wait))
|
575
|
+
return true if self.is_available_via_ssh?()
|
576
|
+
sleep inc
|
577
|
+
end
|
578
|
+
|
579
|
+
return false
|
580
|
+
end
|
581
|
+
|
582
|
+
return true
|
583
|
+
end
|
584
|
+
|
585
|
+
##
|
586
|
+
# _run
|
587
|
+
#
|
588
|
+
# (should be) private method that executes commands on the local host (not guest VM)
|
589
|
+
#
|
590
|
+
# returns STDOUT|STDERR, raises Rouster::LocalExecutionError on non 0 exit code
|
591
|
+
# sets @exitcode
|
592
|
+
#
|
593
|
+
# parameters
|
594
|
+
# * <command> - command to be run
|
595
|
+
def _run(command)
|
596
|
+
tmp_file = sprintf('/tmp/rouster.%s.%s', Time.now.to_i, $$)
|
597
|
+
cmd = sprintf('%s > %s 2> %s', command, tmp_file, tmp_file) # this is a holdover from Salesforce::Vagrant, can we use '2&>1' here?
|
598
|
+
res = `#{cmd}` # what does this actually hold?
|
599
|
+
|
600
|
+
@log.info(sprintf('host running: [%s]', cmd))
|
601
|
+
|
602
|
+
output = File.read(tmp_file)
|
603
|
+
File.delete(tmp_file) or raise InternalError.new(sprintf('unable to delete [%s]: %s', tmp_file, $!))
|
604
|
+
|
605
|
+
unless $?.success?
|
606
|
+
raise LocalExecutionError.new(sprintf('command [%s] exited with code [%s], output [%s]', cmd, $?.to_i(), output))
|
607
|
+
end
|
608
|
+
|
609
|
+
self.output.push(output)
|
610
|
+
@log.debug(sprintf('output: [%s]', output))
|
611
|
+
|
612
|
+
@exitcode = $?.to_i()
|
613
|
+
output
|
614
|
+
end
|
615
|
+
|
616
|
+
##
|
617
|
+
# vagrant
|
618
|
+
#
|
619
|
+
# abstraction layer to call vagrant faces
|
620
|
+
#
|
621
|
+
# parameters
|
622
|
+
# * <face> - vagrant face to call (include arguments)
|
623
|
+
def vagrant(face)
|
624
|
+
self._run(sprintf('cd %s; vagrant %s', File.dirname(@vagrantfile), face))
|
625
|
+
end
|
626
|
+
|
627
|
+
##
|
628
|
+
# get_output
|
629
|
+
#
|
630
|
+
# returns output from commands passed through _run() and run()
|
631
|
+
#
|
632
|
+
# if no parameter passed, returns output from the last command run
|
633
|
+
#
|
634
|
+
# parameters
|
635
|
+
# * [index] - positive or negative indexing of LIFO datastructure
|
636
|
+
def get_output(index = 1)
|
637
|
+
index.is_a?(Fixnum) and index > 0 ? self.output[-index] : self.output[index]
|
638
|
+
end
|
639
|
+
|
640
|
+
##
|
641
|
+
# generate_unique_mac
|
642
|
+
#
|
643
|
+
# returns a ~unique, valid MAC
|
644
|
+
# ht http://www.commandlinefu.com/commands/view/7242/generate-random-valid-mac-addresses
|
645
|
+
#
|
646
|
+
# uses prefix 'b88d12' (actually Apple's prefix)
|
647
|
+
# uniqueness is not guaranteed, is really more just 'random'
|
648
|
+
def generate_unique_mac
|
649
|
+
sprintf('b88d12%s', (1..3).map{"%0.2X" % rand(256)}.join('').downcase)
|
650
|
+
end
|
651
|
+
|
652
|
+
##
|
653
|
+
# traverse_up
|
654
|
+
#
|
655
|
+
# overly complex function to find a file (Vagrantfile, in our case) somewhere up the tree
|
656
|
+
#
|
657
|
+
# returns the first matching filename or nil if none found
|
658
|
+
#
|
659
|
+
# parameters
|
660
|
+
# * [startdir] - directory to start looking in, default is current directory
|
661
|
+
# * [filename] - filename you are looking for
|
662
|
+
# * [levels] - number of directory levels to examine, default is 10
|
663
|
+
def traverse_up(startdir=Dir.pwd, filename=nil, levels=10)
|
664
|
+
raise InternalError.new('must specify a filename') if filename.nil?
|
665
|
+
|
666
|
+
@log.debug(sprintf('traverse_up() looking for [%s] in [%s], up to [%s] levels', filename, startdir, levels)) unless @log.nil?
|
667
|
+
|
668
|
+
dirs = startdir.split('/')
|
669
|
+
count = 0
|
670
|
+
|
671
|
+
while count < levels and ! dirs.nil?
|
672
|
+
|
673
|
+
potential = sprintf('%s/Vagrantfile', dirs.join('/'))
|
674
|
+
|
675
|
+
if File.file?(potential)
|
676
|
+
return potential
|
677
|
+
end
|
678
|
+
|
679
|
+
dirs.pop()
|
680
|
+
count += 1
|
681
|
+
end
|
682
|
+
end
|
683
|
+
|
684
|
+
##
|
685
|
+
# check_key_permissions
|
686
|
+
#
|
687
|
+
# checks (and optionally fixes) permissions on the SSH key used to auth to the Vagrant VM
|
688
|
+
#
|
689
|
+
# parameters
|
690
|
+
# * <key> - full path to SSH key
|
691
|
+
# * [fix] - boolean, if true and required, will attempt to set permissions on key to 0400 - default is false
|
692
|
+
def check_key_permissions(key, fix=false)
|
693
|
+
allowed_modes = ['0400', '0600']
|
694
|
+
|
695
|
+
raw = self._run(sprintf('ls -l %s', key))
|
696
|
+
perms = self.parse_ls_string(raw)
|
697
|
+
|
698
|
+
unless allowed_modes.member?(perms[:mode])
|
699
|
+
if fix.eql?(true)
|
700
|
+
self._run(sprintf('chmod 0400 %s', key))
|
701
|
+
return check_key_permissions(key, fix)
|
702
|
+
else
|
703
|
+
raise InternalError.new(sprintf('perms for [%s] are [%s], expecting [%s]', key, perms[:mode], allowed_modes))
|
704
|
+
end
|
705
|
+
end
|
706
|
+
|
707
|
+
unless perms[:owner].eql?(ENV['USER'])
|
708
|
+
if fix.eql?(true)
|
709
|
+
self._run(sprintf('chown %s %s', ENV['USER'], key))
|
710
|
+
return check_key_permissions(key, fix)
|
711
|
+
else
|
712
|
+
raise InternalError.new(sprintf('owner for [%s] is [%s], expecting [%s]', key, perms[:owner], ENV['USER']))
|
713
|
+
end
|
714
|
+
end
|
715
|
+
|
716
|
+
nil
|
717
|
+
end
|
718
|
+
|
719
|
+
end
|
720
|
+
|
721
|
+
class Object
|
722
|
+
##
|
723
|
+
# false?
|
724
|
+
#
|
725
|
+
# convenience method to tell if an object equals false
|
726
|
+
def false?
|
727
|
+
self.eql?(false)
|
728
|
+
end
|
729
|
+
|
730
|
+
##
|
731
|
+
# true?
|
732
|
+
#
|
733
|
+
# convenience method to tell if an object equals true (think .nil? but more useful)
|
734
|
+
def true?
|
735
|
+
self.eql?(true)
|
736
|
+
end
|
737
|
+
end
|