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
@@ -5,8 +5,6 @@ require 'net/https'
5
5
  require 'socket'
6
6
  require 'uri'
7
7
 
8
- # TODO use @cache_timeout to invalidate data cached here
9
-
10
8
  class Rouster
11
9
 
12
10
  ##
@@ -18,8 +16,17 @@ class Rouster
18
16
  # * [cache] - whether to store/return cached facter data, if available
19
17
  # * [custom_facts] - whether to include custom facts in return (uses -p argument)
20
18
  def facter(cache=true, custom_facts=true)
19
+
21
20
  if cache.true? and ! self.facts.nil?
22
- return self.facts
21
+
22
+ if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:facter]) > self.cache_timeout
23
+ @logger.debug(sprintf('invalidating [facter] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:facter]), self.cache_timeout))
24
+ self.facts = nil
25
+ else
26
+ @logger.debug(sprintf('using cached [facter] from [%s]', self.cache[:facter]))
27
+ return self.facts
28
+ end
29
+
23
30
  end
24
31
 
25
32
  raw = self.run(sprintf('facter %s', custom_facts.true? ? '-p' : ''))
@@ -31,12 +38,35 @@ class Rouster
31
38
  end
32
39
 
33
40
  if cache.true?
41
+ @logger.debug(sprintf('caching [facter] at [%s]', Time.now.asctime))
34
42
  self.facts = res
43
+ self.cache[:facter] = Time.now.to_i
35
44
  end
36
45
 
37
46
  res
38
47
  end
39
48
 
49
+ ##
50
+ # did_exec_fire?
51
+ #
52
+ # given the name of an Exec resource, parse the output from the most recent puppet run
53
+ # and return true/false based on whether the exec in question was fired
54
+ def did_exec_fire?(resource_name, puppet_run = self.last_puppet_run)
55
+ # Notice: /Stage[main]//Exec[foo]/returns: executed successfully
56
+ # Error: /Stage[main]//Exec[bar]/returns: change from notrun to 0 failed: Could not find command '/bin/bar'
57
+ matchers = [
58
+ 'Notice: /Stage\[.*\]//Exec\[%s\]/returns: executed successfully',
59
+ 'Error: /Stage\[.*\]//Exec\[%s\]/returns: change from notrun to 0 failed'
60
+ ]
61
+
62
+ matchers.each do |m|
63
+ matcher = sprintf(m, resource_name)
64
+ return true if puppet_run.match(matcher)
65
+ end
66
+
67
+ false
68
+ end
69
+
40
70
  ##
41
71
  # get_catalog
42
72
  #
@@ -84,8 +114,17 @@ class Rouster
84
114
  # parameters
85
115
  # * [input] - string to look at, defaults to self.get_output()
86
116
  def get_puppet_errors(input=nil)
87
- str = input.nil? ? self.get_output() : input
88
- errors = str.scan(/35merr:.*/)
117
+ str = input.nil? ? self.get_output() : input
118
+ errors = nil
119
+ errors_27 = str.scan(/35merr:.*/)
120
+ errors_30 = str.scan(/Error:.*/)
121
+
122
+ # TODO this is a little less than efficient, don't scan for 3.0 if you found 2.7
123
+ if errors_27.size > 0
124
+ errors = errors_27
125
+ else
126
+ errors = errors_30
127
+ end
89
128
 
90
129
  errors.empty? ? nil : errors
91
130
  end
@@ -98,8 +137,17 @@ class Rouster
98
137
  # parameters
99
138
  # * [input] - string to look at, defaults to self.get_output()
100
139
  def get_puppet_notices(input=nil)
101
- str = input.nil? ? self.get_output() : input
102
- notices = str.scan(/36mnotice:.*/)
140
+ str = input.nil? ? self.get_output() : input
141
+ notices = nil
142
+ notices_27 = str.scan(/36mnotice:.*/) # not sure when this stopped working
143
+ notices_30 = str.scan(/Notice:.*/)
144
+
145
+ # TODO this is a little less than efficient, don't scan for 3.0 if you found 2.7
146
+ if notices_27.size > 0
147
+ notices = notices_27
148
+ else
149
+ notices = notices_30
150
+ end
103
151
 
104
152
  notices.empty? ? nil : notices
105
153
  end
@@ -128,16 +176,41 @@ class Rouster
128
176
  # returns hiera results from self
129
177
  #
130
178
  # parameters
131
- # * <key> - hiera key to look up
132
- # * [config] - path to hiera configuration -- this is only optional if you have a hiera.yaml file in ~/vagrant
133
- def hiera(key, config=nil, options=nil)
179
+ # * <key> - hiera key to look up
180
+ # * [facts] - hash of facts to be used in hiera lookup (technically optional, but most useful hiera lookups are based on facts)
181
+ # * [config] - path to hiera configuration -- this is only optional if you have a hiera.yaml file in ~/vagrant, default option is correct for most puppet installations
182
+ # * [options] - any additional parameters to be passed to hiera directly
183
+ #
184
+ # note
185
+ # * if no facts are provided, facter() will be called - to really run hiera without facts, send an empty hash
186
+ # * this method is mostly useful on your puppet master, as your agents won't likely have /etc/puppet/hiera.yaml - to get data on another node, specify it's facts and call hiera on your ppm
187
+ def hiera(key, facts=nil, config='/etc/puppet/hiera.yaml', options=nil)
188
+ # TODO implement caching? where do we keep it? self.hiera{}? or self.deltas{} -- leaning towards #1
134
189
 
135
190
  cmd = 'hiera'
191
+
192
+ if facts.nil?
193
+ @logger.info('no facts provided, calling facter() automatically')
194
+ facts = self.facter()
195
+ end
196
+
197
+ if facts.keys.size > 0
198
+ scope_file = sprintf('/tmp/rouster-hiera_scope.%s.%s.json', $$, Time.now.to_i)
199
+
200
+ File.write(scope_file, facts.to_json)
201
+ self.put(scope_file, scope_file)
202
+ File.delete(scope_file)
203
+
204
+ cmd << sprintf(' -j %s', scope_file)
205
+ end
206
+
136
207
  cmd << sprintf(' -c %s', config) unless config.nil?
137
208
  cmd << sprintf(' %s', options) unless options.nil?
138
209
  cmd << sprintf(' %s', key)
139
210
 
140
- self.run(cmd)
211
+ raw = self.run(cmd)
212
+
213
+ JSON.parse(raw)
141
214
  end
142
215
 
143
216
  ##
@@ -245,13 +318,14 @@ class Rouster
245
318
  end
246
319
  end
247
320
 
248
-
249
321
  results[:classes] = classes
250
322
  results[:resources] = resources
251
323
 
252
324
  results
253
325
  end
254
326
 
327
+ # TODO: come up with better method names here.. remove_existing_certs() and remove_specific_cert() are not very descriptive
328
+
255
329
  ##
256
330
  # remove_existing_certs
257
331
  #
@@ -260,13 +334,23 @@ class Rouster
260
334
  #
261
335
  # parameters
262
336
  # * <puppetmaster> - string/partial regex of certificate names to keep
263
- def remove_existing_certs (puppetmaster)
264
- hosts = Array.new()
337
+ def remove_existing_certs (except)
338
+ except = except.kind_of?(Array) ? except : [except] # need to move from <>.class.eql? to <>.kind_of? in a number of places
339
+ hosts = Array.new()
265
340
 
266
341
  res = self.run('puppet cert list --all')
267
342
 
343
+ # TODO refactor this away from the hacky_break
268
344
  res.each_line do |line|
269
- next if line.match(/#{puppetmaster}/)
345
+ hacky_break = false
346
+
347
+ except.each do |exception|
348
+ next if hacky_break
349
+ hacky_break = line.match(/#{exception}/)
350
+ end
351
+
352
+ next if hacky_break
353
+
270
354
  host = $1 if line.match(/^\+\s"(.*?)"/)
271
355
 
272
356
  hosts.push(host) unless host.nil? # only want to clear signed certs
@@ -278,6 +362,38 @@ class Rouster
278
362
 
279
363
  end
280
364
 
365
+ ##
366
+ # remove_specific_cert
367
+ #
368
+ # ... removes a specific (or several specific) certificates, effectively the reverse of remove_existing_certs() - and again, really only useful when called on a puppet master
369
+ def remove_specific_cert (targets)
370
+ targets = targets.kind_of?(Array) ? targets : [targets]
371
+ hosts = Array.new()
372
+
373
+ res = self.run('puppet cert list --all')
374
+
375
+ res.each_line do |line|
376
+ hacky_break = true
377
+
378
+ targets.each do |target|
379
+ next unless hacky_break
380
+ hacky_break = line.match(/#{target}/)
381
+ end
382
+
383
+ next unless hacky_break
384
+
385
+ host = $1 if line.match(/^\+\s"(.*?)"/)
386
+ hosts.push(host) unless host.nil?
387
+
388
+ end
389
+
390
+ hosts.each do |host|
391
+ self.run(sprintf('puppet cert --clean %s', host))
392
+ end
393
+
394
+ end
395
+
396
+
281
397
  ##
282
398
  # run_puppet
283
399
  #
@@ -308,7 +424,7 @@ class Rouster
308
424
  # parameters
309
425
  # * [mode] - method to run puppet, defaults to 'master'
310
426
  # * [opts] - hash of additional options
311
- def run_puppet(mode='master', passed_opts=nil)
427
+ def run_puppet(mode='master', passed_opts={})
312
428
 
313
429
  if mode.eql?('master')
314
430
  opts = {
@@ -344,25 +460,36 @@ class Rouster
344
460
  :additional_options => nil
345
461
  }.merge!(passed_opts)
346
462
 
347
- ## validate required arguments
348
- raise InternalError.new(sprintf('invalid hiera config specified[%s]', opts[:hiera_config])) unless self.is_file?(opts[:hiera_config])
349
- raise InternalError.new(sprintf('invalid module dir specified[%s]', opts[:module_dir])) unless self.is_dir?(opts[:module_dir])
463
+ ## validate arguments -- can do better here (:manifest_dir, :manifest_file)
464
+ puppet_version = self.get_puppet_version() # hiera_config specification is only supported in >3.0, but NOT required anywhere
350
465
 
351
- puppet_version = self.get_puppet_version() # hiera_config specification is only supported in >3.0
466
+ if opts[:hiera_config]
467
+ if puppet_version > '3.0'
468
+ raise InternalError.new(sprintf('invalid hiera config specified[%s]', opts[:hiera_config])) unless self.is_file?(opts[:hiera_config])
469
+ else
470
+ @logger.error(sprintf('puppet version[%s] does not support --hiera_config, ignoring', puppet_version))
471
+ end
472
+ end
473
+
474
+ if opts[:module_dir]
475
+ raise InternalError.new(sprintf('invalid module dir specified[%s]', opts[:module_dir])) unless self.is_dir?(opts[:module_dir])
476
+ end
352
477
 
353
478
  if opts[:manifest_file]
354
479
  opts[:manifest_file] = opts[:manifest_file].class.eql?(Array) ? opts[:manifest_file] : [opts[:manifest_file]]
355
480
  opts[:manifest_file].each do |file|
356
481
  raise InternalError.new(sprintf('invalid manifest file specified[%s]', file)) unless self.is_file?(file)
357
482
 
358
- cmd = sprintf('puppet apply %s --modulepath=%s', (puppet_version > '3.0') ? "--hiera_config=#{opts[:hiera_config]}" : '', opts[:module_dir])
483
+ cmd = 'puppet apply --detailed-exitcodes'
484
+ cmd << sprintf(' --modulepath=%s', opts[:module_dir]) unless opts[:module_dir].nil?
485
+ cmd << sprintf(' --hiera_config=%s', opts[:hiera_config]) unless opts[:hiera_config].nil? or puppet_version < '3.0'
359
486
  cmd << sprintf(' --environment %s', opts[:environment]) unless opts[:environment].nil?
360
487
  cmd << sprintf(' --certname %s', opts[:certname]) unless opts[:certname].nil?
361
488
  cmd << ' --pluginsync' if opts[:pluginsync]
362
- cmd << opts[:additional_options] unless opts[:additional_options].nil?
489
+ cmd << sprintf(' %s', opts[:additional_options]) unless opts[:additional_options].nil?
363
490
  cmd << sprintf(' %s', file)
364
491
 
365
- self.run(cmd, opts[:expected_exitcode])
492
+ self.last_puppet_run = self.run(cmd, opts[:expected_exitcode])
366
493
  end
367
494
  end
368
495
 
@@ -375,14 +502,16 @@ class Rouster
375
502
 
376
503
  manifests.each do |m|
377
504
 
378
- cmd = sprintf('puppet apply %s --modulepath=%s', (puppet_version > '3.0') ? "--hiera_config=#{opts[:hiera_config]}" : '', opts[:module_dir])
505
+ cmd = 'puppet apply --detailed-exitcodes'
506
+ cmd << sprintf(' --modulepath=%s', opts[:module_dir]) unless opts[:module_dir].nil?
507
+ cmd << sprintf(' --hiera_config=%s', opts[:hiera_config]) unless opts[:hiera_config].nil? or puppet_version < '3.0'
379
508
  cmd << sprintf(' --environment %s', opts[:environment]) unless opts[:environment].nil?
380
509
  cmd << sprintf(' --certname %s', opts[:certname]) unless opts[:certname].nil?
381
510
  cmd << ' --pluginsync' if opts[:pluginsync]
382
- cmd << opts[:additional_options] unless opts[:additional_options].nil?
511
+ cmd << sprintf(' %s', opts[:additional_options]) unless opts[:additional_options].nil?
383
512
  cmd << sprintf(' %s', m)
384
513
 
385
- self.run(cmd, opts[:expected_exitcode])
514
+ self.last_puppet_run = self.run(cmd, opts[:expected_exitcode])
386
515
  end
387
516
 
388
517
  end
@@ -5,6 +5,103 @@ require 'rouster/deltas'
5
5
 
6
6
  class Rouster
7
7
 
8
+ ##
9
+ # validate_cron
10
+ #
11
+ # given the name of the user who owns crontab, the cron's command to execute and a hash of expectations, returns true|false whether cron matches expectations
12
+ #
13
+ # parameters
14
+ # * <user> - name of user who owns crontab
15
+ # * <name> - the cron's command to execute
16
+ # * <expectations> - hash of expectations, see examples
17
+ # * <fail_fast> - return false immediately on any failure (default is false)
18
+ #
19
+ # example expectations:
20
+ # 'username',
21
+ # '/home/username/test.pl', {
22
+ # :ensure => 'present',
23
+ # :minute => 1,
24
+ # :hour => 0,
25
+ # :dom => '*',
26
+ # :mon => '*',
27
+ # :dow => '*',
28
+ # }
29
+ #
30
+ # 'root',
31
+ # 'printf > /var/log/apache/error_log', {
32
+ # :minute => 59,
33
+ # :hour => [8, 12],
34
+ # :dom => '*',
35
+ # :mon => '*',
36
+ # :dow => '*',
37
+ # }
38
+ #
39
+ # supported keys:
40
+ # * :exists|:ensure -- defaults to present if not specified
41
+ # * :minute
42
+ # * :hour
43
+ # * :dom -- day of month
44
+ # * :mon -- month
45
+ # * :dow -- day of week
46
+ # * :constrain
47
+ def validate_cron(user, name, expectations, fail_fast=false)
48
+ if user.nil?
49
+ raise InternalError.new('no user specified constraint')
50
+ end
51
+
52
+ crontabs = self.get_crontab(user)
53
+
54
+ if expectations[:ensure].nil? and expectations[:exists].nil?
55
+ expectations[:ensure] = 'present'
56
+ end
57
+
58
+ if expectations.has_key?(:constrain)
59
+ expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]
60
+
61
+ expectations[:constrain].each do |constraint|
62
+ fact, expectation = constraint.split("\s")
63
+ unless meets_constraint?(fact, expectation)
64
+ @logger.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
65
+ return true
66
+ end
67
+ end
68
+
69
+ expectations.delete(:constrain)
70
+ end
71
+
72
+ results = Hash.new()
73
+ local = nil
74
+
75
+ expectations.each do |k,v|
76
+ case k
77
+ when :ensure, :exists
78
+ if crontabs.has_key?(name)
79
+ if v.to_s.match(/absent|false/).nil?
80
+ local = true
81
+ else
82
+ local = false
83
+ end
84
+ else
85
+ local = v.to_s.match(/absent|false/).nil? ? false : true
86
+ end
87
+ when :minute, :hour, :dom, :mon, :dow
88
+ if crontabs.has_key?(name) and crontabs[name].has_key?(k) and crontabs[name][k].to_s.eql?(v.to_s)
89
+ local = true
90
+ else
91
+ local = false
92
+ end
93
+ else
94
+ raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
95
+ end
96
+
97
+ return false if local.eql?(false) and fail_fast.eql?(true)
98
+ results[k] = local
99
+ end
100
+
101
+ @logger.info("#{name} [#{expectations}] => #{results}")
102
+ results.find{|k,v| v.false? }.nil?
103
+ end
104
+
8
105
  ##
9
106
  # validate_file
10
107
  #
@@ -57,9 +154,17 @@ class Rouster
57
154
  expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]
58
155
 
59
156
  expectations[:constrain].each do |constraint|
60
- fact, expectation = constraint.split("\s")
157
+ valid = constraint.match(/^(\S+?)\s(.*)$/)
158
+
159
+ if valid.nil?
160
+ raise InternalError.new(sprintf('invalid constraint[%s] specified', constraint))
161
+ end
162
+
163
+ fact = $1
164
+ expectation = $2
165
+
61
166
  unless meets_constraint?(fact, expectation)
62
- @log.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
167
+ @logger.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
63
168
  return true
64
169
  end
65
170
  end
@@ -79,6 +184,14 @@ class Rouster
79
184
  local = true
80
185
  elsif properties.nil?
81
186
  local = false
187
+ elsif v.to_s.match(/symlink|link/)
188
+ if expectations[:target].nil?
189
+ # don't validate the link path, just check whether we're a link
190
+ local = properties[:symlink?]
191
+ else
192
+ # validate the link path
193
+ local = properties[:target].eql?(expectations[:target])
194
+ end
82
195
  else
83
196
  case v
84
197
  when 'dir', 'directory'
@@ -121,10 +234,22 @@ class Rouster
121
234
  local = true
122
235
  begin
123
236
  self.run(sprintf("grep -c '%s' %s", regex, name))
124
- rescue
237
+ rescue => e
125
238
  local = false
126
239
  end
127
- next if local.false?
240
+ break if local.false?
241
+ end
242
+ when :notcontains, :doesntcontain # TODO determine the appropriate attribute title here
243
+ v = v.class.eql?(Array) ? v : [v]
244
+ v.each do |regex|
245
+ local = true
246
+ begin
247
+ self.run(sprintf("grep -c '%s' %s", regex, name))
248
+ local = false
249
+ rescue => e
250
+ local = true
251
+ end
252
+ break if local.false?
128
253
  end
129
254
  when :mode, :permissions
130
255
  if properties.nil?
@@ -158,6 +283,8 @@ class Rouster
158
283
  end
159
284
  when :type
160
285
  # noop allowing parse_catalog() output to be passed directly
286
+ when :target
287
+ # noop allowing ensure => 'link' / 'symlink' to specify their .. target
161
288
  else
162
289
  raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
163
290
  end
@@ -166,9 +293,8 @@ class Rouster
166
293
  results[k] = local
167
294
  end
168
295
 
169
- @log.info(results)
296
+ @logger.info("#{name} [#{expectations}] => #{results}")
170
297
  results.find{|k,v| v.false? }.nil?
171
-
172
298
  end
173
299
 
174
300
  ##
@@ -214,7 +340,7 @@ class Rouster
214
340
  expectations[:constrain].each do |constraint|
215
341
  fact, expectation = constraint.split("\s")
216
342
  unless meets_constraint?(fact, expectation)
217
- @log.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
343
+ @logger.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
218
344
  return true
219
345
  end
220
346
  end
@@ -263,7 +389,7 @@ class Rouster
263
389
  results[k] = local
264
390
  end
265
391
 
266
- @log.info(results)
392
+ @logger.info("#{name} [#{expectations}] => #{results}")
267
393
  results.find{|k,v| v.false? }.nil?
268
394
  end
269
395
 
@@ -309,7 +435,7 @@ class Rouster
309
435
  expectations[:constrain].each do |constraint|
310
436
  fact, expectation = constraint.split("\s")
311
437
  unless meets_constraint?(fact, expectation)
312
- @log.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
438
+ @logger.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
313
439
  return true
314
440
  end
315
441
  end
@@ -333,17 +459,39 @@ class Rouster
333
459
  local = v.to_s.match(/absent|false/).nil? ? false : true
334
460
  end
335
461
  when :version
462
+ # TODO support determination based on multiple versions of the same package installed (?)
336
463
  if packages.has_key?(name)
337
- if v.split("\s").size > 1
338
- ## generic comparator functionality
339
- comp, expectation = v.split("\s")
340
- local = generic_comparator(packages[name], comp, expectation)
341
- else
342
- local = ! v.to_s.match(/#{packages[name]}/).nil?
464
+
465
+ lps = packages[name].is_a?(Array) ? packages[name] : [ packages[name] ]
466
+
467
+ lps.each do |lp|
468
+ if v.split("\s").size > 1
469
+ ## generic comparator functionality
470
+ comp, expectation = v.split("\s")
471
+ local = generic_comparator(lp[:version], comp, expectation)
472
+ break unless local.eql?(true)
473
+ else
474
+ local = ! v.to_s.match(/#{lp[:version]}/).nil?
475
+ break unless local.eql?(true)
476
+ end
343
477
  end
344
478
  else
345
479
  local = false
346
480
  end
481
+ when :arch, :architecture
482
+ if packages.has_key?(name)
483
+ archs = []
484
+ lps = packages[name].is_a?(Array) ? packages[name] : [ packages[name] ]
485
+ lps.each { |p| archs << p[:arch] }
486
+ if v.is_a?(Array)
487
+ v.each do |arch|
488
+ local = archs.member?(arch)
489
+ break unless local.eql?(true) # fail fast - if we are looking for an arch that DNE, bail out
490
+ end
491
+ else
492
+ local = archs.member?(v)
493
+ end
494
+ end
347
495
  when :type
348
496
  # noop allowing parse_catalog() output to be passed directly
349
497
  else
@@ -355,8 +503,8 @@ class Rouster
355
503
  end
356
504
 
357
505
  # TODO figure out a good way to allow access to the entire hash, not just boolean -- for now just print at an info level
358
- @log.info(results)
359
506
 
507
+ @logger.info("#{name} [#{expectations}] => #{results}")
360
508
  results.find{|k,v| v.false? }.nil?
361
509
  end
362
510
 
@@ -404,7 +552,7 @@ class Rouster
404
552
  expectations[:constrain].each do |constraint|
405
553
  fact, expectation = constraint.split("\s")
406
554
  unless meets_constraint?(fact, expectation)
407
- @log.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
555
+ @logger.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
408
556
  return true
409
557
  end
410
558
  end
@@ -433,13 +581,17 @@ class Rouster
433
581
 
434
582
  when :address
435
583
  lr = Array.new
436
- addresses = ports[expectations[:protocol]][number][:address]
437
- addresses.each_key do |address|
438
- lr.push(address.eql?(v.to_s))
439
- end
440
-
441
- local = ! lr.find{|e| e.true? }.nil? # this feels jankity
584
+ if ports[expectations[:protocol]][number]
585
+ addresses = ports[expectations[:protocol]][number][:address]
586
+ addresses.each_key do |address|
587
+ lr.push(address.eql?(v.to_s))
588
+ end
442
589
 
590
+ local = ! lr.find{|e| e.true? }.nil? # this feels jankity
591
+ else
592
+ # this port isn't open in the first place, won't match any addresses we expect to see it on
593
+ local = false
594
+ end
443
595
  else
444
596
  raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
445
597
  end
@@ -448,8 +600,7 @@ class Rouster
448
600
  results[k] = local
449
601
  end
450
602
 
451
- @log.info(results)
452
-
603
+ @logger.info("#{name} [#{expectations}] => #{results}")
453
604
  results.find{|k,v| v.false? }.nil?
454
605
  end
455
606
 
@@ -490,7 +641,7 @@ class Rouster
490
641
  expectations[:constrain].each do |constraint|
491
642
  fact, expectation = constraint.split("\s")
492
643
  unless meets_constraint?(fact, expectation)
493
- @log.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
644
+ @logger.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
494
645
  return true
495
646
  end
496
647
  end
@@ -529,9 +680,8 @@ class Rouster
529
680
  results[k] = local
530
681
  end
531
682
 
532
- @log.info(results)
683
+ @logger.info("#{name} [#{expectations}] => #{results}")
533
684
  results.find{|k,v| v.false? }.nil?
534
-
535
685
  end
536
686
 
537
687
  ##
@@ -581,7 +731,7 @@ class Rouster
581
731
  expectations[:constrain].each do |constraint|
582
732
  fact, expectation = constraint.split("\s")
583
733
  unless meets_constraint?(fact, expectation)
584
- @log.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
734
+ @logger.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
585
735
  return true
586
736
  end
587
737
  end
@@ -650,9 +800,8 @@ class Rouster
650
800
  results[k] = local
651
801
  end
652
802
 
653
- @log.info(results)
803
+ @logger.info("#{name} [#{expectations}] => #{results}")
654
804
  results.find{|k,v| v.false? }.nil?
655
-
656
805
  end
657
806
 
658
807
  ## internal methods
@@ -665,32 +814,41 @@ class Rouster
665
814
  # gets facts from node, and if fact expectation regex matches actual fact, returns true
666
815
  #
667
816
  # parameters
668
- # * <fact> - fact
669
- # * <expectation>
670
- # * [cache]
671
- def meets_constraint?(fact, expectation, cache=true)
672
-
673
- unless self.respond_to?('facter')
674
- # if we haven't loaded puppet.rb, we won't have access to facts
675
- @log.warn('using constraints without loading [rouster/puppet] will not work, forcing no-op')
817
+ # * <key> - fact/hiera key to look up (actual value)
818
+ # * <expectation> -
819
+ # * [cache] - boolean controlling whether facter lookups are cached
820
+ def meets_constraint?(key, expectation, cache=true)
821
+
822
+ expectation = expectation.to_s
823
+
824
+ unless self.respond_to?('facter') or self.respond_to?('hiera')
825
+ # if we haven't loaded puppet.rb, we won't have access to facts/hiera lookups
826
+ @logger.warn('using constraints without loading [rouster/puppet] will not work, forcing no-op')
676
827
  return false
677
828
  end
678
829
 
679
- expectation = expectation.to_s
680
- facts = self.facter(cache)
830
+ facts = self.facter(cache)
831
+ actual = nil
832
+
833
+ if facts[key]
834
+ actual = facts[key]
835
+ else
836
+ # value is not a fact, lets try to find it in hiera
837
+ # TODO how to handle the fact that this will really only work on the puppetmaster
838
+ actual = self.hiera(key, facts)
839
+ end
681
840
 
682
841
  res = nil
842
+
683
843
  if expectation.split("\s").size > 1
684
844
  ## generic comparator functionality
685
845
  comp, expectation = expectation.split("\s")
686
-
687
- res = generic_comparator(facts[fact], comp, expectation)
688
-
846
+ res = generic_comparator(actual, comp, expectation)
689
847
  else
690
- res = ! expectation.match(/#{facts[fact]}/).nil?
691
- @log.debug(sprintf('meets_constraint?(%s, %s): %s', fact, expectation, res.nil?))
848
+ res = ! actual.to_s.match(/#{expectation}/).nil?
692
849
  end
693
850
 
851
+ @logger.debug(sprintf('meets_constraint?(%s, %s): %s', key, expectation, res.nil?))
694
852
  res
695
853
  end
696
854
 
@@ -708,6 +866,7 @@ class Rouster
708
866
  def generic_comparator(comparand1, comparator, comparand2)
709
867
 
710
868
  # TODO rewrite this as an eval so we don't have to support everything..
869
+ # TODO come up with mechanism to determine when is it appropriate to call .to_i vs. otherwise -- comparisons will mainly be numerical (?), but need to support text matching too
711
870
  case comparator
712
871
  when '!='
713
872
  # ugh