rouster 0.5 → 0.7

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.reek +63 -0
  4. data/.travis.yml +11 -0
  5. data/Gemfile +17 -0
  6. data/Gemfile.lock +102 -0
  7. data/README.md +233 -7
  8. data/Rakefile +52 -34
  9. data/Vagrantfile +26 -8
  10. data/examples/aws.rb +85 -0
  11. data/examples/openstack.rb +61 -0
  12. data/examples/passthrough.rb +71 -0
  13. data/lib/rouster.rb +380 -262
  14. data/lib/rouster/deltas.rb +470 -138
  15. data/lib/rouster/puppet.rb +155 -26
  16. data/lib/rouster/testing.rb +205 -46
  17. data/lib/rouster/tests.rb +40 -11
  18. data/lib/rouster/vagrant.rb +311 -0
  19. data/path_helper.rb +3 -4
  20. data/plugins/aws.rb +347 -0
  21. data/plugins/openstack.rb +136 -0
  22. data/test/basic.rb +4 -1
  23. data/test/functional/deltas/test_get_crontab.rb +64 -2
  24. data/test/functional/deltas/test_get_groups.rb +74 -2
  25. data/test/functional/deltas/test_get_os.rb +68 -0
  26. data/test/functional/deltas/test_get_packages.rb +73 -6
  27. data/test/functional/deltas/test_get_ports.rb +26 -1
  28. data/test/functional/deltas/test_get_services.rb +43 -5
  29. data/test/functional/deltas/test_get_users.rb +35 -2
  30. data/test/functional/puppet/test_facter.rb +41 -1
  31. data/test/functional/test_caching.rb +2 -2
  32. data/test/functional/test_inspect.rb +1 -1
  33. data/test/functional/test_is_file.rb +17 -1
  34. data/test/functional/test_is_in_file.rb +40 -0
  35. data/test/functional/test_new.rb +233 -22
  36. data/test/functional/test_passthroughs.rb +94 -0
  37. data/test/functional/test_put.rb +2 -2
  38. data/test/functional/test_validate_file.rb +104 -3
  39. data/test/puppet/test_apply.rb +8 -6
  40. data/test/unit/puppet/resources/puppet_run_with_failed_exec +59 -0
  41. data/test/unit/puppet/resources/puppet_run_with_successful_exec +61 -0
  42. data/test/unit/puppet/test_get_puppet_star.rb +27 -4
  43. data/test/unit/puppet/test_puppet_parsing.rb +44 -0
  44. data/test/unit/test_new.rb +88 -0
  45. data/test/unit/test_parse_ls_string.rb +67 -0
  46. data/test/unit/testing/resources/osx-launchd +285 -0
  47. data/test/unit/testing/resources/rhel-systemd +46 -0
  48. data/test/unit/testing/resources/rhel-systemv +41 -0
  49. data/test/unit/testing/resources/rhel-upstart +20 -0
  50. data/test/unit/testing/test_get_services.rb +178 -0
  51. data/test/unit/testing/test_validate_cron.rb +78 -0
  52. data/test/unit/testing/test_validate_package.rb +36 -10
  53. data/test/unit/testing/test_validate_port.rb +5 -0
  54. metadata +42 -21
  55. data/test/puppet/test_roles.rb +0 -186
data/Rakefile CHANGED
@@ -1,65 +1,83 @@
1
1
  require sprintf('%s/%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
2
2
  require 'rubygems'
3
3
  require 'rake/testtask'
4
+ require 'reek/rake/task'
4
5
 
5
- task :buildgem do
6
+ desc 'build the gem'
7
+ task :build do
6
8
  sh 'gem build rouster.gemspec'
7
9
  end
8
10
 
11
+ desc 'cleanup environment'
12
+ task :clean do
13
+ sh 'rm /tmp/rouster-*'
14
+ end
15
+
9
16
  task :default do
10
17
  sh 'ruby test/basic.rb'
11
18
  end
12
19
 
20
+ desc 'run the example demo'
13
21
  task :demo do
14
22
  sh 'ruby examples/demo.rb'
15
23
  end
16
24
 
25
+ desc 'rdoc generation'
17
26
  task :doc do
18
27
  sh 'rdoc --line-numbers lib/*'
19
28
  end
20
29
 
21
30
  task :examples do
22
31
  Dir['examples/**/*.rb'].each do |example|
23
- sh "ruby #{example}"
24
- end
32
+ sh "ruby #{example}"
33
+ end
25
34
  end
26
35
 
27
- task :reek do
28
- sh "reek lib/**/*.rb"
36
+ desc 'shortcut to vagrant destroy -f'
37
+ task :vdestroy do
38
+ sh 'vagrant destroy -f'
29
39
  end
30
40
 
31
- Rake::TestTask.new do |t|
32
- t.name = 'test'
33
- t.libs << 'lib'
34
- t.test_files = FileList['test/**/test_*.rb']
35
- t.verbose = true
41
+ Reek::Rake::Task.new do |t|
42
+ t.config_file = sprintf('%s/.reek', File.dirname(__FILE__))
43
+ #t.source_files = FileList.new('lib/**/*.rb', 'plugins/**/*.rb')
44
+ t.source_files = FileList.new('lib/**/*.rb')
45
+ t.reek_opts = '--no-wiki-links'
46
+ t.fail_on_error = false
47
+ t.verbose = true
36
48
  end
37
49
 
38
- Rake::TestTask.new do |t|
39
- t.name = 'unit'
40
- t.libs << 'lib'
41
- t.test_files = FileList['test/unit/**/test_*.rb']
42
- t.verbose = true
43
- end
50
+ namespace :test do
51
+ Rake::TestTask.new(:all => :vdestroy) do |t|
52
+ t.description = 'run all tests'
53
+ t.libs << 'lib'
54
+ t.test_files = FileList['test/**/test_*.rb']
55
+ end
44
56
 
45
- Rake::TestTask.new do |t|
46
- t.name = 'functional'
47
- t.libs << 'lib'
48
- t.test_files = FileList['test/functional/**/test_*.rb']
49
- t.verbose = true
50
- end
57
+ Rake::TestTask.new do |t|
58
+ t.name = 'unit'
59
+ t.description = 'run unit tests'
60
+ t.libs << 'lib'
61
+ t.test_files = FileList['test/unit/**/test_*.rb']
62
+ end
51
63
 
52
- Rake::TestTask.new do |t|
53
- t.name = 'deltas'
54
- t.libs << 'lib'
55
- t.test_files = FileList['test/functional/deltas/test_*.rb']
56
- t.verbose = true
57
- end
64
+ Rake::TestTask.new(:functional => :vdestroy) do |t|
65
+ t.description = 'run functional tests'
66
+ t.libs << 'lib'
67
+ t.test_files = FileList['test/functional/**/test_*.rb']
68
+ end
58
69
 
59
- Rake::TestTask.new do |t|
60
- t.name = 'puppet'
61
- t.libs << 'lib'
62
- t.test_files = FileList['test/puppet/test*.rb']
63
- t.verbose = true
64
- end
70
+ Rake::TestTask.new(:deltas => :vdestroy) do |t|
71
+ t.description = 'run delta tests'
72
+ t.libs << 'lib'
73
+ t.test_files = FileList['test/functional/deltas/test_*.rb']
74
+ end
65
75
 
76
+ Rake::TestTask.new do |t|
77
+ t.name = 'puppet'
78
+ t.description = 'run puppet tests'
79
+ t.libs << 'lib'
80
+ t.test_files = FileList['test/puppet/test*.rb']
81
+ t.verbose = true
82
+ end
83
+ end
@@ -1,17 +1,35 @@
1
1
  # stripped down example piab Vagrantfile for rouster
2
2
 
3
- box_url = 'http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210.box'
4
- box_name = 'centos6'
5
- boxes = [:ppm, :app]
3
+ boxes = {
4
+ :ppm => {
5
+ :box_name => 'centos6',
6
+ :box_url => 'http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210.box',
7
+ },
8
+ :app => {
9
+ :box_name => 'centos6',
10
+ :box_url => 'http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210.box',
11
+ },
12
+
13
+ :ubuntu12 => {
14
+ :box_name => 'ubuntu12',
15
+ :box_url => 'http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210.box',
16
+ },
17
+
18
+ :ubuntu13 => {
19
+ :box_name => 'ubuntu13',
20
+ :box_url => 'http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-1310-x64-virtualbox-puppet.box',
21
+ },
22
+
23
+ }
6
24
 
7
25
  Vagrant::Config.run do |config|
8
- boxes.each do |box|
26
+ boxes.each_pair do |box,hash|
9
27
  config.vm.define box do |worker|
10
28
 
11
- worker.vm.box = box_name
12
- worker.vm.box_url = box_url
13
- worker.vm.host_name = box.to_s
14
- worker.vm.network :hostonly, sprintf('10.0.1.%s', rand(253).to_i + 1)
29
+ worker.vm.box = hash[:box_name]
30
+ worker.vm.box_url = hash[:box_url]
31
+ worker.vm.host_name = hash[:box_name]
32
+ worker.vm.network :hostonly, sprintf('10.0.1.%s', rand(253).to_i + 2)
15
33
  worker.ssh.forward_agent = true
16
34
 
17
35
  if box.to_s.eql?('ppm') and File.directory?('../puppet')
@@ -0,0 +1,85 @@
1
+ require sprintf('%s/../%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
2
+
3
+ require 'rouster'
4
+ require 'plugins/aws' # brings in fog and some helpers
5
+
6
+ aws_already_running = Rouster.new(
7
+ :name => 'aws-already-running',
8
+ :passthrough => {
9
+ :type => :aws,
10
+ :instance => 'your-instance-id',
11
+ :key => sprintf('%s/.ssh/id_rsa-aws', ENV['HOME'])
12
+ },
13
+ :verbosity => 1,
14
+ )
15
+
16
+ a = aws_already_running.run('ls -l /etc/hosts; who')
17
+
18
+ aws = Rouster.new(
19
+ :name => 'aws-testing',
20
+ :sudo => false,
21
+ :passthrough => {
22
+ # all required settings
23
+ :type => :aws,
24
+ :keypair => 'your-keypair-name',
25
+ :security_groups => 'integration-testing',
26
+ :key => sprintf('%s/.ssh/id_rsa-aws', ENV['HOME']),
27
+ :userdata => 'foo',
28
+
29
+ # optional, setting to be explicit
30
+ :ami => 'your-ami-id',
31
+ :dns_propagation_sleep => 20,
32
+ :min_count => 1, # TODO don't know how to actually handle multiple machines.. just do the same thing on all of the hosts?
33
+ :max_count => 1,
34
+ :region => 'us-west-2',
35
+ :size => 't1.micro',
36
+ :ssh_port => 22,
37
+ :user => 'ec2-user',
38
+
39
+ :key_id => ENV['AWS_ACCESS_KEY_ID'],
40
+ :secret_key => ENV['AWS_SECRET_ACCESS_KEY'],
41
+ },
42
+ :sshtunnel => false,
43
+ :verbosity => 1,
44
+ )
45
+
46
+ p "up(): #{aws.up}"
47
+
48
+ aws_clone = Rouster.new(
49
+ :name => 'aws-testing-clone',
50
+ :passthrough => {
51
+ :type => :aws,
52
+ :key => sprintf('%s/.ssh/id_rsa-aws', ENV['HOME']),
53
+ :instance => aws.aws_get_instance,
54
+ },
55
+ :verbosity => 1,
56
+ )
57
+
58
+ [ aws, aws_already_running, aws_clone ].each do |a|
59
+ p "aws_get_ami: #{a.aws_get_ami}"
60
+ p "aws_get_instance: #{a.aws_get_instance}"
61
+
62
+ p "status: #{a.status}"
63
+ p "aws_status: #{a.aws_status}" # TODO merge this into status
64
+
65
+ p "aws_get_ip(:internal, :public): #{a.aws_get_ip(:internal, :public)}"
66
+ p "aws_get_ip(:internal, :private): #{a.aws_get_ip(:internal, :private)}"
67
+ p "aws_get_ip(:aws, :public): #{a.aws_get_ip(:aws, :public)}"
68
+ p "aws_get_ip(:aws, :private): #{a.aws_get_ip(:aws, :private)}"
69
+
70
+ p "aws_get_hostname(:internal, :public): #{a.aws_get_hostname(:internal, :public)}"
71
+ p "aws_get_hostname(:internal, :private): #{a.aws_get_hostname(:internal, :private)}"
72
+ p "aws_get_hostname(:aws, :public): #{a.aws_get_hostname(:aws, :public)}"
73
+ p "aws_get_hostname(:aws, :private): #{a.aws_get_hostname(:aws, :private)}"
74
+
75
+ p "run(uptime): #{a.run('uptime')}"
76
+ p "get(/etc/hosts): #{a.get('/etc/hosts')}"
77
+ p "put(/etc/hosts, /tmp): #{a.put('/etc/hosts', '/tmp')}"
78
+
79
+ p "aws_get_userdata: #{a.aws_get_userdata}"
80
+ p "aws_get_metadata: #{a.aws_get_metadata}"
81
+
82
+ p 'DBGZ' if nil?
83
+ end
84
+
85
+ exit
@@ -0,0 +1,61 @@
1
+ require sprintf('%s/../%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
2
+
3
+ require 'rouster'
4
+ require 'plugins/openstack' # brings in fog and some helpers
5
+
6
+ ostack = Rouster.new(
7
+ :name => 'ostack-testing',
8
+ :sudo => false,
9
+ :logfile => true,
10
+ :passthrough => {
11
+ :type => :openstack, # Indicate OpenStack provider
12
+ :openstack_auth_url => 'http://hostname.acme.com:5000/v2.0/tokens', # OpenStack API endpoint
13
+ :openstack_username => 'some_user', # OpenStack console username
14
+ :openstack_tenant => 'tenant_id', # Tenant ID
15
+ :user => 'ssh_user_id', # SSH login ID
16
+ :keypair => 'openstack_key_name', # Name of ssh keypair in OpenStack
17
+ :image_ref => 'c0340afb-577d-1234-87b2-aebdd6d1838f', # Image ID in OpenStack
18
+ :flavor_ref => '547d9af5-096c-1234-98df-7d23162556b8', # Flavor ID in OpenStack
19
+ :openstack_api_key => 'secret_openstack_key', # OpenStack console password
20
+ :key => '/path/to/ssh_keys.pem', # SSH key filename
21
+ },
22
+ :sshtunnel => false,
23
+ :verbosity => 1,
24
+ )
25
+
26
+ p "UP(): #{ostack.up}"
27
+ p "STATUS(): #{ostack.status}"
28
+
29
+ ostack_copy = Rouster.new(
30
+ :name => 'ostack-copy',
31
+ :sudo => false,
32
+ :logfile => true,
33
+ :passthrough => {
34
+ :type => :openstack, # Indicate OpenStack provider
35
+ :openstack_auth_url => 'http://hostname.acme.com:5000/v2.0/tokens', # OpenStack API endpoint
36
+ :openstack_username => 'some_user', # OpenStack console username
37
+ :openstack_tenant => 'tenant_id', # Tenant ID
38
+ :user => 'ssh_user_id', # SSH login ID
39
+ :keypair => 'openstack_key_name', # Name of ssh keypair in OpenStack
40
+ :openstack_api_key => 'secret_openstack_key', # OpenStack console password
41
+ :instance => ostack.ostack_get_instance_id, # ID of a running OpenStack instance.
42
+ },
43
+ :sshtunnel => false,
44
+ :verbosity => 1,
45
+ )
46
+
47
+ [ ostack, ostack_copy ].each do |o|
48
+ p "ostack_get_instance_id: #{o.ostack_get_instance_id}"
49
+
50
+ p "status: #{o.status}"
51
+
52
+ p "ostack_get_ip(): #{o.ostack_get_ip()}"
53
+
54
+ p "run(uptime): #{o.run('uptime')}"
55
+ p "get(/etc/hosts): #{o.get('/etc/hosts')}"
56
+ p "put(/etc/hosts, /tmp): #{o.put('/etc/hosts', '/tmp')}"
57
+
58
+ end
59
+
60
+ p "DESTROY(): #{ostack.destroy}"
61
+ exit
@@ -0,0 +1,71 @@
1
+ require sprintf('%s/../%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
2
+
3
+ require 'rouster'
4
+ require 'rouster/puppet'
5
+ require 'rouster/tests'
6
+
7
+ verbosity = ENV['VERBOSE'].nil? ? 4 : 0
8
+
9
+ # .inspect of this is blank for sshkey and status, looks ugly, but is ~accurate.. fix this?
10
+ local = Rouster.new(
11
+ :name => 'local',
12
+ :sudo => false,
13
+ :passthrough => { :type => :local },
14
+ :verbosity => verbosity,
15
+ )
16
+
17
+ remote = Rouster.new(
18
+ :name => 'remote',
19
+ :sudo => false,
20
+ :passthrough => {
21
+ :type => :remote,
22
+ :host => `hostname`.chomp, # yep, the remote is actually local.. perhaps the right :type would be 'ssh' vs 'shellout' instead..
23
+ :user => ENV['USER'],
24
+ :key => sprintf('%s/.ssh/id_dsa', ENV['HOME']),
25
+ },
26
+ :verbosity => verbosity,
27
+ )
28
+
29
+ sudo = Rouster.new(
30
+ :name => 'sudo',
31
+ :sudo => true,
32
+ :passthrough => { :type => :local },
33
+ :verbosity => verbosity,
34
+ )
35
+
36
+ vagrant = Rouster.new(
37
+ :name => 'ppm',
38
+ :sudo => true,
39
+ :verbosity => verbosity,
40
+ )
41
+
42
+ workers = [ local, remote, vagrant ]
43
+
44
+ workers = [vagrant]
45
+
46
+ workers.each do |r|
47
+ p r
48
+
49
+ ## vagrant command testing
50
+ r.up()
51
+ r.suspend()
52
+ #r.destroy()
53
+ r.up()
54
+
55
+ p r.status() # why is this giving us nil after initial call? want to blame caching, but not sure
56
+
57
+ r.is_vagrant_running?()
58
+ r.sandbox_available?()
59
+
60
+ if r.sandbox_available?()
61
+ r.sandbox_on()
62
+ r.sandbox_off()
63
+ r.sandbox_rollback()
64
+ r.sandbox_commit()
65
+ end
66
+
67
+ p r.run('echo foo')
68
+
69
+ end
70
+
71
+ exit
@@ -7,11 +7,12 @@ require 'net/ssh'
7
7
  require sprintf('%s/../%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
8
8
 
9
9
  require 'rouster/tests'
10
+ require 'rouster/vagrant'
10
11
 
11
12
  class Rouster
12
13
 
13
14
  # sporadically updated version number
14
- VERSION = 0.50
15
+ VERSION = 0.70
15
16
 
16
17
  # custom exceptions -- what else do we want them to include/do?
17
18
  class ArgumentError < StandardError; end # thrown by methods that take parameters from users
@@ -20,42 +21,66 @@ class Rouster
20
21
  class ExternalError < StandardError; end # thrown when external dependencies do not respond as expected
21
22
  class LocalExecutionError < StandardError; end # thrown by _run()
22
23
  class RemoteExecutionError < StandardError; end # thrown by run()
24
+ class PassthroughError < StandardError; end # thrown by anything Passthrough related (mostly vagrant.rb)
23
25
  class SSHConnectionError < StandardError; end # thrown by available_via_ssh() -- and potentially _run()
24
26
 
25
- attr_accessor :facts, :sudo, :verbosity
26
- attr_reader :cache, :cache_timeout, :deltas, :exitcode, :log, :name, :output, :passthrough, :sshkey, :unittest, :vagrantfile
27
+ attr_accessor :facts, :last_puppet_run
28
+ attr_reader :cache, :cache_timeout, :deltas, :exitcode, :logger, :name, :output, :passthrough, :retries, :sshkey, :unittest, :vagrantbinary, :vagrantfile
27
29
 
28
30
  ##
29
31
  # initialize - object instantiation
30
32
  #
31
33
  # 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)
34
+ # * <name> - the name of the VM as specified in the Vagrantfile
35
+ # * [cache_timeout] - integer specifying how long Rouster should cache status() and is_available_via_ssh?() results, default is false
36
+ # * [logfile] - allows logging to an external file, if passed true, generates a dynamic filename, otherwise uses what is passed, default is false
37
+ # * [passthrough] - boolean of whether this is a VM or passthrough, default is false -- passthrough is not completely implemented
38
+ # * [retries] - integer specifying number of retries Rouster should attempt when running external (currently only vagrant()) commands
39
+ # * [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/)
40
+ # * [sshtunnel] - boolean of whether or not to instantiate the SSH tunnel upon upping the VM, default is true
41
+ # * [sudo] - boolean of whether or not to prefix commands run in VM with 'sudo', default is true
42
+ # * [vagrantfile] - the full or relative path to the Vagrantfile to use, if not specified, will look for one in 5 directories above current location
43
+ # * [vagrant_concurrency] - boolean controlling whether Rouster will attempt to run `vagrant *` if another vagrant process is already running, default is false
44
+ # * [vagrant_reboot] - particularly sticky systems restart better if Vagrant does it for us, default is false
45
+ # * [verbosity] - an integer representing console level logging, or an array of integers representing console,file level logging - DEBUG (0) < INFO (1) < WARN (2) < ERROR (3) < FATAL (4)
40
46
  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
47
+ @cache_timeout = opts[:cache_timeout].nil? ? false : opts[:cache_timeout]
48
+ @logfile = opts[:logfile].nil? ? false : opts[:logfile]
49
+ @name = opts[:name]
50
+ @passthrough = opts[:passthrough].nil? ? false : opts[:passthrough]
51
+ @retries = opts[:retries].nil? ? 0 : opts[:retries]
52
+ @sshkey = opts[:sshkey]
53
+ @sshtunnel = opts[:sshtunnel].nil? ? true : opts[:sshtunnel]
54
+ @unittest = opts[:unittest].nil? ? false : opts[:unittest]
55
+ @vagrantfile = opts[:vagrantfile].nil? ? traverse_up(Dir.pwd, 'Vagrantfile', 5) : opts[:vagrantfile]
56
+ @vagrant_concurrency = opts[:vagrant_concurrency].nil? ? false : opts[:vagrant_concurrency]
57
+ @vagrant_reboot = opts[:vagrant_reboot].nil? ? false : opts[:vagrant_reboot]
58
+
59
+ # TODO kind of want to invert this, 0 = trace, 1 = debug, 2 = info, 3 = warning, 4 = error
60
+ # could do `fixed_ordering = [4, 3, 2, 1, 0]` and use user input as index instead, so an input of 4 (which should be more verbose), yields 0
61
+ if opts[:verbosity]
62
+ # TODO decide how to handle this case -- currently #2 is implemented
63
+ # - option 1, if passed a single integer, use that level for both loggers
64
+ # - option 2, if passed a single integer, use that level for stdout, and a hardcoded level (probably INFO) to logfile
65
+
66
+ # kind of want to do if opts[:verbosity].respond_to?(:[]), but for 1.87 compatability, going this way..
67
+ if ! opts[:verbosity].is_a?(Array) or opts[:verbosity].is_a?(Integer)
68
+ @verbosity_console = opts[:verbosity].to_i
69
+ @verbosity_logfile = 2
70
+ elsif opts[:verbosity].is_a?(Array)
71
+ # TODO more error checking here when we are sure this is the right way to go
72
+ @verbosity_console = opts[:verbosity][0].to_i
73
+ @verbosity_logfile = opts[:verbosity][1].to_i
74
+ @logfile = true if @logfile.eql?(false) # overriding the default setting
75
+ end
54
76
  else
55
- @sudo = true
77
+ @verbosity_console = 3
78
+ @verbosity_logfile = 2 # this is kind of arbitrary, but won't actually be created unless opts[:logfile] is also passed
56
79
  end
57
80
 
58
- @ostype = nil
81
+ @ostype = nil
82
+ @osversion = nil
83
+
59
84
  @output = Array.new
60
85
  @cache = Hash.new
61
86
  @deltas = Hash.new
@@ -68,38 +93,155 @@ class Rouster
68
93
  require 'log4r/config'
69
94
  Log4r.define_levels(*Log4r::Log4rConfig::LogLevels)
70
95
 
71
- @log = Log4r::Logger.new(sprintf('rouster:%s', @name))
72
- @log.outputters = Log4r::Outputter.stderr
73
- @log.level = @verbosity
96
+ @logger = Log4r::Logger.new(sprintf('rouster:%s', @name))
97
+ @logger.outputters << Log4r::Outputter.stderr
98
+ #@log.outputters << Log4r::Outputter.stdout
74
99
 
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))
100
+ if @logfile
101
+ @logfile = @logfile.eql?(true) ? sprintf('/tmp/rouster-%s.%s.%s.log', @name, Time.now.to_i, $$) : @logfile
102
+ @logger.outputters << Log4r::FileOutputter.new(sprintf('rouster:%s', @name), :filename => @logfile, :level => @verbosity_logfile)
78
103
  end
79
104
 
80
- raise InternalError.new() if @name.nil?
81
-
82
- return if opts[:unittest].eql?(true) # quick return if we're a unit test
105
+ @logger.outputters[0].level = @verbosity_console # can't set this when instantiating a .std* logger, and want the FileOutputter at a different level
83
106
 
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()
107
+ if opts.has_key?(:sudo)
108
+ @sudo = opts[:sudo]
109
+ elsif @passthrough.class.eql?(Hash)
110
+ @logger.debug(sprintf('passthrough without sudo specification, defaulting to false'))
111
+ @sudo = false
112
+ else
113
+ @sudo = true
90
114
  end
91
115
 
92
- begin
93
- self._run('which vagrant')
94
- rescue
95
- raise ExternalError.new('vagrant not found in path')
96
- end
116
+ if @passthrough
117
+ @vagrantbinary = 'vagrant' # hacky fix to is_vagrant_running?() grepping, doesn't need to actually be in $PATH
118
+ @sshtunnel = opts[:sshtunnel].nil? ? false : @sshtunnel # unless user has specified it, non-local passthroughs default to not open tunnel
119
+
120
+ defaults = {
121
+ :paranoid => false, # valid overrides are: false, true, :very, or :secure
122
+ :ssh_sleep_ceiling => 9,
123
+ :ssh_sleep_time => 10,
124
+ }
125
+
126
+ @passthrough = defaults.merge(@passthrough)
127
+
128
+ if @passthrough.class != Hash
129
+ raise ArgumentError.new('passthrough specification should be hash')
130
+ elsif @passthrough[:type].nil?
131
+ raise ArgumentError.new('passthrough :type must be specified, :local, :remote or :aws allowed')
132
+ elsif @passthrough[:type].eql?(:local)
133
+ @logger.debug('instantiating a local passthrough worker')
134
+ @sshtunnel = opts[:sshtunnel].nil? ? true : opts[:sshtunnel] # override default, if local, open immediately
135
+
136
+ elsif @passthrough[:type].eql?(:remote)
137
+ @logger.debug('instantiating a remote passthrough worker')
138
+
139
+ [:host, :user, :key].each do |r|
140
+ raise ArgumentError.new(sprintf('remote passthrough requires[%s] specification', r)) if @passthrough[r].nil?
141
+ end
142
+
143
+ raise ArgumentError.new('remote passthrough requires valid :key specification, should be path to private half') unless File.file?(@passthrough[:key])
144
+ @sshkey = @passthrough[:key] # TODO refactor so that you don't have to do this..
145
+
146
+ elsif @passthrough[:type].eql?(:aws) or @passthrough[:type].eql?(:raiden)
147
+ @logger.debug(sprintf('instantiating an %s passthrough worker', @passthrough[:type]))
148
+
149
+ aws_defaults = {
150
+ :ami => 'ami-7bdaa84b', # RHEL 6.5 x64 in us-west-2
151
+ :dns_propagation_sleep => 30, # how much time to wait after ELB creation before attempting to connect
152
+ :elb_cleanup => false,
153
+ :key_id => ENV['AWS_ACCESS_KEY_ID'],
154
+ :min_count => 1,
155
+ :max_count => 1,
156
+ :region => 'us-west-2',
157
+ :secret_key => ENV['AWS_SECRET_ACCESS_KEY'],
158
+ :size => 't1.micro',
159
+ :ssh_port => 22,
160
+ :user => 'ec2-user',
161
+ }
162
+
163
+ if @passthrough.has_key?(:ami)
164
+ @logger.debug(':ami specified, will start new EC2 instance')
165
+
166
+ @passthrough[:security_groups] = @passthrough[:security_groups].is_a?(Array) ? @passthrough[:security_groups] : [ @passthrough[:security_groups] ]
167
+
168
+ @passthrough = aws_defaults.merge(@passthrough)
169
+
170
+ [:ami, :size, :user, :region, :key, :keypair, :key_id, :secret_key, :security_groups].each do |r|
171
+ raise ArgumentError.new(sprintf('AWS passthrough requires %s specification', r)) if @passthrough[r].nil?
172
+ end
173
+
174
+ elsif @passthrough.has_key?(:instance)
175
+ @logger.debug(':instance specified, will connect to existing EC2 instance')
176
+
177
+ @passthrough = aws_defaults.merge(@passthrough)
178
+
179
+ if @passthrough[:type].eql?(:aws)
180
+ @passthrough[:host] = self.aws_describe_instance(@passthrough[:instance])['dnsName']
181
+ else
182
+ @passthrough[:host] = self.find_ssh_elb(true)
183
+ end
184
+
185
+ [:instance, :key, :user, :host].each do |r|
186
+ raise ArgumentError.new(sprintf('AWS passthrough requires [%s] specification', r)) if @passthrough[r].nil?
187
+ end
188
+
189
+ else
190
+ raise ArgumentError.new('AWS passthrough requires either :ami or :instance specification')
191
+ end
192
+
193
+ raise ArgumentError.new('AWS passthrough requires valid :sshkey specification, should be path to private half') unless File.file?(@passthrough[:key])
194
+ @sshkey = @passthrough[:key]
195
+ elsif @passthrough[:type].eql?(:openstack)
196
+ @logger.debug(sprintf('instantiating an %s passthrough worker', @passthrough[:type]))
197
+ @sshkey = @passthrough[:key]
198
+
199
+ ostack_defaults = {
200
+ :ssh_port => 22,
201
+ }
202
+ @passthrough = ostack_defaults.merge(@passthrough)
203
+
204
+ [:openstack_auth_url, :openstack_username, :openstack_tenant, :openstack_api_key,
205
+ :key ].each do |r|
206
+ raise ArgumentError.new(sprintf('OpenStack passthrough requires %s specification', r)) if @passthrough[r].nil?
207
+ end
97
208
 
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')
209
+ if @passthrough.has_key?(:image_ref)
210
+ @logger.debug(':image_ref specified, will start new Nova instance')
211
+ elsif @passthrough.has_key?(:instance)
212
+ @logger.debug(':instance specified, will connect to existing OpenStack instance')
213
+ inst_details = self.ostack_describe_instance(@passthrough[:instance])
214
+ raise ArgumentError.new(sprintf('No such instance found in OpenStack - %s', @passthrough[:instance])) if inst_details.nil?
215
+ inst_details.addresses.each_key do |address_key|
216
+ if defined?(inst_details.addresses[address_key].first['addr'])
217
+ @passthrough[:host] = inst_details.addresses[address_key].first['addr']
218
+ break
219
+ end
220
+ end
221
+ end
102
222
  else
223
+ raise ArgumentError.new(sprintf('passthrough :type [%s] unknown, allowed: :aws, :openstack, :local, :remote', @passthrough[:type]))
224
+ end
225
+
226
+ else
227
+
228
+ @logger.debug('Vagrantfile and VM name validation..')
229
+ unless File.file?(@vagrantfile)
230
+ raise ArgumentError.new(sprintf('specified Vagrantfile [%s] does not exist', @vagrantfile))
231
+ end
232
+
233
+ raise ArgumentError.new('name of Vagrant VM not specified') if @name.nil?
234
+
235
+ return if opts[:unittest].eql?(true) # quick return if we're a unit test
236
+
237
+ begin
238
+ @vagrantbinary = self._run('which vagrant').chomp!
239
+ rescue
240
+ raise ExternalError.new('vagrant not found in path')
241
+ end
242
+
243
+ @logger.debug('SSH key discovery and viability tests..')
244
+ if @sshkey.nil?
103
245
  # ref the key from the vagrant home dir if it's been overridden
104
246
  @sshkey = sprintf('%s/insecure_private_key', ENV['VAGRANT_HOME']) if ENV['VAGRANT_HOME']
105
247
  @sshkey = sprintf('%s/.vagrant.d/insecure_private_key', ENV['HOME']) unless ENV['VAGRANT_HOME']
@@ -111,14 +253,18 @@ class Rouster
111
253
  raise InternalError.new('ssh key does not exist') unless File.file?(@sshkey)
112
254
  self.check_key_permissions(@sshkey)
113
255
  rescue => e
114
- raise InternalError.new("specified key [#{@sshkey}] has bad permissions. Vagrant exception: [#{e.message}]")
256
+
257
+ unless self.is_passthrough? and @passthrough[:type].eql?(:local)
258
+ raise InternalError.new("specified key [#{@sshkey}] has bad permissions. Vagrant exception: [#{e.message}]")
259
+ end
260
+
115
261
  end
116
262
 
117
263
  if @sshtunnel
118
264
  self.up()
119
265
  end
120
266
 
121
- @log.info('Rouster object successfully instantiated')
267
+ @logger.info('Rouster object successfully instantiated')
122
268
  end
123
269
 
124
270
 
@@ -127,86 +273,15 @@ class Rouster
127
273
  #
128
274
  # overloaded method to return useful information about Rouster objects
129
275
  def inspect
276
+ s = self.status()
130
277
  "name [#{@name}]:
131
278
  is_available_via_ssh?[#{self.is_available_via_ssh?}],
132
279
  passthrough[#{@passthrough}],
133
280
  sshkey[#{@sshkey}],
134
- status[#{self.status()}],
281
+ status[#{s}],
135
282
  sudo[#{@sudo}],
136
283
  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))
284
+ verbosity console[#{@verbosity_console}] / log[#{@verbosity_logfile} - #{@logfile}]\n"
210
285
  end
211
286
 
212
287
  ## internal methods
@@ -233,10 +308,28 @@ class Rouster
233
308
  expected_exitcode = [expected_exitcode] unless expected_exitcode.class.eql?(Array) # yuck, but 2.0 no longer coerces strings into single element arrays
234
309
 
235
310
  cmd = sprintf('%s%s; echo ec[$?]', self.uses_sudo? ? 'sudo ' : '', command)
236
- @log.info(sprintf('vm running: [%s]', cmd))
311
+ @logger.info(sprintf('vm running: [%s]', cmd)) # TODO decide whether this should be changed in light of passthroughs.. 'remotely'?
312
+
313
+ 0.upto(@retries) do |try|
314
+ begin
315
+ if self.is_passthrough? and self.passthrough[:type].eql?(:local)
316
+ output = `#{cmd}`
317
+ else
318
+ output = @ssh.exec!(cmd)
319
+ end
320
+
321
+ break
322
+ rescue => e
323
+ @logger.error(sprintf('failed to run [%s] with [%s], attempt[%s/%s]', cmd, e, try, retries)) if self.retries > 0
324
+ sleep 10 # TODO need to expose this as a variable
325
+ end
237
326
 
238
- output = @ssh.exec!(cmd)
239
- if output.match(/ec\[(\d+)\]/)
327
+ end
328
+
329
+ if output.nil?
330
+ output = "error gathering output, last logged output[#{self.get_output()}]"
331
+ @exitcode = 256
332
+ elsif output.match(/ec\[(\d+)\]/)
240
333
  @exitcode = $1.to_i
241
334
  output.gsub!(/ec\[(\d+)\]\n/, '')
242
335
  else
@@ -244,9 +337,10 @@ class Rouster
244
337
  end
245
338
 
246
339
  self.output.push(output)
247
- @log.debug(sprintf('output: [%s]', output))
340
+ @logger.debug(sprintf('output: [%s]', output))
248
341
 
249
342
  unless expected_exitcode.member?(@exitcode)
343
+ # TODO technically this could be a 'LocalPassthroughExecutionError' now too if local passthrough.. should we update?
250
344
  raise RemoteExecutionError.new("output[#{output}], exitcode[#{@exitcode}], expected[#{expected_exitcode}]")
251
345
  end
252
346
 
@@ -266,7 +360,7 @@ class Rouster
266
360
  if @cache_timeout
267
361
  if @cache.has_key?(:is_available_via_ssh?)
268
362
  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]))
363
+ @logger.debug(sprintf('using cached is_available_via_ssh?[%s] from [%s]', @cache[:is_available_via_ssh?][:status], @cache[:is_available_via_ssh?][:time]))
270
364
  return @cache[:is_available_via_ssh?][:status]
271
365
  end
272
366
  end
@@ -274,96 +368,32 @@ class Rouster
274
368
 
275
369
  if @ssh.nil? or @ssh.closed?
276
370
  begin
277
- self.connect_ssh_tunnel()
278
- rescue Rouster::InternalError, Net::SSH::Disconnect => e
371
+ res = self.connect_ssh_tunnel()
372
+ rescue Rouster::InternalError, Net::SSH::Disconnect, Errno::ECONNREFUSED, Errno::ECONNRESET => e
279
373
  res = false
280
374
  end
281
375
 
282
376
  end
283
377
 
284
- if res.nil?
378
+ if res.nil? or res.is_a?(Net::SSH::Connection::Session)
285
379
  begin
286
380
  self.run('echo functional test of SSH tunnel')
381
+ res = true
287
382
  rescue
288
383
  res = false
289
384
  end
290
385
  end
291
386
 
292
- res = true if res.nil?
293
-
294
387
  if @cache_timeout
295
388
  @cache[:is_available_via_ssh?] = Hash.new unless @cache[:is_available_via_ssh?].class.eql?(Hash)
296
389
  @cache[:is_available_via_ssh?][:time] = Time.now.to_i
297
390
  @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]))
391
+ @logger.debug(sprintf('caching is_available_via_ssh?[%s] at [%s]', @cache[:is_available_via_ssh?][:status], @cache[:is_available_via_ssh?][:time]))
299
392
  end
300
393
 
301
394
  res
302
395
  end
303
396
 
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
397
  ##
368
398
  # get_ssh_info
369
399
  #
@@ -375,6 +405,7 @@ class Rouster
375
405
  h = Hash.new()
376
406
 
377
407
  if @ssh_info.class.eql?(Hash)
408
+ @logger.debug('using cached SSH info')
378
409
  h = @ssh_info
379
410
  else
380
411
 
@@ -388,8 +419,8 @@ class Rouster
388
419
  elsif line.match(/Port (\d*?)$/)
389
420
  h[:ssh_port] = $1
390
421
  elsif line.match(/IdentityFile (.*?)$/)
391
- # TODO what to do if the user has specified @sshkey ?
392
422
  h[:identity_file] = $1
423
+ @logger.info(sprintf('vagrant specified key[%s] differs from provided[%s], will use both', @sshkey, h[:identity_file]))
393
424
  end
394
425
  end
395
426
 
@@ -406,14 +437,56 @@ class Rouster
406
437
  #
407
438
  # raises its own InternalError if the machine isn't running, otherwise returns Net::SSH connection object
408
439
  def connect_ssh_tunnel
409
- @log.debug('opening SSH tunnel..')
410
440
 
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)
441
+ if self.is_passthrough?
442
+ if @passthrough[:type].eql?(:local)
443
+ @logger.debug("local passthroughs don't need ssh tunnel, shell execs are used")
444
+ return false
445
+ elsif @passthrough[:host].nil?
446
+ @logger.info(sprintf('not attempting to connect, no known hostname for[%s]', self.passthrough))
447
+ return false
448
+ else
449
+ ceiling = @passthrough[:ssh_sleep_ceiling]
450
+ sleep_time = @passthrough[:ssh_sleep_time]
451
+
452
+ 0.upto(ceiling) do |try|
453
+ @logger.debug(sprintf('opening remote SSH tunnel[%s]..', @passthrough[:host]))
454
+ begin
455
+ @ssh = Net::SSH.start(
456
+ @passthrough[:host],
457
+ @passthrough[:user],
458
+ :port => @passthrough[:ssh_port],
459
+ :keys => [ @passthrough[:key] ], # TODO this should be @sshkey
460
+ :paranoid => false
461
+ )
462
+ break
463
+ rescue => e
464
+ raise e if try.eql?(ceiling) # eventually want to throw a SocketError
465
+ @logger.debug(sprintf('failed to open tunnel[%s], trying again in %ss', e.message, sleep_time))
466
+ sleep sleep_time
467
+ end
468
+ end
469
+ end
470
+ @logger.debug(sprintf('successfully opened SSH tunnel to[%s]', passthrough[:host]))
471
+
415
472
  else
416
- raise InternalError.new(sprintf('VM is not running[%s], unable open SSH tunnel', status))
473
+ # not a passthrough, normal connection
474
+ status = self.status()
475
+
476
+ if status.eql?('running')
477
+ self.get_ssh_info()
478
+ @logger.debug('opening VM SSH tunnel..')
479
+ @ssh = Net::SSH.start(
480
+ @ssh_info[:hostname],
481
+ @ssh_info[:user],
482
+ :port => @ssh_info[:ssh_port],
483
+ :keys => [ @sshkey, @ssh_info[:identity_file] ].uniq, # try to use what the user specified first, but fall back to what vagrant says
484
+ :paranoid => false
485
+ )
486
+ else
487
+ # TODO will we ever hit this? or will we be thrown first?
488
+ raise InternalError.new(sprintf('VM is not running[%s], unable open SSH tunnel', status))
489
+ end
417
490
  end
418
491
 
419
492
  @ssh
@@ -425,7 +498,7 @@ class Rouster
425
498
  # shuts down the persistent Net::SSH tunnel
426
499
  #
427
500
  def disconnect_ssh_tunnel
428
- @log.debug('closing SSH tunnel..')
501
+ @logger.debug('closing SSH tunnel..')
429
502
 
430
503
  @ssh.shutdown! unless @ssh.nil?
431
504
  @ssh = nil
@@ -441,30 +514,61 @@ class Rouster
441
514
  return @ostype
442
515
  end
443
516
 
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
517
+ res = :invalid
518
+
519
+ Rouster.os_files.each_pair do |os, f|
520
+ [ f ].flatten.each do |candidate|
521
+ if self.is_file?(candidate)
522
+ next if candidate.eql?('/etc/os-release') and ! self.is_in_file?(candidate, os.to_s, 'i') # CentOS detection
523
+ @logger.debug(sprintf('determined OS to be[%s] via[%s]', os, candidate))
524
+ res = os
461
525
  end
526
+ end
527
+ break unless res.eql?(:invalid)
462
528
  end
463
529
 
530
+ @logger.error(sprintf('unable to determine OS, looking for[%s]', Rouster.os_files)) if res.eql?(:invalid)
531
+
464
532
  @ostype = res
465
533
  res
466
534
  end
467
535
 
536
+ ##
537
+ # os_version
538
+ #
539
+ #
540
+ def os_version(os_type)
541
+ return @osversion if @osversion
542
+
543
+ res = :invalid
544
+
545
+ [ Rouster.os_files[os_type] ].flatten.each do |candidate|
546
+ if self.is_file?(candidate)
547
+ next if candidate.eql?('/etc/os-release') and ! self.is_in_file?(candidate, os_type.to_s, 'i') # CentOS detection
548
+ contents = self.run(sprintf('cat %s', candidate))
549
+ if os_type.eql?(:ubuntu)
550
+ version = $1 if contents.match(/.*VERSION\="(\d+\.\d+).*"/) # VERSION="13.10, Saucy Salamander"
551
+ res = version unless version.nil?
552
+ elsif os_type.eql?(:rhel)
553
+ version = $1 if contents.match(/.*VERSION\="(\d+)"/) # VERSION="7 (Core)"
554
+ version = $1 if version.nil? and contents.match(/.*(\d+.\d+)/) # CentOS release 6.4 (Final)
555
+ res = version unless version.nil?
556
+ elsif os_type.eql?(:osx)
557
+ version = $1 if contents.match(/<key>ProductVersion<\/key>.*<string>(.*)<\/string>/m) # <key>ProductVersion</key>\n <string>10.12.1</string>
558
+ res = version unless version.nil?
559
+ end
560
+
561
+ end
562
+ break unless res.eql?(:invalid)
563
+ end
564
+
565
+ @logger.error(sprintf('unable to determine OS version, looking for[%s]', Rouster.os_files[os_type])) if res.eql?(:invalid)
566
+
567
+ @osversion = res
568
+
569
+ res
570
+ end
571
+
468
572
  ##
469
573
  # get
470
574
  #
@@ -481,7 +585,7 @@ class Rouster
481
585
  # TODO what happens when we pass a wildcard as remote_file?
482
586
 
483
587
  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))
588
+ @logger.debug(sprintf('scp from VM[%s] to host[%s]', remote_file, local_file))
485
589
 
486
590
  begin
487
591
  @ssh.scp.download!(remote_file, local_file)
@@ -502,7 +606,7 @@ class Rouster
502
606
  # * [remote_file] - full or relative path (based on ~vagrant) of filename to upload to
503
607
  def put(local_file, remote_file=nil)
504
608
  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))
609
+ @logger.debug(sprintf('scp from host[%s] to VM[%s]', local_file, remote_file))
506
610
 
507
611
  raise FileTransferError.new(sprintf('unable to put[%s], local file does not exist', local_file)) unless File.file?(local_file)
508
612
 
@@ -512,6 +616,7 @@ class Rouster
512
616
  raise FileTransferError.new(sprintf('unable to put[%s], exception[%s]', local_file, e.message()))
513
617
  end
514
618
 
619
+ return true
515
620
  end
516
621
 
517
622
  ##
@@ -519,7 +624,7 @@ class Rouster
519
624
  #
520
625
  # convenience getter for @passthrough truthiness
521
626
  def is_passthrough?
522
- self.passthrough.eql?(true)
627
+ @passthrough.class.eql?(Hash)
523
628
  end
524
629
 
525
630
  ##
@@ -527,7 +632,7 @@ class Rouster
527
632
  #
528
633
  # convenience getter for @sudo truthiness
529
634
  def uses_sudo?
530
- self.sudo.eql?(true)
635
+ @sudo.eql?(true)
531
636
  end
532
637
 
533
638
  ##
@@ -535,7 +640,7 @@ class Rouster
535
640
  #
536
641
  # destroy and then up the machine in question
537
642
  def rebuild
538
- @log.debug('rebuild()')
643
+ @logger.debug('rebuild()')
539
644
  self.destroy
540
645
  self.up
541
646
  end
@@ -547,31 +652,41 @@ class Rouster
547
652
  #
548
653
  # parameters
549
654
  # * [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()')
655
+ def restart(wait=nil, expected_exitcodes = [0])
656
+ @logger.debug('restart()')
552
657
 
553
- if self.is_passthrough? and self.passthrough.eql?(local)
554
- @log.warn(sprintf('intercepted [restart] sent to a local passthrough, no op'))
658
+ if self.is_passthrough? and self.passthrough[:type].eql?(:local)
659
+ @logger.warn(sprintf('intercepted [restart] sent to a local passthrough, no op'))
555
660
  return nil
556
661
  end
557
662
 
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))
663
+ if @vagrant_reboot
664
+ # leading vagrant handle this through 'reload --no-provision'
665
+ self.reload
666
+ else
667
+ # trying to do it ourselves
668
+ case os_type
669
+ when :osx
670
+ self.run('shutdown -r now', expected_exitcodes)
671
+ when :rhel, :ubuntu
672
+ if os_type.eql?(:rhel) and os_version(os_type).match(/7/)
673
+ self.run('shutdown --halt --reboot now', expected_exitcodes << 256)
674
+ else
675
+ self.run('shutdown -rf now')
676
+ end
677
+ when :solaris
678
+ self.run('shutdown -y -i5 -g0', expected_exitcodes)
679
+ else
680
+ raise InternalError.new(sprintf('unsupported OS[%s]', @ostype))
681
+ end
567
682
  end
568
683
 
569
684
  @ssh, @ssh_info = nil # severing the SSH tunnel, getting ready in case this box is brought back up on a different port
570
685
 
571
686
  if wait
572
687
  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))
688
+ 0.upto(9) do |e|
689
+ @logger.debug(sprintf('waiting for reboot: round[%s], step[%s], total[%s]', e, inc, wait))
575
690
  return true if self.is_available_via_ssh?()
576
691
  sleep inc
577
692
  end
@@ -593,37 +708,26 @@ class Rouster
593
708
  # parameters
594
709
  # * <command> - command to be run
595
710
  def _run(command)
596
- tmp_file = sprintf('/tmp/rouster.%s.%s', Time.now.to_i, $$)
711
+ tmp_file = sprintf('/tmp/rouster-cmd_output.%s.%s', Time.now.to_i, $$)
597
712
  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
713
  res = `#{cmd}` # what does this actually hold?
599
714
 
600
- @log.info(sprintf('host running: [%s]', cmd))
715
+ @logger.info(sprintf('host running: [%s]', cmd))
601
716
 
602
717
  output = File.read(tmp_file)
603
718
  File.delete(tmp_file) or raise InternalError.new(sprintf('unable to delete [%s]: %s', tmp_file, $!))
604
719
 
720
+ self.output.push(output)
721
+ @logger.debug(sprintf('output: [%s]', output))
722
+
605
723
  unless $?.success?
606
724
  raise LocalExecutionError.new(sprintf('command [%s] exited with code [%s], output [%s]', cmd, $?.to_i(), output))
607
725
  end
608
726
 
609
- self.output.push(output)
610
- @log.debug(sprintf('output: [%s]', output))
611
-
612
727
  @exitcode = $?.to_i()
613
728
  output
614
729
  end
615
730
 
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
731
  ##
628
732
  # get_output
629
733
  #
@@ -663,7 +767,7 @@ class Rouster
663
767
  def traverse_up(startdir=Dir.pwd, filename=nil, levels=10)
664
768
  raise InternalError.new('must specify a filename') if filename.nil?
665
769
 
666
- @log.debug(sprintf('traverse_up() looking for [%s] in [%s], up to [%s] levels', filename, startdir, levels)) unless @log.nil?
770
+ @logger.debug(sprintf('traverse_up() looking for [%s] in [%s], up to [%s] levels', filename, startdir, levels)) unless @logger.nil?
667
771
 
668
772
  dirs = startdir.split('/')
669
773
  count = 0
@@ -692,6 +796,11 @@ class Rouster
692
796
  def check_key_permissions(key, fix=false)
693
797
  allowed_modes = ['0400', '0600']
694
798
 
799
+ if key.match(/\.pub$/)
800
+ # if this is the public half of the key, be more permissive
801
+ allowed_modes << '0644'
802
+ end
803
+
695
804
  raw = self._run(sprintf('ls -l %s', key))
696
805
  perms = self.parse_ls_string(raw)
697
806
 
@@ -716,6 +825,15 @@ class Rouster
716
825
  nil
717
826
  end
718
827
 
828
+ def self.os_files
829
+ {
830
+ :ubuntu => '/etc/os-release', # debian too
831
+ :solaris => '/etc/release',
832
+ :rhel => ['/etc/os-release', '/etc/redhat-release'], # and centos
833
+ :osx => '/System/Library/CoreServices/SystemVersion.plist',
834
+ }
835
+ end
836
+
719
837
  end
720
838
 
721
839
  class Object