rouster 0.7 → 0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +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
|