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.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/.reek +63 -0
- data/.travis.yml +11 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +102 -0
- data/README.md +233 -7
- data/Rakefile +52 -34
- data/Vagrantfile +26 -8
- data/examples/aws.rb +85 -0
- data/examples/openstack.rb +61 -0
- data/examples/passthrough.rb +71 -0
- data/lib/rouster.rb +380 -262
- data/lib/rouster/deltas.rb +470 -138
- data/lib/rouster/puppet.rb +155 -26
- data/lib/rouster/testing.rb +205 -46
- data/lib/rouster/tests.rb +40 -11
- data/lib/rouster/vagrant.rb +311 -0
- data/path_helper.rb +3 -4
- data/plugins/aws.rb +347 -0
- data/plugins/openstack.rb +136 -0
- data/test/basic.rb +4 -1
- data/test/functional/deltas/test_get_crontab.rb +64 -2
- data/test/functional/deltas/test_get_groups.rb +74 -2
- data/test/functional/deltas/test_get_os.rb +68 -0
- data/test/functional/deltas/test_get_packages.rb +73 -6
- data/test/functional/deltas/test_get_ports.rb +26 -1
- data/test/functional/deltas/test_get_services.rb +43 -5
- data/test/functional/deltas/test_get_users.rb +35 -2
- data/test/functional/puppet/test_facter.rb +41 -1
- data/test/functional/test_caching.rb +2 -2
- data/test/functional/test_inspect.rb +1 -1
- data/test/functional/test_is_file.rb +17 -1
- data/test/functional/test_is_in_file.rb +40 -0
- data/test/functional/test_new.rb +233 -22
- data/test/functional/test_passthroughs.rb +94 -0
- data/test/functional/test_put.rb +2 -2
- data/test/functional/test_validate_file.rb +104 -3
- data/test/puppet/test_apply.rb +8 -6
- data/test/unit/puppet/resources/puppet_run_with_failed_exec +59 -0
- data/test/unit/puppet/resources/puppet_run_with_successful_exec +61 -0
- data/test/unit/puppet/test_get_puppet_star.rb +27 -4
- data/test/unit/puppet/test_puppet_parsing.rb +44 -0
- data/test/unit/test_new.rb +88 -0
- data/test/unit/test_parse_ls_string.rb +67 -0
- data/test/unit/testing/resources/osx-launchd +285 -0
- data/test/unit/testing/resources/rhel-systemd +46 -0
- data/test/unit/testing/resources/rhel-systemv +41 -0
- data/test/unit/testing/resources/rhel-upstart +20 -0
- data/test/unit/testing/test_get_services.rb +178 -0
- data/test/unit/testing/test_validate_cron.rb +78 -0
- data/test/unit/testing/test_validate_package.rb +36 -10
- data/test/unit/testing/test_validate_port.rb +5 -0
- metadata +42 -21
- 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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
32
|
+
sh "ruby #{example}"
|
|
33
|
+
end
|
|
25
34
|
end
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
desc 'shortcut to vagrant destroy -f'
|
|
37
|
+
task :vdestroy do
|
|
38
|
+
sh 'vagrant destroy -f'
|
|
29
39
|
end
|
|
30
40
|
|
|
31
|
-
Rake::
|
|
32
|
-
t.
|
|
33
|
-
t.
|
|
34
|
-
t.
|
|
35
|
-
t.
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
data/Vagrantfile
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
# stripped down example piab Vagrantfile for rouster
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
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 =
|
|
14
|
-
worker.vm.network :hostonly, sprintf('10.0.1.%s', rand(253).to_i +
|
|
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')
|
data/examples/aws.rb
ADDED
|
@@ -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
|
data/lib/rouster.rb
CHANGED
|
@@ -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.
|
|
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, :
|
|
26
|
-
attr_reader :cache, :cache_timeout, :deltas, :exitcode, :
|
|
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>
|
|
33
|
-
# * [cache_timeout]
|
|
34
|
-
# * [
|
|
35
|
-
# * [
|
|
36
|
-
# * [
|
|
37
|
-
# * [
|
|
38
|
-
# * [
|
|
39
|
-
# * [
|
|
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
|
|
42
|
-
@
|
|
43
|
-
@
|
|
44
|
-
@
|
|
45
|
-
@
|
|
46
|
-
@
|
|
47
|
-
@
|
|
48
|
-
@
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
72
|
-
@
|
|
73
|
-
|
|
96
|
+
@logger = Log4r::Logger.new(sprintf('rouster:%s', @name))
|
|
97
|
+
@logger.outputters << Log4r::Outputter.stderr
|
|
98
|
+
#@log.outputters << Log4r::Outputter.stdout
|
|
74
99
|
|
|
75
|
-
@
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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[#{
|
|
281
|
+
status[#{s}],
|
|
135
282
|
sudo[#{@sudo}],
|
|
136
283
|
vagrantfile[#{@vagrantfile}],
|
|
137
|
-
verbosity[#{@
|
|
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
|
-
@
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
|
574
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|