rouster 0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/LICENSE +9 -0
  4. data/README.md +175 -0
  5. data/Rakefile +65 -0
  6. data/Vagrantfile +23 -0
  7. data/examples/bootstrap.rb +113 -0
  8. data/examples/demo.rb +71 -0
  9. data/examples/error.rb +30 -0
  10. data/lib/rouster.rb +737 -0
  11. data/lib/rouster/deltas.rb +481 -0
  12. data/lib/rouster/puppet.rb +398 -0
  13. data/lib/rouster/testing.rb +743 -0
  14. data/lib/rouster/tests.rb +596 -0
  15. data/path_helper.rb +21 -0
  16. data/rouster.gemspec +30 -0
  17. data/test/basic.rb +10 -0
  18. data/test/functional/deltas/test_get_crontab.rb +99 -0
  19. data/test/functional/deltas/test_get_groups.rb +48 -0
  20. data/test/functional/deltas/test_get_packages.rb +71 -0
  21. data/test/functional/deltas/test_get_ports.rb +119 -0
  22. data/test/functional/deltas/test_get_services.rb +43 -0
  23. data/test/functional/deltas/test_get_users.rb +45 -0
  24. data/test/functional/puppet/test_facter.rb +59 -0
  25. data/test/functional/test_caching.rb +124 -0
  26. data/test/functional/test_destroy.rb +51 -0
  27. data/test/functional/test_dirs.rb +88 -0
  28. data/test/functional/test_files.rb +64 -0
  29. data/test/functional/test_get.rb +76 -0
  30. data/test/functional/test_inspect.rb +31 -0
  31. data/test/functional/test_is_dir.rb +118 -0
  32. data/test/functional/test_is_file.rb +119 -0
  33. data/test/functional/test_new.rb +92 -0
  34. data/test/functional/test_put.rb +81 -0
  35. data/test/functional/test_rebuild.rb +49 -0
  36. data/test/functional/test_restart.rb +44 -0
  37. data/test/functional/test_run.rb +77 -0
  38. data/test/functional/test_status.rb +38 -0
  39. data/test/functional/test_suspend.rb +31 -0
  40. data/test/functional/test_up.rb +27 -0
  41. data/test/functional/test_validate_file.rb +30 -0
  42. data/test/puppet/manifests/default.pp +9 -0
  43. data/test/puppet/manifests/hiera.yaml +12 -0
  44. data/test/puppet/manifests/hieradata/common.json +3 -0
  45. data/test/puppet/manifests/hieradata/vagrant.json +3 -0
  46. data/test/puppet/manifests/manifest.pp +78 -0
  47. data/test/puppet/modules/role/manifests/ui.pp +5 -0
  48. data/test/puppet/test_apply.rb +149 -0
  49. data/test/puppet/test_roles.rb +186 -0
  50. data/test/tunnel_vs_scp.rb +41 -0
  51. data/test/unit/puppet/test_get_puppet_star.rb +68 -0
  52. data/test/unit/test_generate_unique_mac.rb +43 -0
  53. data/test/unit/test_new.rb +31 -0
  54. data/test/unit/test_parse_ls_string.rb +334 -0
  55. data/test/unit/test_traverse_up.rb +43 -0
  56. data/test/unit/testing/test_meets_constraint.rb +55 -0
  57. data/test/unit/testing/test_validate_file.rb +112 -0
  58. data/test/unit/testing/test_validate_group.rb +72 -0
  59. data/test/unit/testing/test_validate_package.rb +69 -0
  60. data/test/unit/testing/test_validate_port.rb +98 -0
  61. data/test/unit/testing/test_validate_service.rb +73 -0
  62. data/test/unit/testing/test_validate_user.rb +92 -0
  63. 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