rouster 0.53 → 0.57
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +5 -2
- data/examples/passthrough.rb +71 -0
- data/lib/rouster.rb +194 -242
- data/lib/rouster/deltas.rb +44 -39
- data/lib/rouster/puppet.rb +82 -21
- data/lib/rouster/testing.rb +129 -19
- data/lib/rouster/tests.rb +34 -6
- data/lib/rouster/vagrant.rb +242 -0
- data/test/basic.rb +4 -1
- data/test/functional/deltas/test_get_crontab.rb +22 -1
- data/test/functional/deltas/test_get_services.rb +6 -0
- data/test/functional/test_inspect.rb +1 -1
- data/test/functional/test_is_file.rb +17 -1
- data/test/functional/test_new.rb +185 -16
- data/test/functional/test_passthroughs.rb +94 -0
- data/test/functional/test_put.rb +2 -2
- data/test/functional/test_validate_file.rb +55 -3
- data/test/puppet/test_apply.rb +7 -5
- data/test/unit/puppet/test_get_puppet_star.rb +27 -4
- data/test/unit/test_new.rb +23 -0
- data/test/unit/test_parse_ls_string.rb +24 -0
- data/test/unit/testing/test_validate_cron.rb +78 -0
- metadata +24 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ca887947fd8eb61d347ebb19f562d4ab6f79cf79
|
4
|
+
data.tar.gz: 424047f4ad62c0b279baa71d503045096416b37f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 90856a137433d2c34a2f2f7e3977c748bb8457f96049c0111b650ab6f5e0520380f74406b20b7a274f01bc8c8b1341b5d0c6fd345cec5960db718b76c6b9a55c
|
7
|
+
data.tar.gz: e6f9d8b1400e684cf89451a4ac2d46ace2f3642927a7b70cb666ed03e2475b250c45a82c68174f8ce1cebf252ec2b2ccb07546b69193ecbf2568d3cad3858e18
|
data/README.md
CHANGED
@@ -24,8 +24,11 @@ The first implementation of Rouster was in Perl, called [Salesforce::Vagrant](ht
|
|
24
24
|
|
25
25
|
* [Ruby](http://rubylang.org), version 2.0+ (best attempt made to support 1.8.7 and 1.9.3 as well)
|
26
26
|
* [Vagrant](http://vagrantup.com), version 1.0.5+
|
27
|
-
|
28
|
-
|
27
|
+
* Gems
|
28
|
+
* json
|
29
|
+
* log4r
|
30
|
+
* net-scp
|
31
|
+
* net-ssh
|
29
32
|
|
30
33
|
Note: Rouster should work exactly the same on Windows as it does on \*nix and OSX (minus rouster/deltas.rb functionality, at least currently),
|
31
34
|
but no real testing has been done to confirm this. Please file issues as appropriate.
|
@@ -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.57
|
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,36 +21,65 @@ 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
|
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
|
+
# * [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
45
|
def initialize(opts = nil)
|
41
|
-
@cache_timeout
|
42
|
-
@
|
43
|
-
@
|
44
|
-
@
|
45
|
-
@
|
46
|
-
@
|
47
|
-
@
|
48
|
-
@
|
46
|
+
@cache_timeout = opts[:cache_timeout].nil? ? false : opts[:cache_timeout]
|
47
|
+
@logfile = opts[:logfile].nil? ? false : opts[:logfile]
|
48
|
+
@name = opts[:name]
|
49
|
+
@passthrough = opts[:passthrough].nil? ? false : opts[:passthrough]
|
50
|
+
@retries = opts[:retries].nil? ? 0 : opts[:retries]
|
51
|
+
@sshkey = opts[:sshkey]
|
52
|
+
@sshtunnel = opts[:sshtunnel].nil? ? true : opts[:sshtunnel]
|
53
|
+
@unittest = opts[:unittest].nil? ? false : opts[:unittest]
|
54
|
+
@vagrantfile = opts[:vagrantfile].nil? ? traverse_up(Dir.pwd, 'Vagrantfile', 5) : opts[:vagrantfile]
|
55
|
+
@vagrant_concurrency = opts[:vagrant_concurrency].nil? ? false : opts[:vagrant_concurrency]
|
56
|
+
|
57
|
+
# TODO kind of want to invert this, 0 = trace, 1 = debug, 2 = info, 3 = warning, 4 = error
|
58
|
+
# 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
|
59
|
+
if opts[:verbosity]
|
60
|
+
# TODO decide how to handle this case -- currently #2 is implemented
|
61
|
+
# - option 1, if passed a single integer, use that level for both loggers
|
62
|
+
# - option 2, if passed a single integer, use that level for stdout, and a hardcoded level (probably INFO) to logfile
|
63
|
+
|
64
|
+
# kind of want to do if opts[:verbosity].respond_to?(:[]), but for 1.87 compatability, going this way..
|
65
|
+
if ! opts[:verbosity].is_a?(Array) or opts[:verbosity].is_a?(Integer)
|
66
|
+
@verbosity_console = opts[:verbosity].to_i
|
67
|
+
@verbosity_logfile = 2
|
68
|
+
elsif opts[:verbosity].is_a?(Array)
|
69
|
+
# TODO more error checking here when we are sure this is the right way to go
|
70
|
+
@verbosity_console = opts[:verbosity][0].to_i
|
71
|
+
@verbosity_logfile = opts[:verbosity][1].to_i
|
72
|
+
@logfile = true if @logfile.eql?(false) # overriding the default setting
|
73
|
+
end
|
74
|
+
else
|
75
|
+
@verbosity_console = 3
|
76
|
+
@verbosity_logfile = 2 # this is kind of arbitrary, but won't actually be created unless opts[:logfile] is also passed
|
77
|
+
end
|
49
78
|
|
50
79
|
if opts.has_key?(:sudo)
|
51
80
|
@sudo = opts[:sudo]
|
52
|
-
elsif @passthrough.eql?(
|
81
|
+
elsif @passthrough.class.eql?(Hash)
|
82
|
+
# TODO say something here.. or maybe check to see if our user has passwordless sudo?
|
53
83
|
@sudo = false
|
54
84
|
else
|
55
85
|
@sudo = true
|
@@ -68,18 +98,61 @@ class Rouster
|
|
68
98
|
require 'log4r/config'
|
69
99
|
Log4r.define_levels(*Log4r::Log4rConfig::LogLevels)
|
70
100
|
|
71
|
-
@
|
72
|
-
@
|
73
|
-
|
101
|
+
@logger = Log4r::Logger.new(sprintf('rouster:%s', @name))
|
102
|
+
@logger.outputters << Log4r::Outputter.stderr
|
103
|
+
#@log.outputters << Log4r::Outputter.stdout
|
104
|
+
|
105
|
+
if @logfile
|
106
|
+
@logfile = @logfile.eql?(true) ? sprintf('/tmp/rouster-%s.%s.%s.log', @name, Time.now.to_i, $$) : @logfile
|
107
|
+
@logger.outputters << Log4r::FileOutputter.new(sprintf('rouster:%s', @name), :filename => @logfile, :level => @verbosity_logfile)
|
108
|
+
end
|
109
|
+
|
110
|
+
@logger.outputters[0].level = @verbosity_console # can't set this when instantiating a .std* logger, and want the FileOutputter at a different level
|
111
|
+
|
112
|
+
if @passthrough
|
113
|
+
# TODO do better about informing of required specifications, maybe point them to an URL?
|
114
|
+
@vagrantbinary = 'vagrant' # hacky fix to is_vagrant_running?() grepping, doesn't need to actually be in $PATH
|
115
|
+
if @passthrough.class != Hash
|
116
|
+
raise ArgumentError.new('passthrough specification should be hash')
|
117
|
+
elsif @passthrough[:type].nil?
|
118
|
+
raise ArgumentError.new('passthrough :type must be specified, :local or :remote allowed')
|
119
|
+
elsif @passthrough[:type].eql?(:local)
|
120
|
+
@sshtunnel = false
|
121
|
+
@logger.debug('instantiating a local passthrough worker')
|
122
|
+
elsif @passthrough[:type].eql?(:remote)
|
123
|
+
raise ArgumentError.new('remote passthrough requires :host specification') if @passthrough[:host].nil?
|
124
|
+
raise ArgumentError.new('remote passthrough requires :user specification') if @passthrough[:user].nil?
|
125
|
+
raise ArgumentError.new('remote passthrough requires :key specification') if @passthrough[:key].nil?
|
126
|
+
raise ArgumentError.new('remote passthrough requires valid :key specification, should be path to private half') unless File.file?(@passthrough[:key])
|
127
|
+
@sshkey = @passthrough[:key] # TODO refactor so that you don't have to do this..
|
128
|
+
@logger.debug('instantiating a remote passthrough worker')
|
129
|
+
else
|
130
|
+
raise ArgumentError.new(sprintf('passthrough :type [%s] unknown, allowed: :local, :remote', @passthrough[:type]))
|
131
|
+
end
|
132
|
+
else
|
74
133
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
134
|
+
@logger.debug('Vagrantfile and VM name validation..')
|
135
|
+
unless File.file?(@vagrantfile)
|
136
|
+
raise ArgumentError.new(sprintf('specified Vagrantfile [%s] does not exist', @vagrantfile))
|
137
|
+
end
|
79
138
|
|
80
|
-
|
139
|
+
raise ArgumentError.new('name of Vagrant VM not specified') if @name.nil?
|
81
140
|
|
82
|
-
|
141
|
+
return if opts[:unittest].eql?(true) # quick return if we're a unit test
|
142
|
+
|
143
|
+
begin
|
144
|
+
@vagrantbinary = self._run('which vagrant').chomp!
|
145
|
+
rescue
|
146
|
+
raise ExternalError.new('vagrant not found in path')
|
147
|
+
end
|
148
|
+
|
149
|
+
@logger.debug('SSH key discovery and viability tests..')
|
150
|
+
if @sshkey.nil?
|
151
|
+
# ref the key from the vagrant home dir if it's been overridden
|
152
|
+
@sshkey = sprintf('%s/insecure_private_key', ENV['VAGRANT_HOME']) if ENV['VAGRANT_HOME']
|
153
|
+
@sshkey = sprintf('%s/.vagrant.d/insecure_private_key', ENV['HOME']) unless ENV['VAGRANT_HOME']
|
154
|
+
end
|
155
|
+
end
|
83
156
|
|
84
157
|
# this is breaking test/functional/test_caching.rb test_ssh_caching (if the VM was not running when the test started)
|
85
158
|
# it slows down object instantiation, but is a good test to ensure the machine name is valid..
|
@@ -89,36 +162,23 @@ class Rouster
|
|
89
162
|
raise InternalError.new(sprintf('caught non-0 exitcode from status(): %s', e.message))
|
90
163
|
end
|
91
164
|
|
92
|
-
begin
|
93
|
-
self._run('which vagrant')
|
94
|
-
rescue
|
95
|
-
raise ExternalError.new('vagrant not found in path')
|
96
|
-
end
|
97
|
-
|
98
|
-
@log.debug('SSH key discovery and viability tests..')
|
99
|
-
if @sshkey.nil?
|
100
|
-
if @passthrough.eql?(true)
|
101
|
-
raise InternalError.new('must specify sshkey when using a passthrough host')
|
102
|
-
else
|
103
|
-
# ref the key from the vagrant home dir if it's been overridden
|
104
|
-
@sshkey = sprintf('%s/insecure_private_key', ENV['VAGRANT_HOME']) if ENV['VAGRANT_HOME']
|
105
|
-
@sshkey = sprintf('%s/.vagrant.d/insecure_private_key', ENV['HOME']) unless ENV['VAGRANT_HOME']
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
165
|
begin
|
110
166
|
raise InternalError.new('ssh key not specified') if @sshkey.nil?
|
111
167
|
raise InternalError.new('ssh key does not exist') unless File.file?(@sshkey)
|
112
168
|
self.check_key_permissions(@sshkey)
|
113
169
|
rescue => e
|
114
|
-
|
170
|
+
|
171
|
+
unless self.is_passthrough? and @passthrough[:type].eql?(:local)
|
172
|
+
raise InternalError.new("specified key [#{@sshkey}] has bad permissions. Vagrant exception: [#{e.message}]")
|
173
|
+
end
|
174
|
+
|
115
175
|
end
|
116
176
|
|
117
177
|
if @sshtunnel
|
118
178
|
self.up()
|
119
179
|
end
|
120
180
|
|
121
|
-
@
|
181
|
+
@logger.info('Rouster object successfully instantiated')
|
122
182
|
end
|
123
183
|
|
124
184
|
|
@@ -127,86 +187,15 @@ class Rouster
|
|
127
187
|
#
|
128
188
|
# overloaded method to return useful information about Rouster objects
|
129
189
|
def inspect
|
190
|
+
s = self.status()
|
130
191
|
"name [#{@name}]:
|
131
192
|
is_available_via_ssh?[#{self.is_available_via_ssh?}],
|
132
193
|
passthrough[#{@passthrough}],
|
133
194
|
sshkey[#{@sshkey}],
|
134
|
-
status[#{
|
195
|
+
status[#{s}],
|
135
196
|
sudo[#{@sudo}],
|
136
197
|
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))
|
198
|
+
verbosity console[#{@verbosity_console}] / log[#{@verbosity_logfile} - #{@logfile}]\n"
|
210
199
|
end
|
211
200
|
|
212
201
|
## internal methods
|
@@ -233,10 +222,28 @@ class Rouster
|
|
233
222
|
expected_exitcode = [expected_exitcode] unless expected_exitcode.class.eql?(Array) # yuck, but 2.0 no longer coerces strings into single element arrays
|
234
223
|
|
235
224
|
cmd = sprintf('%s%s; echo ec[$?]', self.uses_sudo? ? 'sudo ' : '', command)
|
236
|
-
@
|
225
|
+
@logger.info(sprintf('vm running: [%s]', cmd)) # TODO decide whether this should be changed in light of passthroughs.. 'remotely'?
|
237
226
|
|
238
|
-
|
239
|
-
|
227
|
+
0.upto(@retries) do |try|
|
228
|
+
begin
|
229
|
+
if self.is_passthrough? and self.passthrough[:type].eql?(:local)
|
230
|
+
output = `#{cmd}`
|
231
|
+
else
|
232
|
+
output = @ssh.exec!(cmd)
|
233
|
+
end
|
234
|
+
|
235
|
+
break
|
236
|
+
rescue => e
|
237
|
+
@logger.error(sprintf('failed to run [%s] with [%s], attempt[%s/%s]', cmd, e, try, retries)) if self.retries > 0
|
238
|
+
sleep 10 # TODO need to expose this as a variable
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
|
243
|
+
if output.nil?
|
244
|
+
output = "error gathering output, last logged output[#{self.get_output()}]"
|
245
|
+
@exitcode = 256
|
246
|
+
elsif output.match(/ec\[(\d+)\]/)
|
240
247
|
@exitcode = $1.to_i
|
241
248
|
output.gsub!(/ec\[(\d+)\]\n/, '')
|
242
249
|
else
|
@@ -244,9 +251,10 @@ class Rouster
|
|
244
251
|
end
|
245
252
|
|
246
253
|
self.output.push(output)
|
247
|
-
@
|
254
|
+
@logger.debug(sprintf('output: [%s]', output))
|
248
255
|
|
249
256
|
unless expected_exitcode.member?(@exitcode)
|
257
|
+
# TODO technically this could be a 'LocalPassthroughExecutionError' now too if local passthrough.. should we update?
|
250
258
|
raise RemoteExecutionError.new("output[#{output}], exitcode[#{@exitcode}], expected[#{expected_exitcode}]")
|
251
259
|
end
|
252
260
|
|
@@ -266,7 +274,7 @@ class Rouster
|
|
266
274
|
if @cache_timeout
|
267
275
|
if @cache.has_key?(:is_available_via_ssh?)
|
268
276
|
if (Time.now.to_i - @cache[:is_available_via_ssh?][:time]) < @cache_timeout
|
269
|
-
@
|
277
|
+
@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
278
|
return @cache[:is_available_via_ssh?][:status]
|
271
279
|
end
|
272
280
|
end
|
@@ -295,86 +303,12 @@ class Rouster
|
|
295
303
|
@cache[:is_available_via_ssh?] = Hash.new unless @cache[:is_available_via_ssh?].class.eql?(Hash)
|
296
304
|
@cache[:is_available_via_ssh?][:time] = Time.now.to_i
|
297
305
|
@cache[:is_available_via_ssh?][:status] = res
|
298
|
-
@
|
306
|
+
@logger.debug(sprintf('caching is_available_via_ssh?[%s] at [%s]', @cache[:is_available_via_ssh?][:status], @cache[:is_available_via_ssh?][:time]))
|
299
307
|
end
|
300
308
|
|
301
309
|
res
|
302
310
|
end
|
303
311
|
|
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
|
-
if self.sandbox_available?
|
335
|
-
return self.vagrant(sprintf('sandbox on %s', @name))
|
336
|
-
else
|
337
|
-
raise ExternalError.new('sandbox plugin not installed')
|
338
|
-
end
|
339
|
-
end
|
340
|
-
|
341
|
-
##
|
342
|
-
# sandbox_off
|
343
|
-
# runs `vagrant sandbox off` from the Vagrantfile path
|
344
|
-
def sandbox_off
|
345
|
-
if self.sandbox_available?
|
346
|
-
return self.vagrant(sprintf('sandbox off %s', @name))
|
347
|
-
else
|
348
|
-
raise ExternalError.new('sandbox plugin not installed')
|
349
|
-
end
|
350
|
-
end
|
351
|
-
|
352
|
-
##
|
353
|
-
# sandbox_rollback
|
354
|
-
# runs `vagrant sandbox rollback` from the Vagrantfile path
|
355
|
-
def sandbox_rollback
|
356
|
-
if self.sandbox_available?
|
357
|
-
self.disconnect_ssh_tunnel
|
358
|
-
self.vagrant(sprintf('sandbox rollback %s', @name))
|
359
|
-
self.connect_ssh_tunnel
|
360
|
-
else
|
361
|
-
raise ExternalError.new('sandbox plugin not installed')
|
362
|
-
end
|
363
|
-
end
|
364
|
-
|
365
|
-
##
|
366
|
-
# sandbox_commit
|
367
|
-
# runs `vagrant sandbox commit` from the Vagrantfile path
|
368
|
-
def sandbox_commit
|
369
|
-
if self.sandbox_available?
|
370
|
-
self.disconnect_ssh_tunnel
|
371
|
-
self.vagrant(sprintf('sandbox commit %s', @name))
|
372
|
-
self.connect_ssh_tunnel
|
373
|
-
else
|
374
|
-
raise ExternalError.new('sandbox plugin not installed')
|
375
|
-
end
|
376
|
-
end
|
377
|
-
|
378
312
|
##
|
379
313
|
# get_ssh_info
|
380
314
|
#
|
@@ -386,7 +320,7 @@ class Rouster
|
|
386
320
|
h = Hash.new()
|
387
321
|
|
388
322
|
if @ssh_info.class.eql?(Hash)
|
389
|
-
@
|
323
|
+
@logger.debug('using cached SSH info')
|
390
324
|
h = @ssh_info
|
391
325
|
else
|
392
326
|
|
@@ -404,7 +338,7 @@ class Rouster
|
|
404
338
|
unless @sshkey.eql?(key)
|
405
339
|
h[:identity_file] = key
|
406
340
|
else
|
407
|
-
@
|
341
|
+
@logger.info(sprintf('using specified key[%s] instead of discovered key[%s]', @sshkey, key))
|
408
342
|
h[:identity_file] = @sshkey
|
409
343
|
end
|
410
344
|
|
@@ -424,14 +358,37 @@ class Rouster
|
|
424
358
|
#
|
425
359
|
# raises its own InternalError if the machine isn't running, otherwise returns Net::SSH connection object
|
426
360
|
def connect_ssh_tunnel
|
427
|
-
@log.debug('opening SSH tunnel..')
|
428
361
|
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
362
|
+
if self.is_passthrough?
|
363
|
+
if self.passthrough[:type].eql?(:local)
|
364
|
+
return false
|
365
|
+
else
|
366
|
+
@logger.debug('opening remote SSH tunnel..')
|
367
|
+
@ssh = Net::SSH.start(
|
368
|
+
@passthrough[:host],
|
369
|
+
@passthrough[:user],
|
370
|
+
:port => @passthrough[:port],
|
371
|
+
:keys => [ @passthrough[:key] ],
|
372
|
+
:paranoid => false
|
373
|
+
)
|
374
|
+
end
|
433
375
|
else
|
434
|
-
|
376
|
+
# not a passthrough, normal connection
|
377
|
+
status = self.status()
|
378
|
+
|
379
|
+
if status.eql?('running')
|
380
|
+
self.get_ssh_info()
|
381
|
+
@logger.debug('opening VM SSH tunnel..')
|
382
|
+
@ssh = Net::SSH.start(
|
383
|
+
@ssh_info[:hostname],
|
384
|
+
@ssh_info[:user],
|
385
|
+
:port => @ssh_info[:ssh_port],
|
386
|
+
:keys => [@sshkey],
|
387
|
+
:paranoid => false
|
388
|
+
)
|
389
|
+
else
|
390
|
+
raise InternalError.new(sprintf('VM is not running[%s], unable open SSH tunnel', status))
|
391
|
+
end
|
435
392
|
end
|
436
393
|
|
437
394
|
@ssh
|
@@ -443,7 +400,7 @@ class Rouster
|
|
443
400
|
# shuts down the persistent Net::SSH tunnel
|
444
401
|
#
|
445
402
|
def disconnect_ssh_tunnel
|
446
|
-
@
|
403
|
+
@logger.debug('closing SSH tunnel..')
|
447
404
|
|
448
405
|
@ssh.shutdown! unless @ssh.nil?
|
449
406
|
@ssh = nil
|
@@ -459,13 +416,19 @@ class Rouster
|
|
459
416
|
return @ostype
|
460
417
|
end
|
461
418
|
|
419
|
+
# TODO switch to file based detection
|
420
|
+
# Ubuntu - /etc/os-release
|
421
|
+
# Solaris - /etc/release
|
422
|
+
# RHEL/CentOS - /etc/redhat-release
|
423
|
+
# OSX - ?
|
424
|
+
|
462
425
|
res = nil
|
463
426
|
uname = self.run('uname -a')
|
464
427
|
|
465
428
|
case uname
|
466
429
|
when /Darwin/i
|
467
430
|
res = :osx
|
468
|
-
when /
|
431
|
+
when /SunOS|Solaris/i
|
469
432
|
res =:solaris
|
470
433
|
when /Ubuntu/i
|
471
434
|
res = :ubuntu
|
@@ -499,7 +462,7 @@ class Rouster
|
|
499
462
|
# TODO what happens when we pass a wildcard as remote_file?
|
500
463
|
|
501
464
|
local_file = local_file.nil? ? File.basename(remote_file) : local_file
|
502
|
-
@
|
465
|
+
@logger.debug(sprintf('scp from VM[%s] to host[%s]', remote_file, local_file))
|
503
466
|
|
504
467
|
begin
|
505
468
|
@ssh.scp.download!(remote_file, local_file)
|
@@ -520,7 +483,7 @@ class Rouster
|
|
520
483
|
# * [remote_file] - full or relative path (based on ~vagrant) of filename to upload to
|
521
484
|
def put(local_file, remote_file=nil)
|
522
485
|
remote_file = remote_file.nil? ? File.basename(local_file) : remote_file
|
523
|
-
@
|
486
|
+
@logger.debug(sprintf('scp from host[%s] to VM[%s]', local_file, remote_file))
|
524
487
|
|
525
488
|
raise FileTransferError.new(sprintf('unable to put[%s], local file does not exist', local_file)) unless File.file?(local_file)
|
526
489
|
|
@@ -537,7 +500,7 @@ class Rouster
|
|
537
500
|
#
|
538
501
|
# convenience getter for @passthrough truthiness
|
539
502
|
def is_passthrough?
|
540
|
-
|
503
|
+
@passthrough.class.eql?(Hash)
|
541
504
|
end
|
542
505
|
|
543
506
|
##
|
@@ -545,7 +508,7 @@ class Rouster
|
|
545
508
|
#
|
546
509
|
# convenience getter for @sudo truthiness
|
547
510
|
def uses_sudo?
|
548
|
-
|
511
|
+
@sudo.eql?(true)
|
549
512
|
end
|
550
513
|
|
551
514
|
##
|
@@ -553,7 +516,7 @@ class Rouster
|
|
553
516
|
#
|
554
517
|
# destroy and then up the machine in question
|
555
518
|
def rebuild
|
556
|
-
@
|
519
|
+
@logger.debug('rebuild()')
|
557
520
|
self.destroy
|
558
521
|
self.up
|
559
522
|
end
|
@@ -566,10 +529,10 @@ class Rouster
|
|
566
529
|
# parameters
|
567
530
|
# * [wait] - number of seconds to wait until is_available_via_ssh?() returns true before assuming failure
|
568
531
|
def restart(wait=nil)
|
569
|
-
@
|
532
|
+
@logger.debug('restart()')
|
570
533
|
|
571
|
-
if self.is_passthrough? and self.passthrough.eql?(local)
|
572
|
-
@
|
534
|
+
if self.is_passthrough? and self.passthrough[:type].eql?(:local)
|
535
|
+
@logger.warn(sprintf('intercepted [restart] sent to a local passthrough, no op'))
|
573
536
|
return nil
|
574
537
|
end
|
575
538
|
|
@@ -589,7 +552,7 @@ class Rouster
|
|
589
552
|
if wait
|
590
553
|
inc = wait.to_i / 10
|
591
554
|
0..wait.each do |e|
|
592
|
-
@
|
555
|
+
@logger.debug(sprintf('waiting for reboot: round[%s], step[%s], total[%s]', e, inc, wait))
|
593
556
|
return true if self.is_available_via_ssh?()
|
594
557
|
sleep inc
|
595
558
|
end
|
@@ -615,38 +578,22 @@ class Rouster
|
|
615
578
|
cmd = sprintf('%s > %s 2> %s', command, tmp_file, tmp_file) # this is a holdover from Salesforce::Vagrant, can we use '2&>1' here?
|
616
579
|
res = `#{cmd}` # what does this actually hold?
|
617
580
|
|
618
|
-
@
|
581
|
+
@logger.info(sprintf('host running: [%s]', cmd))
|
619
582
|
|
620
583
|
output = File.read(tmp_file)
|
621
584
|
File.delete(tmp_file) or raise InternalError.new(sprintf('unable to delete [%s]: %s', tmp_file, $!))
|
622
585
|
|
586
|
+
self.output.push(output)
|
587
|
+
@logger.debug(sprintf('output: [%s]', output))
|
588
|
+
|
623
589
|
unless $?.success?
|
624
590
|
raise LocalExecutionError.new(sprintf('command [%s] exited with code [%s], output [%s]', cmd, $?.to_i(), output))
|
625
591
|
end
|
626
592
|
|
627
|
-
self.output.push(output)
|
628
|
-
@log.debug(sprintf('output: [%s]', output))
|
629
|
-
|
630
593
|
@exitcode = $?.to_i()
|
631
594
|
output
|
632
595
|
end
|
633
596
|
|
634
|
-
##
|
635
|
-
# vagrant
|
636
|
-
#
|
637
|
-
# abstraction layer to call vagrant faces
|
638
|
-
#
|
639
|
-
# parameters
|
640
|
-
# * <face> - vagrant face to call (include arguments)
|
641
|
-
def vagrant(face)
|
642
|
-
if self.is_passthrough?
|
643
|
-
@log.info(sprintf('calling [vagrant %s] on a passthrough host is a noop', face))
|
644
|
-
return nil
|
645
|
-
end
|
646
|
-
|
647
|
-
self._run(sprintf('cd %s; vagrant %s', File.dirname(@vagrantfile), face))
|
648
|
-
end
|
649
|
-
|
650
597
|
##
|
651
598
|
# get_output
|
652
599
|
#
|
@@ -686,7 +633,7 @@ class Rouster
|
|
686
633
|
def traverse_up(startdir=Dir.pwd, filename=nil, levels=10)
|
687
634
|
raise InternalError.new('must specify a filename') if filename.nil?
|
688
635
|
|
689
|
-
@
|
636
|
+
@logger.debug(sprintf('traverse_up() looking for [%s] in [%s], up to [%s] levels', filename, startdir, levels)) unless @logger.nil?
|
690
637
|
|
691
638
|
dirs = startdir.split('/')
|
692
639
|
count = 0
|
@@ -715,6 +662,11 @@ class Rouster
|
|
715
662
|
def check_key_permissions(key, fix=false)
|
716
663
|
allowed_modes = ['0400', '0600']
|
717
664
|
|
665
|
+
if key.match(/\.pub$/)
|
666
|
+
# if this is the public half of the key, be more permissive
|
667
|
+
allowed_modes << '0644'
|
668
|
+
end
|
669
|
+
|
718
670
|
raw = self._run(sprintf('ls -l %s', key))
|
719
671
|
perms = self.parse_ls_string(raw)
|
720
672
|
|