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.
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