rouster 0.7 → 0.41
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +0 -3
- data/README.md +7 -241
- data/Rakefile +18 -55
- data/Vagrantfile +8 -26
- data/lib/rouster.rb +183 -404
- data/lib/rouster/deltas.rb +118 -577
- data/lib/rouster/puppet.rb +34 -209
- data/lib/rouster/testing.rb +59 -366
- data/lib/rouster/tests.rb +19 -70
- data/path_helper.rb +7 -5
- data/rouster.gemspec +1 -3
- data/test/basic.rb +1 -4
- data/test/functional/deltas/test_get_groups.rb +2 -74
- data/test/functional/deltas/test_get_packages.rb +4 -86
- data/test/functional/deltas/test_get_ports.rb +1 -26
- data/test/functional/deltas/test_get_services.rb +4 -43
- data/test/functional/deltas/test_get_users.rb +2 -35
- data/test/functional/puppet/test_facter.rb +1 -41
- data/test/functional/puppet/test_get_puppet_star.rb +68 -0
- data/test/functional/test_caching.rb +1 -5
- data/test/functional/test_dirs.rb +0 -25
- data/test/functional/test_get.rb +6 -10
- data/test/functional/test_inspect.rb +1 -1
- data/test/functional/test_is_file.rb +1 -17
- data/test/functional/test_new.rb +22 -233
- data/test/functional/test_put.rb +11 -9
- data/test/functional/test_restart.rb +4 -1
- data/test/functional/test_run.rb +3 -2
- data/test/puppet/test_apply.rb +11 -13
- data/test/puppet/test_roles.rb +173 -0
- data/test/unit/test_new.rb +0 -88
- data/test/unit/test_parse_ls_string.rb +0 -67
- data/test/unit/testing/test_validate_file.rb +47 -39
- data/test/unit/testing/test_validate_package.rb +10 -36
- metadata +6 -46
- data/.reek +0 -63
- data/.travis.yml +0 -11
- data/Gemfile +0 -17
- data/Gemfile.lock +0 -102
- data/LICENSE +0 -9
- data/examples/aws.rb +0 -85
- data/examples/openstack.rb +0 -61
- data/examples/passthrough.rb +0 -71
- data/lib/rouster/vagrant.rb +0 -311
- data/plugins/aws.rb +0 -347
- data/plugins/openstack.rb +0 -136
- data/test/functional/deltas/test_get_crontab.rb +0 -161
- data/test/functional/deltas/test_get_os.rb +0 -68
- data/test/functional/test_is_in_file.rb +0 -40
- data/test/functional/test_passthroughs.rb +0 -94
- data/test/functional/test_validate_file.rb +0 -131
- data/test/unit/puppet/resources/puppet_run_with_failed_exec +0 -59
- data/test/unit/puppet/resources/puppet_run_with_successful_exec +0 -61
- data/test/unit/puppet/test_get_puppet_star.rb +0 -91
- data/test/unit/puppet/test_puppet_parsing.rb +0 -44
- data/test/unit/testing/resources/osx-launchd +0 -285
- data/test/unit/testing/resources/rhel-systemd +0 -46
- data/test/unit/testing/resources/rhel-systemv +0 -41
- data/test/unit/testing/resources/rhel-upstart +0 -20
- data/test/unit/testing/test_get_services.rb +0 -178
- data/test/unit/testing/test_validate_cron.rb +0 -78
- data/test/unit/testing/test_validate_port.rb +0 -103
data/lib/rouster.rb
CHANGED
@@ -7,241 +7,87 @@ 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'
|
11
10
|
|
12
11
|
class Rouster
|
13
12
|
|
14
13
|
# sporadically updated version number
|
15
|
-
VERSION = 0.
|
14
|
+
VERSION = 0.41
|
16
15
|
|
17
16
|
# custom exceptions -- what else do we want them to include/do?
|
18
|
-
class ArgumentError < StandardError; end # thrown by methods that take parameters from users
|
19
17
|
class FileTransferError < StandardError; end # thrown by get() and put()
|
20
18
|
class InternalError < StandardError; end # thrown by most (if not all) Rouster methods
|
21
19
|
class ExternalError < StandardError; end # thrown when external dependencies do not respond as expected
|
22
20
|
class LocalExecutionError < StandardError; end # thrown by _run()
|
23
21
|
class RemoteExecutionError < StandardError; end # thrown by run()
|
24
|
-
class PassthroughError < StandardError; end # thrown by anything Passthrough related (mostly vagrant.rb)
|
25
22
|
class SSHConnectionError < StandardError; end # thrown by available_via_ssh() -- and potentially _run()
|
26
23
|
|
27
|
-
attr_accessor :facts, :
|
28
|
-
attr_reader :cache, :cache_timeout, :deltas, :exitcode, :
|
24
|
+
attr_accessor :facts, :sudo, :verbosity
|
25
|
+
attr_reader :cache, :cache_timeout, :deltas, :exitcode, :log, :name, :output, :passthrough, :sshkey, :vagrantfile
|
29
26
|
|
30
27
|
##
|
31
28
|
# initialize - object instantiation
|
32
29
|
#
|
33
30
|
# parameters
|
34
|
-
# * <name>
|
35
|
-
# * [cache_timeout]
|
36
|
-
# * [
|
37
|
-
# * [
|
38
|
-
# * [
|
39
|
-
# * [
|
40
|
-
# * [
|
41
|
-
# * [
|
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)
|
31
|
+
# * <name> - the name of the VM as specified in the Vagrantfile
|
32
|
+
# * [cache_timeout] - integer specifying how long Rouster should cache status() and is_available_via_ssh?() results, default is false
|
33
|
+
# * [passthrough] - boolean of whether this is a VM or passthrough, default is false -- passthrough is not completely implemented
|
34
|
+
# * [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/)
|
35
|
+
# * [sshtunnel] - boolean of whether or not to instantiate the SSH tunnel upon upping the VM, default is true
|
36
|
+
# * [sudo] - boolean of whether or not to prefix commands run in VM with 'sudo', default is true
|
37
|
+
# * [vagrantfile] - the full or relative path to the Vagrantfile to use, if not specified, will look for one in 5 directories above current location
|
38
|
+
# * [verbosity] - DEBUG (0) < INFO (1) < WARN (2) < ERROR (3) < FATAL (4)
|
46
39
|
def initialize(opts = nil)
|
47
|
-
@cache_timeout
|
48
|
-
@
|
49
|
-
@
|
50
|
-
@
|
51
|
-
@
|
52
|
-
@
|
53
|
-
@
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
@
|
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
|
40
|
+
@cache_timeout = opts[:cache_timeout].nil? ? false : opts[:cache_timeout]
|
41
|
+
@name = opts[:name]
|
42
|
+
@passthrough = opts[:passthrough].nil? ? false : opts[:passthrough]
|
43
|
+
@sshkey = opts[:sshkey]
|
44
|
+
@sshtunnel = opts[:sshtunnel].nil? ? true : opts[:sshtunnel]
|
45
|
+
@vagrantfile = opts[:vagrantfile].nil? ? traverse_up(Dir.pwd, 'Vagrantfile', 5) : opts[:vagrantfile]
|
46
|
+
@verbosity = opts[:verbosity].is_a?(Integer) ? opts[:verbosity] : 4
|
47
|
+
|
48
|
+
if opts.has_key?(:sudo)
|
49
|
+
@sudo = opts[:sudo]
|
50
|
+
elsif @passthrough.eql?(true)
|
51
|
+
@sudo = false
|
76
52
|
else
|
77
|
-
@
|
78
|
-
@verbosity_logfile = 2 # this is kind of arbitrary, but won't actually be created unless opts[:logfile] is also passed
|
53
|
+
@sudo = true
|
79
54
|
end
|
80
55
|
|
81
|
-
@ostype
|
82
|
-
@osversion = nil
|
83
|
-
|
56
|
+
@ostype = nil
|
84
57
|
@output = Array.new
|
85
58
|
@cache = Hash.new
|
86
59
|
@deltas = Hash.new
|
87
60
|
|
88
61
|
@exitcode = nil
|
89
|
-
@
|
90
|
-
@ssh_info = nil # hash containing connection information
|
62
|
+
@ssh_info = nil # will be hash containing connection information
|
91
63
|
|
92
64
|
# set up logging
|
93
65
|
require 'log4r/config'
|
94
66
|
Log4r.define_levels(*Log4r::Log4rConfig::LogLevels)
|
95
67
|
|
96
|
-
@
|
97
|
-
@
|
98
|
-
|
68
|
+
@log = Log4r::Logger.new(sprintf('rouster:%s', @name))
|
69
|
+
@log.outputters = Log4r::Outputter.stderr
|
70
|
+
@log.level = @verbosity
|
99
71
|
|
100
|
-
|
101
|
-
|
102
|
-
|
72
|
+
@log.debug('Vagrantfile and VM name validation..')
|
73
|
+
unless File.file?(@vagrantfile)
|
74
|
+
raise InternalError.new(sprintf('specified Vagrantfile [%s] does not exist', @vagrantfile))
|
103
75
|
end
|
104
76
|
|
105
|
-
|
77
|
+
raise InternalError.new() if @name.nil?
|
78
|
+
return if opts[:unittest].eql?(true) # quick return if we're a unit test
|
106
79
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
@sudo = false
|
112
|
-
else
|
113
|
-
@sudo = true
|
80
|
+
begin
|
81
|
+
self.status()
|
82
|
+
rescue Rouster::LocalExecutionError
|
83
|
+
raise InternalError.new()
|
114
84
|
end
|
115
85
|
|
116
|
-
|
117
|
-
|
118
|
-
@
|
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
|
208
|
-
|
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
|
86
|
+
@log.debug('SSH key discovery and viability tests..')
|
87
|
+
if @sshkey.nil?
|
88
|
+
if @passthrough.eql?(true)
|
89
|
+
raise InternalError.new('must specify sshkey when using a passthrough host')
|
222
90
|
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?
|
245
91
|
# ref the key from the vagrant home dir if it's been overridden
|
246
92
|
@sshkey = sprintf('%s/insecure_private_key', ENV['VAGRANT_HOME']) if ENV['VAGRANT_HOME']
|
247
93
|
@sshkey = sprintf('%s/.vagrant.d/insecure_private_key', ENV['HOME']) unless ENV['VAGRANT_HOME']
|
@@ -253,18 +99,19 @@ class Rouster
|
|
253
99
|
raise InternalError.new('ssh key does not exist') unless File.file?(@sshkey)
|
254
100
|
self.check_key_permissions(@sshkey)
|
255
101
|
rescue => e
|
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
|
-
|
102
|
+
raise InternalError.new("specified key [#{@sshkey}] has bad permissions. Vagrant exception: [#{e.message}]")
|
261
103
|
end
|
262
104
|
|
263
105
|
if @sshtunnel
|
264
|
-
self.
|
106
|
+
unless self.status.eql?('running')
|
107
|
+
@log.info(sprintf('upping machine[%s] in order to open SSH tunnel', @name))
|
108
|
+
self.up()
|
109
|
+
end
|
110
|
+
|
111
|
+
self.connect_ssh_tunnel()
|
265
112
|
end
|
266
113
|
|
267
|
-
@
|
114
|
+
@log.info('Rouster object successfully instantiated')
|
268
115
|
end
|
269
116
|
|
270
117
|
|
@@ -273,15 +120,81 @@ class Rouster
|
|
273
120
|
#
|
274
121
|
# overloaded method to return useful information about Rouster objects
|
275
122
|
def inspect
|
276
|
-
s = self.status()
|
277
123
|
"name [#{@name}]:
|
278
124
|
is_available_via_ssh?[#{self.is_available_via_ssh?}],
|
279
125
|
passthrough[#{@passthrough}],
|
280
126
|
sshkey[#{@sshkey}],
|
281
|
-
status[#{
|
127
|
+
status[#{self.status()}],
|
282
128
|
sudo[#{@sudo}],
|
283
129
|
vagrantfile[#{@vagrantfile}],
|
284
|
-
verbosity
|
130
|
+
verbosity[#{@verbosity}]\n"
|
131
|
+
end
|
132
|
+
|
133
|
+
## Vagrant methods
|
134
|
+
|
135
|
+
##
|
136
|
+
# up
|
137
|
+
# runs `vagrant up` from the Vagrantfile path
|
138
|
+
# if :sshtunnel is passed to the object during instantiation, the tunnel is created here as well
|
139
|
+
def up
|
140
|
+
@log.info('up()')
|
141
|
+
self._run(sprintf('cd %s; vagrant up %s', File.dirname(@vagrantfile), @name))
|
142
|
+
|
143
|
+
@ssh_info = nil # in case the ssh-info has changed, a la destroy/rebuild
|
144
|
+
self.connect_ssh_tunnel() if @sshtunnel
|
145
|
+
end
|
146
|
+
|
147
|
+
##
|
148
|
+
# destroy
|
149
|
+
# runs `vagrant destroy <name>` from the Vagrantfile path
|
150
|
+
def destroy
|
151
|
+
@log.info('destroy()')
|
152
|
+
self._run(sprintf('cd %s; vagrant destroy -f %s', File.dirname(@vagrantfile), @name))
|
153
|
+
end
|
154
|
+
|
155
|
+
##
|
156
|
+
# status
|
157
|
+
#
|
158
|
+
# runs `vagrant status <name>` from the Vagrantfile path
|
159
|
+
# parses the status and provider out of output, but only status is returned
|
160
|
+
def status
|
161
|
+
status = nil
|
162
|
+
|
163
|
+
if @cache_timeout
|
164
|
+
if @cache.has_key?(:status)
|
165
|
+
if (Time.now.to_i - @cache[:status][:time]) < @cache_timeout
|
166
|
+
@log.debug(sprintf('using cached status[%s] from [%s]', @cache[:status][:status], @cache[:status][:time]))
|
167
|
+
return @cache[:status][:status]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
@log.info('status()')
|
173
|
+
self._run(sprintf('cd %s; vagrant status %s', File.dirname(@vagrantfile), @name))
|
174
|
+
|
175
|
+
# else case here is handled by non-0 exit code
|
176
|
+
if self.get_output().match(/^#{@name}\s*(.*\s?\w+)\s(.+)$/)
|
177
|
+
# $1 = name, $2 = provider
|
178
|
+
status = $1
|
179
|
+
end
|
180
|
+
|
181
|
+
if @cache_timeout
|
182
|
+
@cache[:status] = Hash.new unless @cache[:status].class.eql?(Hash)
|
183
|
+
@cache[:status][:time] = Time.now.to_i
|
184
|
+
@cache[:status][:status] = status
|
185
|
+
@log.debug(sprintf('caching status[%s] at [%s]', @cache[:status][:status], @cache[:status][:time]))
|
186
|
+
end
|
187
|
+
|
188
|
+
return status
|
189
|
+
end
|
190
|
+
|
191
|
+
##
|
192
|
+
# suspend
|
193
|
+
#
|
194
|
+
# runs `vagrant suspend <name>` from the Vagrantfile path
|
195
|
+
def suspend
|
196
|
+
@log.info('suspend()')
|
197
|
+
self._run(sprintf('cd %s; vagrant suspend %s', File.dirname(@vagrantfile), @name))
|
285
198
|
end
|
286
199
|
|
287
200
|
## internal methods
|
@@ -308,28 +221,10 @@ class Rouster
|
|
308
221
|
expected_exitcode = [expected_exitcode] unless expected_exitcode.class.eql?(Array) # yuck, but 2.0 no longer coerces strings into single element arrays
|
309
222
|
|
310
223
|
cmd = sprintf('%s%s; echo ec[$?]', self.uses_sudo? ? 'sudo ' : '', command)
|
311
|
-
@
|
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
|
224
|
+
@log.info(sprintf('vm running: [%s]', cmd))
|
326
225
|
|
327
|
-
|
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+)\]/)
|
226
|
+
output = @ssh.exec!(cmd)
|
227
|
+
if output.match(/ec\[(\d+)\]/)
|
333
228
|
@exitcode = $1.to_i
|
334
229
|
output.gsub!(/ec\[(\d+)\]\n/, '')
|
335
230
|
else
|
@@ -337,10 +232,9 @@ class Rouster
|
|
337
232
|
end
|
338
233
|
|
339
234
|
self.output.push(output)
|
340
|
-
@
|
235
|
+
@log.debug(sprintf('output: [%s]', output))
|
341
236
|
|
342
237
|
unless expected_exitcode.member?(@exitcode)
|
343
|
-
# TODO technically this could be a 'LocalPassthroughExecutionError' now too if local passthrough.. should we update?
|
344
238
|
raise RemoteExecutionError.new("output[#{output}], exitcode[#{@exitcode}], expected[#{expected_exitcode}]")
|
345
239
|
end
|
346
240
|
|
@@ -360,7 +254,7 @@ class Rouster
|
|
360
254
|
if @cache_timeout
|
361
255
|
if @cache.has_key?(:is_available_via_ssh?)
|
362
256
|
if (Time.now.to_i - @cache[:is_available_via_ssh?][:time]) < @cache_timeout
|
363
|
-
@
|
257
|
+
@log.debug(sprintf('using cached is_available_via_ssh?[%s] from [%s]', @cache[:is_available_via_ssh?][:status], @cache[:is_available_via_ssh?][:time]))
|
364
258
|
return @cache[:is_available_via_ssh?][:status]
|
365
259
|
end
|
366
260
|
end
|
@@ -368,27 +262,28 @@ class Rouster
|
|
368
262
|
|
369
263
|
if @ssh.nil? or @ssh.closed?
|
370
264
|
begin
|
371
|
-
|
372
|
-
rescue Rouster::InternalError, Net::SSH::Disconnect
|
265
|
+
self.connect_ssh_tunnel()
|
266
|
+
rescue Rouster::InternalError, Net::SSH::Disconnect => e
|
373
267
|
res = false
|
374
268
|
end
|
375
269
|
|
376
270
|
end
|
377
271
|
|
378
|
-
if res.nil?
|
272
|
+
if res.nil?
|
379
273
|
begin
|
380
274
|
self.run('echo functional test of SSH tunnel')
|
381
|
-
res = true
|
382
275
|
rescue
|
383
276
|
res = false
|
384
277
|
end
|
385
278
|
end
|
386
279
|
|
280
|
+
res = true if res.nil?
|
281
|
+
|
387
282
|
if @cache_timeout
|
388
283
|
@cache[:is_available_via_ssh?] = Hash.new unless @cache[:is_available_via_ssh?].class.eql?(Hash)
|
389
284
|
@cache[:is_available_via_ssh?][:time] = Time.now.to_i
|
390
285
|
@cache[:is_available_via_ssh?][:status] = res
|
391
|
-
@
|
286
|
+
@log.debug(sprintf('caching is_available_via_ssh?[%s] at [%s]', @cache[:is_available_via_ssh?][:status], @cache[:is_available_via_ssh?][:time]))
|
392
287
|
end
|
393
288
|
|
394
289
|
res
|
@@ -405,11 +300,10 @@ class Rouster
|
|
405
300
|
h = Hash.new()
|
406
301
|
|
407
302
|
if @ssh_info.class.eql?(Hash)
|
408
|
-
@logger.debug('using cached SSH info')
|
409
303
|
h = @ssh_info
|
410
304
|
else
|
411
305
|
|
412
|
-
res = self.
|
306
|
+
res = self._run(sprintf('cd %s; vagrant ssh-config %s', File.dirname(@vagrantfile), @name))
|
413
307
|
|
414
308
|
res.split("\n").each do |line|
|
415
309
|
if line.match(/HostName (.*?)$/)
|
@@ -419,8 +313,8 @@ class Rouster
|
|
419
313
|
elsif line.match(/Port (\d*?)$/)
|
420
314
|
h[:ssh_port] = $1
|
421
315
|
elsif line.match(/IdentityFile (.*?)$/)
|
316
|
+
# TODO what to do if the user has specified @sshkey ?
|
422
317
|
h[:identity_file] = $1
|
423
|
-
@logger.info(sprintf('vagrant specified key[%s] differs from provided[%s], will use both', @sshkey, h[:identity_file]))
|
424
318
|
end
|
425
319
|
end
|
426
320
|
|
@@ -437,73 +331,18 @@ class Rouster
|
|
437
331
|
#
|
438
332
|
# raises its own InternalError if the machine isn't running, otherwise returns Net::SSH connection object
|
439
333
|
def connect_ssh_tunnel
|
334
|
+
@log.debug('opening SSH tunnel..')
|
440
335
|
|
441
|
-
if self.
|
442
|
-
|
443
|
-
|
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
|
-
|
336
|
+
if self.status.eql?('running')
|
337
|
+
self.get_ssh_info()
|
338
|
+
@ssh = Net::SSH.start(@ssh_info[:hostname], @ssh_info[:user], :port => @ssh_info[:ssh_port], :keys => [@sshkey], :paranoid => false)
|
472
339
|
else
|
473
|
-
|
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
|
340
|
+
raise InternalError.new('VM is not running, unable open SSH tunnel')
|
490
341
|
end
|
491
342
|
|
492
343
|
@ssh
|
493
344
|
end
|
494
345
|
|
495
|
-
##
|
496
|
-
# disconnect_ssh_tunnel
|
497
|
-
#
|
498
|
-
# shuts down the persistent Net::SSH tunnel
|
499
|
-
#
|
500
|
-
def disconnect_ssh_tunnel
|
501
|
-
@logger.debug('closing SSH tunnel..')
|
502
|
-
|
503
|
-
@ssh.shutdown! unless @ssh.nil?
|
504
|
-
@ssh = nil
|
505
|
-
end
|
506
|
-
|
507
346
|
##
|
508
347
|
# os_type
|
509
348
|
#
|
@@ -514,61 +353,28 @@ class Rouster
|
|
514
353
|
return @ostype
|
515
354
|
end
|
516
355
|
|
517
|
-
res
|
356
|
+
res = nil
|
357
|
+
uname = self.run('uname -a')
|
518
358
|
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
359
|
+
case uname
|
360
|
+
when /Darwin/i
|
361
|
+
res = :osx
|
362
|
+
when /Sun|Solaris/i
|
363
|
+
res =:solaris
|
364
|
+
when /Ubuntu/i
|
365
|
+
res = :ubuntu
|
366
|
+
else
|
367
|
+
if self.is_file?('/etc/redhat-release')
|
368
|
+
res = :redhat
|
369
|
+
else
|
370
|
+
res = nil
|
525
371
|
end
|
526
|
-
end
|
527
|
-
break unless res.eql?(:invalid)
|
528
372
|
end
|
529
373
|
|
530
|
-
@logger.error(sprintf('unable to determine OS, looking for[%s]', Rouster.os_files)) if res.eql?(:invalid)
|
531
|
-
|
532
374
|
@ostype = res
|
533
375
|
res
|
534
376
|
end
|
535
377
|
|
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
|
-
|
572
378
|
##
|
573
379
|
# get
|
574
380
|
#
|
@@ -579,13 +385,13 @@ class Rouster
|
|
579
385
|
# * [local_file] - full or relative path (based on $PWD) of file to download to
|
580
386
|
#
|
581
387
|
# if no local_file is specified, will be downloaded to $PWD with the same shortname as it had in the VM
|
582
|
-
#
|
583
|
-
# returns true on successful download, false if the file DNE and raises a FileTransferError.. well, you know
|
584
388
|
def get(remote_file, local_file=nil)
|
585
389
|
# TODO what happens when we pass a wildcard as remote_file?
|
586
390
|
|
587
391
|
local_file = local_file.nil? ? File.basename(remote_file) : local_file
|
588
|
-
@
|
392
|
+
@log.debug(sprintf('scp from VM[%s] to host[%s]', remote_file, local_file))
|
393
|
+
|
394
|
+
# TODO should we do a self.file?(remote_file) test before trying to download?
|
589
395
|
|
590
396
|
begin
|
591
397
|
@ssh.scp.download!(remote_file, local_file)
|
@@ -593,7 +399,6 @@ class Rouster
|
|
593
399
|
raise FileTransferError.new(sprintf('unable to get[%s], exception[%s]', remote_file, e.message()))
|
594
400
|
end
|
595
401
|
|
596
|
-
return true
|
597
402
|
end
|
598
403
|
|
599
404
|
##
|
@@ -606,7 +411,7 @@ class Rouster
|
|
606
411
|
# * [remote_file] - full or relative path (based on ~vagrant) of filename to upload to
|
607
412
|
def put(local_file, remote_file=nil)
|
608
413
|
remote_file = remote_file.nil? ? File.basename(local_file) : remote_file
|
609
|
-
@
|
414
|
+
@log.debug(sprintf('scp from host[%s] to VM[%s]', local_file, remote_file))
|
610
415
|
|
611
416
|
raise FileTransferError.new(sprintf('unable to put[%s], local file does not exist', local_file)) unless File.file?(local_file)
|
612
417
|
|
@@ -616,7 +421,6 @@ class Rouster
|
|
616
421
|
raise FileTransferError.new(sprintf('unable to put[%s], exception[%s]', local_file, e.message()))
|
617
422
|
end
|
618
423
|
|
619
|
-
return true
|
620
424
|
end
|
621
425
|
|
622
426
|
##
|
@@ -624,7 +428,7 @@ class Rouster
|
|
624
428
|
#
|
625
429
|
# convenience getter for @passthrough truthiness
|
626
430
|
def is_passthrough?
|
627
|
-
|
431
|
+
self.passthrough.eql?(true)
|
628
432
|
end
|
629
433
|
|
630
434
|
##
|
@@ -632,7 +436,7 @@ class Rouster
|
|
632
436
|
#
|
633
437
|
# convenience getter for @sudo truthiness
|
634
438
|
def uses_sudo?
|
635
|
-
|
439
|
+
self.sudo.eql?(true)
|
636
440
|
end
|
637
441
|
|
638
442
|
##
|
@@ -640,7 +444,7 @@ class Rouster
|
|
640
444
|
#
|
641
445
|
# destroy and then up the machine in question
|
642
446
|
def rebuild
|
643
|
-
@
|
447
|
+
@log.debug('rebuild()')
|
644
448
|
self.destroy
|
645
449
|
self.up
|
646
450
|
end
|
@@ -652,49 +456,37 @@ class Rouster
|
|
652
456
|
#
|
653
457
|
# parameters
|
654
458
|
# * [wait] - number of seconds to wait until is_available_via_ssh?() returns true before assuming failure
|
655
|
-
def restart(wait=nil
|
656
|
-
@
|
459
|
+
def restart(wait=nil)
|
460
|
+
@log.debug('restart()')
|
657
461
|
|
658
|
-
if self.is_passthrough? and self.passthrough
|
659
|
-
@
|
462
|
+
if self.is_passthrough? and self.passthrough.eql?(local)
|
463
|
+
@log.warn(sprintf('intercepted [restart] sent to a local passthrough, no op'))
|
660
464
|
return nil
|
661
465
|
end
|
662
466
|
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
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
|
467
|
+
case os_type
|
468
|
+
when :osx
|
469
|
+
self.run('shutdown -r now')
|
470
|
+
when :redhat, :ubuntu
|
471
|
+
self.run('/sbin/shutdown -rf now')
|
472
|
+
when :solaris
|
473
|
+
self.run('shutdown -y -i5 -g0')
|
474
|
+
else
|
475
|
+
raise InternalError.new(sprintf('unsupported OS[%s]', @ostype))
|
682
476
|
end
|
683
477
|
|
684
|
-
@ssh, @ssh_info = nil # severing the SSH tunnel, getting ready in case this box is brought back up on a different port
|
685
|
-
|
686
478
|
if wait
|
687
479
|
inc = wait.to_i / 10
|
688
|
-
0.
|
689
|
-
@
|
690
|
-
|
480
|
+
0..wait.each do |e|
|
481
|
+
@log.debug(sprintf('waiting for reboot: round[%s], step[%s], total[%s]', e, inc, wait))
|
482
|
+
true if self.is_available_via_ssh?()
|
691
483
|
sleep inc
|
692
484
|
end
|
693
485
|
|
694
|
-
|
486
|
+
false
|
695
487
|
end
|
696
488
|
|
697
|
-
|
489
|
+
@ssh, @ssh_info = nil, nil
|
698
490
|
end
|
699
491
|
|
700
492
|
##
|
@@ -708,22 +500,22 @@ class Rouster
|
|
708
500
|
# parameters
|
709
501
|
# * <command> - command to be run
|
710
502
|
def _run(command)
|
711
|
-
tmp_file = sprintf('/tmp/rouster
|
503
|
+
tmp_file = sprintf('/tmp/rouster.%s.%s', Time.now.to_i, $$)
|
712
504
|
cmd = sprintf('%s > %s 2> %s', command, tmp_file, tmp_file) # this is a holdover from Salesforce::Vagrant, can we use '2&>1' here?
|
713
505
|
res = `#{cmd}` # what does this actually hold?
|
714
506
|
|
715
|
-
@
|
507
|
+
@log.info(sprintf('host running: [%s]', cmd))
|
716
508
|
|
717
509
|
output = File.read(tmp_file)
|
718
510
|
File.delete(tmp_file) or raise InternalError.new(sprintf('unable to delete [%s]: %s', tmp_file, $!))
|
719
511
|
|
720
|
-
self.output.push(output)
|
721
|
-
@logger.debug(sprintf('output: [%s]', output))
|
722
|
-
|
723
512
|
unless $?.success?
|
724
513
|
raise LocalExecutionError.new(sprintf('command [%s] exited with code [%s], output [%s]', cmd, $?.to_i(), output))
|
725
514
|
end
|
726
515
|
|
516
|
+
self.output.push(output)
|
517
|
+
@log.debug(sprintf('output: [%s]', output))
|
518
|
+
|
727
519
|
@exitcode = $?.to_i()
|
728
520
|
output
|
729
521
|
end
|
@@ -765,9 +557,10 @@ class Rouster
|
|
765
557
|
# * [filename] - filename you are looking for
|
766
558
|
# * [levels] - number of directory levels to examine, default is 10
|
767
559
|
def traverse_up(startdir=Dir.pwd, filename=nil, levels=10)
|
560
|
+
# TODO not sure this signature is exactly right..
|
768
561
|
raise InternalError.new('must specify a filename') if filename.nil?
|
769
562
|
|
770
|
-
@
|
563
|
+
@log.debug(sprintf('traverse_up() looking for [%s] in [%s], up to [%s] levels', filename, startdir, levels)) unless @log.nil?
|
771
564
|
|
772
565
|
dirs = startdir.split('/')
|
773
566
|
count = 0
|
@@ -796,11 +589,6 @@ class Rouster
|
|
796
589
|
def check_key_permissions(key, fix=false)
|
797
590
|
allowed_modes = ['0400', '0600']
|
798
591
|
|
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
|
-
|
804
592
|
raw = self._run(sprintf('ls -l %s', key))
|
805
593
|
perms = self.parse_ls_string(raw)
|
806
594
|
|
@@ -825,15 +613,6 @@ class Rouster
|
|
825
613
|
nil
|
826
614
|
end
|
827
615
|
|
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
|
-
|
837
616
|
end
|
838
617
|
|
839
618
|
class Object
|