rouster 0.53 → 0.57
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/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
|
|