rouster 0.5 → 0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.reek +63 -0
  4. data/.travis.yml +11 -0
  5. data/Gemfile +17 -0
  6. data/Gemfile.lock +102 -0
  7. data/README.md +233 -7
  8. data/Rakefile +52 -34
  9. data/Vagrantfile +26 -8
  10. data/examples/aws.rb +85 -0
  11. data/examples/openstack.rb +61 -0
  12. data/examples/passthrough.rb +71 -0
  13. data/lib/rouster.rb +380 -262
  14. data/lib/rouster/deltas.rb +470 -138
  15. data/lib/rouster/puppet.rb +155 -26
  16. data/lib/rouster/testing.rb +205 -46
  17. data/lib/rouster/tests.rb +40 -11
  18. data/lib/rouster/vagrant.rb +311 -0
  19. data/path_helper.rb +3 -4
  20. data/plugins/aws.rb +347 -0
  21. data/plugins/openstack.rb +136 -0
  22. data/test/basic.rb +4 -1
  23. data/test/functional/deltas/test_get_crontab.rb +64 -2
  24. data/test/functional/deltas/test_get_groups.rb +74 -2
  25. data/test/functional/deltas/test_get_os.rb +68 -0
  26. data/test/functional/deltas/test_get_packages.rb +73 -6
  27. data/test/functional/deltas/test_get_ports.rb +26 -1
  28. data/test/functional/deltas/test_get_services.rb +43 -5
  29. data/test/functional/deltas/test_get_users.rb +35 -2
  30. data/test/functional/puppet/test_facter.rb +41 -1
  31. data/test/functional/test_caching.rb +2 -2
  32. data/test/functional/test_inspect.rb +1 -1
  33. data/test/functional/test_is_file.rb +17 -1
  34. data/test/functional/test_is_in_file.rb +40 -0
  35. data/test/functional/test_new.rb +233 -22
  36. data/test/functional/test_passthroughs.rb +94 -0
  37. data/test/functional/test_put.rb +2 -2
  38. data/test/functional/test_validate_file.rb +104 -3
  39. data/test/puppet/test_apply.rb +8 -6
  40. data/test/unit/puppet/resources/puppet_run_with_failed_exec +59 -0
  41. data/test/unit/puppet/resources/puppet_run_with_successful_exec +61 -0
  42. data/test/unit/puppet/test_get_puppet_star.rb +27 -4
  43. data/test/unit/puppet/test_puppet_parsing.rb +44 -0
  44. data/test/unit/test_new.rb +88 -0
  45. data/test/unit/test_parse_ls_string.rb +67 -0
  46. data/test/unit/testing/resources/osx-launchd +285 -0
  47. data/test/unit/testing/resources/rhel-systemd +46 -0
  48. data/test/unit/testing/resources/rhel-systemv +41 -0
  49. data/test/unit/testing/resources/rhel-upstart +20 -0
  50. data/test/unit/testing/test_get_services.rb +178 -0
  51. data/test/unit/testing/test_validate_cron.rb +78 -0
  52. data/test/unit/testing/test_validate_package.rb +36 -10
  53. data/test/unit/testing/test_validate_port.rb +5 -0
  54. metadata +42 -21
  55. data/test/puppet/test_roles.rb +0 -186
@@ -44,7 +44,7 @@ class Rouster
44
44
  if raw.match(/No such file or directory/)
45
45
  res = nil
46
46
  elsif raw.match(/Permission denied/)
47
- @log.info(sprintf('dir(%s) output[%s], try with sudo', dir, raw)) unless self.uses_sudo?
47
+ @logger.info(sprintf('dir(%s) output[%s], try with sudo', dir, raw)) unless self.uses_sudo?
48
48
  res = nil
49
49
  else
50
50
  res = parse_ls_string(raw)
@@ -120,7 +120,7 @@ class Rouster
120
120
  end
121
121
 
122
122
  if raw.match(/No such file or directory/)
123
- @log.info(sprintf('is_file?(%s) output[%s], try with sudo', file, raw)) unless self.uses_sudo?
123
+ @logger.info(sprintf('is_file?(%s) output[%s], try with sudo', file, raw)) unless self.uses_sudo?
124
124
  res = nil
125
125
  elsif raw.match(/Permission denied/)
126
126
  res = nil
@@ -257,18 +257,19 @@ class Rouster
257
257
  # parameters
258
258
  # * <file> - path of filename to examine
259
259
  # * <regex> - regular expression/string to be passed to grep
260
+ # * <flags> - flags to include in grep command
260
261
  # * [scp] - downloads file to host machine before grepping (functionality not implemented, was planned when a new SSH connection was required for each run() command, not sure it is necessary any longer)
261
- def is_in_file?(file, regex, scp=false)
262
+ def is_in_file?(file, regex, flags='', scp=false)
262
263
 
263
264
  res = nil
264
265
 
265
266
  if scp
266
267
  # download the file to a temporary directory
267
- @log.warn('is_in_file? scp option not implemented yet')
268
+ @logger.warn('is_in_file? scp option not implemented yet')
268
269
  end
269
270
 
270
271
  begin
271
- command = sprintf("grep -c '%s' %s", regex, file)
272
+ command = sprintf("grep -c%s '%s' %s", flags, regex, file)
272
273
  res = self.run(command)
273
274
  rescue Rouster::RemoteExecutionError
274
275
  return false
@@ -373,14 +374,14 @@ class Rouster
373
374
  # * RedHat
374
375
  # * Ubuntu
375
376
  def is_process_running?(name)
376
- # TODO support other flavors - this will work on RHEL and OSX
377
- # TODO do better validation than just grepping for a matching filename
377
+ # TODO support Solaris
378
+ # TODO do better validation than just grepping for a matching filename, start with removing 'grep' from output
378
379
  begin
379
380
 
380
381
  os = self.os_type()
381
382
 
382
383
  case os
383
- when :redhat, :osx, :ubuntu, :debian
384
+ when :rhel, :osx, :ubuntu, :debian
384
385
  res = self.run(sprintf('ps ax | grep -c %s', name))
385
386
  else
386
387
  raise InternalError.new(sprintf('currently unable to determine running process list on OS[%s]', os))
@@ -467,6 +468,25 @@ class Rouster
467
468
  end
468
469
  end
469
470
 
471
+ ##
472
+ # is_symlink?
473
+ #
474
+ # uses file() to return boolean indicating whether parameter passed is a symlink
475
+ #
476
+ # parameters
477
+ # * <file> - path of filename to validate
478
+ def is_symlink?(file)
479
+ res = nil
480
+
481
+ begin
482
+ res = self.file(file)
483
+ rescue => e
484
+ return false
485
+ end
486
+
487
+ res.class.eql?(Hash) ? res[:symlink?] : false
488
+ end
489
+
470
490
  ##
471
491
  # is_user?
472
492
  #
@@ -564,8 +584,8 @@ class Rouster
564
584
  value += 4
565
585
  when 'w'
566
586
  value += 2
567
- when 'x', 't'
568
- # is 't' really right here? copying Salesforce::Vagrant
587
+ when 'x', 't', 's'
588
+ # is 't' / 's' really right here? copying Salesforce::Vagrant
569
589
  value += 1
570
590
  when '-'
571
591
  # noop
@@ -579,17 +599,26 @@ class Rouster
579
599
  end
580
600
 
581
601
  res[:mode] = mode
582
- res[:name] = tokens[-1] # TODO better here: this does not support files/dirs with spaces
583
602
  res[:owner] = tokens[2]
584
603
  res[:group] = tokens[3]
585
604
  res[:size] = tokens[4]
586
605
 
587
606
  res[:directory?] = tokens[0][0].chr.eql?('d')
588
607
  res[:file?] = ! res[:directory?]
608
+ res[:symlink?] = tokens[0][0].chr.eql?('l')
589
609
  res[:executable?] = [ tokens[0][3].chr.eql?('x'), tokens[0][6].chr.eql?('x'), tokens[0][9].chr.eql?('x') || tokens[0][9].chr.eql?('t') ]
590
610
  res[:writeable?] = [ tokens[0][2].chr.eql?('w'), tokens[0][5].chr.eql?('w'), tokens[0][8].chr.eql?('w') ]
591
611
  res[:readable?] = [ tokens[0][1].chr.eql?('r'), tokens[0][4].chr.eql?('r'), tokens[0][7].chr.eql?('r') ]
592
612
 
613
+ # TODO better here: this does not support files/dirs with spaces
614
+ if res[:symlink?]
615
+ # not sure if we should only be adding this value if we're a symlink, or adding it to all results and just using nil if not a link
616
+ res[:target] = tokens[-1]
617
+ res[:name] = tokens[-3]
618
+ else
619
+ res[:name] = tokens[-1]
620
+ end
621
+
593
622
  res
594
623
  end
595
624
 
@@ -0,0 +1,311 @@
1
+ require sprintf('%s/../../%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
2
+
3
+ ## Vagrant specific (and related) methods
4
+
5
+ class Rouster
6
+
7
+ ##
8
+ # vagrant
9
+ #
10
+ # abstraction layer to call vagrant faces
11
+ #
12
+ # parameters
13
+ # * <face> - vagrant face to call (include arguments)
14
+ def vagrant(face, sleep_time=10)
15
+ if self.is_passthrough?
16
+ @logger.warn(sprintf('calling [vagrant %s] on a passthrough host is a noop', face))
17
+ return nil
18
+ end
19
+
20
+ unless @vagrant_concurrency.eql?(true)
21
+ # TODO don't (ab|re)use variables
22
+ 0.upto(@retries) do |try|
23
+ break if self.is_vagrant_running?().eql?(false)
24
+
25
+ sleep sleep_time # TODO log a message?
26
+ end
27
+ end
28
+
29
+ 0.upto(@retries) do |try| # TODO should really be doing this with 'retry', but i think this code is actually cleaner
30
+ begin
31
+ return self._run(sprintf('cd %s; vagrant %s', File.dirname(@vagrantfile), face))
32
+ rescue
33
+ @logger.error(sprintf('failed vagrant command[%s], attempt[%s/%s]', face, try, retries)) if self.retries > 0
34
+ sleep sleep_time
35
+ end
36
+ end
37
+
38
+ raise InternalError.new(sprintf('failed to execute [%s], exitcode[%s], output[%s]', face, self.exitcode, self.get_output()))
39
+
40
+
41
+ end
42
+
43
+ ##
44
+ # up
45
+ # runs `vagrant up <name>` from the Vagrantfile path
46
+ # if :sshtunnel is passed to the object during instantiation, the tunnel is created here as well
47
+ def up
48
+ @logger.info('up()')
49
+
50
+ # don't like putting this here, may be refactored
51
+ if self.is_passthrough?
52
+ if (self.passthrough[:type].equal?(:aws) or self.passthrough[:type].equal?(:raiden))
53
+ self.aws_up()
54
+ elsif (self.passthrough[:type].equal?(:openstack))
55
+ self.ostack_up()
56
+ else
57
+ self.vagrant(sprintf('up %s', @name))
58
+ end
59
+ else
60
+ self.vagrant(sprintf('up %s', @name))
61
+ end
62
+
63
+ @ssh_info = nil # in case the ssh-info has changed, a la destroy/rebuild
64
+ self.connect_ssh_tunnel() if @sshtunnel
65
+ end
66
+
67
+ ##
68
+ # halt
69
+ # runs `vagrant halt <name>` from the Vagrantfile path
70
+ def halt
71
+ @logger.info('halt()')
72
+ self.vagrant(sprintf('halt %s', @name))
73
+ end
74
+
75
+ ##
76
+ # package -- though vagrant docs still refer to 'repackage'
77
+ # runs `vagrant package <name> <provider>`
78
+ def package(provider='virtualbox') # TODO get the provider as a first class citizen on the rouster object
79
+ @logger.info(sprintf('package(%s)', provider))
80
+ self.vagrant(sprintf('package %s %s', @name, provider))
81
+ end
82
+
83
+ ##
84
+ # destroy
85
+ # runs `vagrant destroy <name>` from the Vagrantfile path
86
+ def destroy
87
+ @logger.info('destroy()')
88
+
89
+ # don't like putting this here, may be refactored
90
+ if self.is_passthrough?
91
+ if (self.passthrough[:type].equal?(:aws) or self.passthrough[:type].equal?(:raiden))
92
+ self.aws_destroy()
93
+ elsif self.is_passthrough? and self.passthrough[:type].equal?(:openstack)
94
+ self.ostack_destroy()
95
+ else
96
+ raise InternalError.new(sprintf('failed to execute destroy(), unsupported passthrough type %s', self.passthrough[:type]))
97
+ end
98
+ else
99
+ self.vagrant(sprintf('destroy -f %s', @name))
100
+ end
101
+
102
+ disconnect_ssh_tunnel
103
+ end
104
+
105
+ ##
106
+ # status
107
+ #
108
+ # runs `vagrant status <name>` from the Vagrantfile path
109
+ # parses the status and provider out of output, but only status is returned
110
+ def status
111
+ status = nil
112
+
113
+ if @cache_timeout
114
+ if @cache.has_key?(:status)
115
+ if (Time.now.to_i - @cache[:status][:time]) < @cache_timeout
116
+ @logger.debug(sprintf('using cached status[%s] from [%s]', @cache[:status][:status], @cache[:status][:time]))
117
+ return @cache[:status][:status]
118
+ end
119
+ end
120
+ end
121
+
122
+ # don't like putting this here, may be refactored
123
+ @logger.info('status()')
124
+ if self.is_passthrough?
125
+ if (self.passthrough[:type].equal?(:aws) or self.passthrough[:type].equal?(:raiden))
126
+ status = self.aws_status()
127
+ elsif self.passthrough[:type].equal?(:openstack)
128
+ status = self.ostack_status()
129
+ else
130
+ raise InternalError.new(sprintf('failed to execute status(), unsupported passthrough type %s', self.passthrough[:type]))
131
+ end
132
+ else
133
+ self.vagrant(sprintf('status %s', @name))
134
+
135
+ # else case here (both for nil/non-matching output) is handled by non-0 exit code
136
+ output = self.get_output()
137
+ if output.nil?
138
+ if self.is_passthrough?() and self.passthrough[:type].eql?(:local)
139
+ status = 'running'
140
+ else
141
+ status = 'not-created'
142
+ end
143
+ elsif output.match(/^#{@name}\s*(.*\s?\w+)\s\((.+)\)$/)
144
+ # vagrant 1.2+, $1 = status, $2 = provider
145
+ status = $1
146
+ elsif output.match(/^#{@name}\s+(.+)$/)
147
+ # vagrant 1.2-, $1 = status
148
+ status = $1
149
+ end
150
+ end
151
+
152
+ if @cache_timeout
153
+ @cache[:status] = Hash.new unless @cache[:status].class.eql?(Hash)
154
+ @cache[:status][:time] = Time.now.to_i
155
+ @cache[:status][:status] = status
156
+ @logger.debug(sprintf('caching status[%s] at [%s]', @cache[:status][:status], @cache[:status][:time]))
157
+ end
158
+
159
+ return status
160
+ end
161
+
162
+ ##
163
+ # reload
164
+ #
165
+ # runs `vagrant reload <name> [--no-provision]` from the Vagrantfile path
166
+ # +no_provision+ Boolean whether or not to stop reprovisioning
167
+ def reload(no_provision = true)
168
+
169
+ if self.is_passthrough?
170
+ @logger.warn('calling [vagrant reload] on a passthrough host is a noop')
171
+ return nil
172
+ end
173
+
174
+ @logger.info('reload()')
175
+ self.vagrant(sprintf('reload %s %s', @name, no_provision ? '--no-provision' : ''))
176
+ end
177
+
178
+ ##
179
+ # suspend
180
+ #
181
+ # runs `vagrant suspend <name>` from the Vagrantfile path
182
+ def suspend
183
+ @logger.info('suspend()')
184
+ self.vagrant(sprintf('suspend %s', @name))
185
+ disconnect_ssh_tunnel() unless self.is_passthrough?()
186
+ end
187
+
188
+ ##
189
+ # is_vagrant_running?()
190
+ #
191
+ # returns true|false if a vagrant process is running on the host machine
192
+ #
193
+ # meant to be used to prevent race-y conditions when interacting with VirtualBox (potentially others, haven't tested)
194
+ def is_vagrant_running?
195
+ res = false
196
+
197
+ begin
198
+ # TODO would like to get the 2 -v greps into a single call..
199
+ raw = self._run("ps -ef | grep -v 'grep' | grep -v 'ssh' | grep '#{self.vagrantbinary}'")
200
+ res = true
201
+ rescue
202
+ end
203
+
204
+ @logger.debug(sprintf('is_vagrant_running?[%s]', res))
205
+ res
206
+ end
207
+
208
+ ##
209
+ # sandbox_available?
210
+ #
211
+ # returns true or false after attempting to find out if the sandbox
212
+ # subcommand is available
213
+ def sandbox_available?
214
+ if self.is_passthrough?
215
+ @logger.warn('sandbox* methods on a passthrough host is a noop')
216
+ return nil
217
+ end
218
+
219
+ if @cache.has_key?(:sandbox_available?)
220
+ @logger.debug(sprintf('using cached sandbox_available?[%s]', @cache[:sandbox_available?]))
221
+ return @cache[:sandbox_available?]
222
+ end
223
+
224
+ @logger.info('sandbox_available()')
225
+ begin
226
+ # at some point, vagrant changed its behavior on exit code here, so rescuing
227
+ self._run(sprintf('cd %s; vagrant', File.dirname(@vagrantfile))) # calling 'vagrant' without parameters to determine available faces
228
+ rescue
229
+ end
230
+
231
+ sandbox_available = false
232
+ if self.get_output().match(/^\s+sandbox$/)
233
+ sandbox_available = true
234
+ end
235
+
236
+ @cache[:sandbox_available?] = sandbox_available
237
+ @logger.debug(sprintf('caching sandbox_available?[%s]', @cache[:sandbox_available?]))
238
+ @logger.error('sandbox support is not available, please install the "sahara" gem first, https://github.com/jedi4ever/sahara') unless sandbox_available
239
+
240
+ return sandbox_available
241
+ end
242
+
243
+ ##
244
+ # sandbox_on
245
+ # runs `vagrant sandbox on` from the Vagrantfile path
246
+ def sandbox_on
247
+ if self.is_passthrough?
248
+ @logger.warn('sandbox* methods on a passthrough host is a noop')
249
+ return nil
250
+ end
251
+
252
+ if self.sandbox_available?
253
+ return self.vagrant(sprintf('sandbox on %s', @name))
254
+ else
255
+ raise ExternalError.new('sandbox plugin not installed')
256
+ end
257
+ end
258
+
259
+ ##
260
+ # sandbox_off
261
+ # runs `vagrant sandbox off` from the Vagrantfile path
262
+ def sandbox_off
263
+ if self.is_passthrough?
264
+ @logger.warn('sandbox* methods on a passthrough host is a noop')
265
+ return nil
266
+ end
267
+
268
+ if self.sandbox_available?
269
+ return self.vagrant(sprintf('sandbox off %s', @name))
270
+ else
271
+ raise ExternalError.new('sandbox plugin not installed')
272
+ end
273
+ end
274
+
275
+ ##
276
+ # sandbox_rollback
277
+ # runs `vagrant sandbox rollback` from the Vagrantfile path
278
+ def sandbox_rollback
279
+ if self.is_passthrough?
280
+ @logger.warn('sandbox* methods on a passthrough host is a noop')
281
+ return nil
282
+ end
283
+
284
+ if self.sandbox_available?
285
+ self.disconnect_ssh_tunnel
286
+ self.vagrant(sprintf('sandbox rollback %s', @name))
287
+ self.connect_ssh_tunnel
288
+ else
289
+ raise ExternalError.new('sandbox plugin not installed')
290
+ end
291
+ end
292
+
293
+ ##
294
+ # sandbox_commit
295
+ # runs `vagrant sandbox commit` from the Vagrantfile path
296
+ def sandbox_commit
297
+ if self.is_passthrough?
298
+ @logger.warn('sandbox* methods on a passthrough host is a noop')
299
+ return nil
300
+ end
301
+
302
+ if self.sandbox_available?
303
+ self.disconnect_ssh_tunnel
304
+ self.vagrant(sprintf('sandbox commit %s', @name))
305
+ self.connect_ssh_tunnel
306
+ else
307
+ raise ExternalError.new('sandbox plugin not installed')
308
+ end
309
+ end
310
+
311
+ end
@@ -2,14 +2,13 @@
2
2
 
3
3
  # this gets us Rouster, still need to figure out how to find vagrant
4
4
  $LOAD_PATH << File.join([File.dirname(__FILE__), 'lib'])
5
+ $LOAD_PATH << File.join([File.dirname(__FILE__), 'plugins'])
6
+ $LOAD_PATH << File.expand_path(sprintf('%s/..', File.dirname(__FILE__)))
7
+ $LOAD_PATH << File.dirname(__FILE__)
5
8
 
6
9
  require 'rubygems'
7
10
 
8
11
  # debugging help
9
- begin
10
- require 'debugger'
11
- rescue LoadError
12
- end
13
12
 
14
13
  class Object
15
14
  def my_methods