rouster 0.5
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 +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
|