rouster 0.5

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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/LICENSE +9 -0
  4. data/README.md +175 -0
  5. data/Rakefile +65 -0
  6. data/Vagrantfile +23 -0
  7. data/examples/bootstrap.rb +113 -0
  8. data/examples/demo.rb +71 -0
  9. data/examples/error.rb +30 -0
  10. data/lib/rouster.rb +737 -0
  11. data/lib/rouster/deltas.rb +481 -0
  12. data/lib/rouster/puppet.rb +398 -0
  13. data/lib/rouster/testing.rb +743 -0
  14. data/lib/rouster/tests.rb +596 -0
  15. data/path_helper.rb +21 -0
  16. data/rouster.gemspec +30 -0
  17. data/test/basic.rb +10 -0
  18. data/test/functional/deltas/test_get_crontab.rb +99 -0
  19. data/test/functional/deltas/test_get_groups.rb +48 -0
  20. data/test/functional/deltas/test_get_packages.rb +71 -0
  21. data/test/functional/deltas/test_get_ports.rb +119 -0
  22. data/test/functional/deltas/test_get_services.rb +43 -0
  23. data/test/functional/deltas/test_get_users.rb +45 -0
  24. data/test/functional/puppet/test_facter.rb +59 -0
  25. data/test/functional/test_caching.rb +124 -0
  26. data/test/functional/test_destroy.rb +51 -0
  27. data/test/functional/test_dirs.rb +88 -0
  28. data/test/functional/test_files.rb +64 -0
  29. data/test/functional/test_get.rb +76 -0
  30. data/test/functional/test_inspect.rb +31 -0
  31. data/test/functional/test_is_dir.rb +118 -0
  32. data/test/functional/test_is_file.rb +119 -0
  33. data/test/functional/test_new.rb +92 -0
  34. data/test/functional/test_put.rb +81 -0
  35. data/test/functional/test_rebuild.rb +49 -0
  36. data/test/functional/test_restart.rb +44 -0
  37. data/test/functional/test_run.rb +77 -0
  38. data/test/functional/test_status.rb +38 -0
  39. data/test/functional/test_suspend.rb +31 -0
  40. data/test/functional/test_up.rb +27 -0
  41. data/test/functional/test_validate_file.rb +30 -0
  42. data/test/puppet/manifests/default.pp +9 -0
  43. data/test/puppet/manifests/hiera.yaml +12 -0
  44. data/test/puppet/manifests/hieradata/common.json +3 -0
  45. data/test/puppet/manifests/hieradata/vagrant.json +3 -0
  46. data/test/puppet/manifests/manifest.pp +78 -0
  47. data/test/puppet/modules/role/manifests/ui.pp +5 -0
  48. data/test/puppet/test_apply.rb +149 -0
  49. data/test/puppet/test_roles.rb +186 -0
  50. data/test/tunnel_vs_scp.rb +41 -0
  51. data/test/unit/puppet/test_get_puppet_star.rb +68 -0
  52. data/test/unit/test_generate_unique_mac.rb +43 -0
  53. data/test/unit/test_new.rb +31 -0
  54. data/test/unit/test_parse_ls_string.rb +334 -0
  55. data/test/unit/test_traverse_up.rb +43 -0
  56. data/test/unit/testing/test_meets_constraint.rb +55 -0
  57. data/test/unit/testing/test_validate_file.rb +112 -0
  58. data/test/unit/testing/test_validate_group.rb +72 -0
  59. data/test/unit/testing/test_validate_package.rb +69 -0
  60. data/test/unit/testing/test_validate_port.rb +98 -0
  61. data/test/unit/testing/test_validate_service.rb +73 -0
  62. data/test/unit/testing/test_validate_user.rb +92 -0
  63. metadata +203 -0
@@ -0,0 +1,398 @@
1
+ require sprintf('%s/../../%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
2
+
3
+ require 'json'
4
+ require 'net/https'
5
+ require 'socket'
6
+ require 'uri'
7
+
8
+ # TODO use @cache_timeout to invalidate data cached here
9
+
10
+ class Rouster
11
+
12
+ ##
13
+ # facter
14
+ #
15
+ # runs facter, returns parsed hash of { fact1 => value1, factN => valueN }
16
+ #
17
+ # parameters
18
+ # * [cache] - whether to store/return cached facter data, if available
19
+ # * [custom_facts] - whether to include custom facts in return (uses -p argument)
20
+ def facter(cache=true, custom_facts=true)
21
+ if cache.true? and ! self.facts.nil?
22
+ return self.facts
23
+ end
24
+
25
+ raw = self.run(sprintf('facter %s', custom_facts.true? ? '-p' : ''))
26
+ res = Hash.new()
27
+
28
+ raw.split("\n").each do |line|
29
+ next unless line.match(/(\S*?)\s\=\>\s(.*)/)
30
+ res[$1] = $2
31
+ end
32
+
33
+ if cache.true?
34
+ self.facts = res
35
+ end
36
+
37
+ res
38
+ end
39
+
40
+ ##
41
+ # get_catalog
42
+ #
43
+ # not completely implemented method to get a compiled catalog about a node (based on its facts) from a puppetmaster
44
+ #
45
+ # original implementation used the catalog face, which does not actually work. switched to an API call, but still need to convert facts into PSON
46
+ #
47
+ # parameters
48
+ # * [hostname] - hostname of node to return catalog for, if not specified, will use `hostname --fqdn`
49
+ # * [puppetmaster] - hostname of puppetmaster to use in API call, defaults to 'puppet'
50
+ # * [facts] - hash of facts to pass to puppetmaster
51
+ # * [puppetmaster_port] - port to talk to the puppetmaster on, defaults to 8140
52
+ def get_catalog(hostname=nil, puppetmaster=nil, facts=nil, puppetmaster_port=8140)
53
+ # post https://<puppetmaster>/catalog/<node>?facts_format=pson&facts=<pson URL encoded> == ht to patrick@puppetlabs
54
+ certname = hostname.nil? ? self.run('hostname --fqdn').chomp : hostname
55
+ puppetmaster = puppetmaster.nil? ? 'puppet' : puppetmaster
56
+ facts = facts.nil? ? self.facter() : facts
57
+
58
+ %w(fqdn hostname operatingsystem operatingsystemrelease osfamily rubyversion).each do |required|
59
+ raise ArgumentError.new(sprintf('missing required fact[%s]', required)) unless facts.has_key?(required)
60
+ end
61
+
62
+ raise InternalError.new('need to finish conversion of facts to PSON')
63
+ facts.to_pson # this does not work, but needs to
64
+
65
+ json = nil
66
+ url = sprintf('https://%s:%s/catalog/%s?facts_format=pson&facts=%s', puppetmaster, puppetmaster_port, certname, facts)
67
+ uri = URI.parse(url)
68
+
69
+ begin
70
+ res = Net::HTTP.get(uri)
71
+ json = res.to_json
72
+ rescue => e
73
+ raise ExternalError.new("calling[#{url}] led to exception[#{e}")
74
+ end
75
+
76
+ json
77
+ end
78
+
79
+ ##
80
+ # get_puppet_errors
81
+ #
82
+ # parses input for puppet errors, returns array of strings
83
+ #
84
+ # parameters
85
+ # * [input] - string to look at, defaults to self.get_output()
86
+ def get_puppet_errors(input=nil)
87
+ str = input.nil? ? self.get_output() : input
88
+ errors = str.scan(/35merr:.*/)
89
+
90
+ errors.empty? ? nil : errors
91
+ end
92
+
93
+ ##
94
+ # get_puppet_notices
95
+ #
96
+ # parses input for puppet notices, returns array of strings
97
+ #
98
+ # parameters
99
+ # * [input] - string to look at, defaults to self.get_output()
100
+ def get_puppet_notices(input=nil)
101
+ str = input.nil? ? self.get_output() : input
102
+ notices = str.scan(/36mnotice:.*/)
103
+
104
+ notices.empty? ? nil : notices
105
+ end
106
+
107
+ ##
108
+ # get_puppet_version
109
+ #
110
+ # executes `puppet --version` and returns parsed version string or nil
111
+ def get_puppet_version
112
+ version = nil
113
+ installed = self.is_in_path?('puppet')
114
+
115
+ if installed
116
+ raw = self.run('puppet --version')
117
+ version = raw.match(/([\d\.]*)\s/) ? $1 : nil
118
+ else
119
+ version = nil
120
+ end
121
+
122
+ version
123
+ end
124
+
125
+ ##
126
+ # hiera
127
+ #
128
+ # returns hiera results from self
129
+ #
130
+ # 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)
134
+
135
+ cmd = 'hiera'
136
+ cmd << sprintf(' -c %s', config) unless config.nil?
137
+ cmd << sprintf(' %s', options) unless options.nil?
138
+ cmd << sprintf(' %s', key)
139
+
140
+ self.run(cmd)
141
+ end
142
+
143
+ ##
144
+ # parse_catalog
145
+ #
146
+ # looks at the ['data']['resources'] keys in catalog for Files, Groups, Packages, Services and Users, returns hash of expectations compatible with validate_*
147
+ #
148
+ # this is a very lightly tested implementation, please open issues as necessary
149
+ #
150
+ # parameters
151
+ # * <catalog> - JSON string or Hash representation of catalog, typically from get_catalog()
152
+ def parse_catalog(catalog)
153
+ classes = nil
154
+ resources = nil
155
+ results = Hash.new()
156
+
157
+ if catalog.is_a?(String)
158
+ begin
159
+ JSON.parse!(catalog)
160
+ rescue
161
+ raise InternalError.new(sprintf('unable to parse catalog[%s] as JSON', catalog))
162
+ end
163
+ end
164
+
165
+ unless catalog.has_key?('data') and catalog['data'].has_key?('classes')
166
+ raise InternalError.new(sprintf('catalog does not contain a classes key[%s]', catalog))
167
+ end
168
+
169
+ unless catalog.has_key?('data') and catalog['data'].has_key?('resources')
170
+ raise InternalError.new(sprintf('catalog does not contain a resources key[%s]', catalog))
171
+ end
172
+
173
+ raw_resources = catalog['data']['resources']
174
+
175
+ raw_resources.each do |r|
176
+ # samples of eacb type of resource is available at
177
+ # https://github.com/chorankates/rouster/issues/20#issuecomment-18635576
178
+ #
179
+ # we can do a lot better here
180
+ type = r['type']
181
+ case type
182
+ when 'Class'
183
+ classes.push(r['title'])
184
+ when 'File'
185
+ name = r['title']
186
+ resources[name] = Hash.new()
187
+
188
+ resources[name][:type] = :file
189
+ resources[name][:directory] = false
190
+ resources[name][:ensure] = r['ensure'] ||= 'present'
191
+ resources[name][:file] = true
192
+ resources[name][:group] = r['parameters'].has_key?('group') ? r['parameters']['group'] : nil
193
+ resources[name][:mode] = r['parameters'].has_key?('mode') ? r['parameters']['mode'] : nil
194
+ resources[name][:owner] = r['parameters'].has_key?('owner') ? r['parameters']['owner'] : nil
195
+ resources[name][:contains] = r.has_key?('content') ? r['content'] : nil
196
+
197
+ when 'Group'
198
+ name = r['title']
199
+ resources[name] = Hash.new()
200
+
201
+ resources[name][:type] = :group
202
+ resources[name][:ensure] = r['ensure'] ||= 'present'
203
+ resources[name][:gid] = r['parameters'].has_key?('gid') ? r['parameters']['gid'] : nil
204
+
205
+ when 'Package'
206
+ name = r['title']
207
+ resources[name] = Hash.new()
208
+
209
+ resources[name][:type] = :package
210
+ resources[name][:ensure] = r['ensure'] ||= 'present'
211
+ resources[name][:version] = r['ensure'] =~ /\d/ ? r['ensure'] : nil
212
+
213
+ when 'Service'
214
+ name = r['title']
215
+ resources[name] = Hash.new()
216
+
217
+ resources[name][:type] = :service
218
+ resources[name][:ensure] = r['ensure'] ||= 'present'
219
+ resources[name][:state] = r['ensure']
220
+
221
+ when 'User'
222
+ name = r['title']
223
+ resources[name] = Hash.new()
224
+
225
+ resources[name][:type] = :user
226
+ resources[name][:ensure] = r['ensure'] ||= 'present'
227
+ resources[name][:home] = r['parameters'].has_key?('home') ? r['parameters']['home'] : nil
228
+ resources[name][:gid] = r['parameters'].has_key?('gid') ? r['parameters']['gid'] : nil
229
+ resources[name][:group] = r['parameters'].has_key?('groups') ? r['parameters']['groups'] : nil
230
+ resources[name][:shell] = r['parameters'].has_key?('shell') ? r['parameters']['shell'] : nil
231
+ resources[name][:uid] = r['parameters'].has_key?('uid') ? r['parameters']['uid'] : nil
232
+
233
+ else
234
+ raise NotImplementedError.new(sprintf('parsing support for [%s] is incomplete', type))
235
+ end
236
+
237
+ end
238
+
239
+ # remove all nil references
240
+ resources.each_key do |name|
241
+ resources[name].each_pair do |k,v|
242
+ unless v
243
+ resources[name].delete(k)
244
+ end
245
+ end
246
+ end
247
+
248
+
249
+ results[:classes] = classes
250
+ results[:resources] = resources
251
+
252
+ results
253
+ end
254
+
255
+ ##
256
+ # remove_existing_certs
257
+ #
258
+ # ... removes existing certificates - really only useful when called on a puppetmaster
259
+ # useful in testing environments where you want to destroy/rebuild agents without rebuilding the puppetmaster every time (think autosign)
260
+ #
261
+ # parameters
262
+ # * <puppetmaster> - string/partial regex of certificate names to keep
263
+ def remove_existing_certs (puppetmaster)
264
+ hosts = Array.new()
265
+
266
+ res = self.run('puppet cert list --all')
267
+
268
+ res.each_line do |line|
269
+ next if line.match(/#{puppetmaster}/)
270
+ host = $1 if line.match(/^\+\s"(.*?)"/)
271
+
272
+ hosts.push(host) unless host.nil? # only want to clear signed certs
273
+ end
274
+
275
+ hosts.each do |host|
276
+ self.run(sprintf('puppet cert --clean %s', host))
277
+ end
278
+
279
+ end
280
+
281
+ ##
282
+ # run_puppet
283
+ #
284
+ # ... runs puppet on self, returns nothing
285
+ #
286
+ # currently supports 2 methods of running puppet:
287
+ # * master - runs 'puppet agent -t'
288
+ # * supported options
289
+ # * expected_exitcode - string/integer/array of acceptable exit code(s)
290
+ # * configtimeout - string/integer of the acceptable configtimeout value
291
+ # * environment - string of the environment to use
292
+ # * certname - string of the certname to use in place of the host fqdn
293
+ # * pluginsync - bool value if pluginsync should be used
294
+ # * server - string value of the puppetmasters fqdn / ip
295
+ # * additional_options - string of various options that would be passed to puppet
296
+ # * masterless - runs 'puppet apply <options>' after determining version of puppet running and adjusting arguments
297
+ # * supported options
298
+ # * expected_exitcode - string/integer/array of acceptable exit code(s)
299
+ # * hiera_config - path to hiera configuration -- only supported by Puppet 3.0+
300
+ # * manifest_file - string/array of strings of paths to manifest(s) to apply
301
+ # * manifest_dir - string/array of strings of directories containing manifest(s) to apply - is recursive
302
+ # * module_dir - path to module directory -- currently a required parameter, is this correct?
303
+ # * environment - string of the environment to use (default: production)
304
+ # * certname - string of the certname to use in place of the host fqdn (default: unused)
305
+ # * pluginsync - bool value if pluginsync should be used (default: true)
306
+ # * additional_options - string of various options that would be passed to puppet
307
+ #
308
+ # parameters
309
+ # * [mode] - method to run puppet, defaults to 'master'
310
+ # * [opts] - hash of additional options
311
+ def run_puppet(mode='master', passed_opts=nil)
312
+
313
+ if mode.eql?('master')
314
+ opts = {
315
+ :expected_exitcode => 0,
316
+ :configtimeout => nil,
317
+ :environment => nil,
318
+ :certname => nil,
319
+ :server => nil,
320
+ :pluginsync => false,
321
+ :additional_options => nil
322
+ }.merge!(passed_opts)
323
+
324
+ cmd = 'puppet agent -t'
325
+ cmd << sprintf(' --configtimeout %s', opts[:configtimeout]) unless opts[:configtimeout].nil?
326
+ cmd << sprintf(' --environment %s', opts[:environment]) unless opts[:environment].nil?
327
+ cmd << sprintf(' --certname %s', opts[:certname]) unless opts[:certname].nil?
328
+ cmd << sprintf(' --server %s', opts[:server]) unless opts[:server].nil?
329
+ cmd << ' --pluginsync' if opts[:pluginsync]
330
+ cmd << opts[:additional_options] unless opts[:additional_options].nil?
331
+
332
+ self.run(cmd, opts[:expected_exitcode])
333
+
334
+ elsif mode.eql?('masterless')
335
+ opts = {
336
+ :expected_exitcode => 2,
337
+ :hiera_config => nil,
338
+ :manifest_file => nil, # can be a string or array, will 'puppet apply' each
339
+ :manifest_dir => nil, # can be a string or array, will 'puppet apply' each module in the dir (recursively)
340
+ :module_dir => nil,
341
+ :environment => nil,
342
+ :certname => nil,
343
+ :pluginsync => false,
344
+ :additional_options => nil
345
+ }.merge!(passed_opts)
346
+
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])
350
+
351
+ puppet_version = self.get_puppet_version() # hiera_config specification is only supported in >3.0
352
+
353
+ if opts[:manifest_file]
354
+ opts[:manifest_file] = opts[:manifest_file].class.eql?(Array) ? opts[:manifest_file] : [opts[:manifest_file]]
355
+ opts[:manifest_file].each do |file|
356
+ raise InternalError.new(sprintf('invalid manifest file specified[%s]', file)) unless self.is_file?(file)
357
+
358
+ cmd = sprintf('puppet apply %s --modulepath=%s', (puppet_version > '3.0') ? "--hiera_config=#{opts[:hiera_config]}" : '', opts[:module_dir])
359
+ cmd << sprintf(' --environment %s', opts[:environment]) unless opts[:environment].nil?
360
+ cmd << sprintf(' --certname %s', opts[:certname]) unless opts[:certname].nil?
361
+ cmd << ' --pluginsync' if opts[:pluginsync]
362
+ cmd << opts[:additional_options] unless opts[:additional_options].nil?
363
+ cmd << sprintf(' %s', file)
364
+
365
+ self.run(cmd, opts[:expected_exitcode])
366
+ end
367
+ end
368
+
369
+ if opts[:manifest_dir]
370
+ opts[:manifest_dir] = opts[:manifest_dir].class.eql?(Array) ? opts[:manifest_dir] : [opts[:manifest_dir]]
371
+ opts[:manifest_dir].each do |dir|
372
+ raise InternalError.new(sprintf('invalid manifest dir specified[%s]', dir)) unless self.is_dir?(dir)
373
+
374
+ manifests = self.files(dir, '*.pp', true)
375
+
376
+ manifests.each do |m|
377
+
378
+ cmd = sprintf('puppet apply %s --modulepath=%s', (puppet_version > '3.0') ? "--hiera_config=#{opts[:hiera_config]}" : '', opts[:module_dir])
379
+ cmd << sprintf(' --environment %s', opts[:environment]) unless opts[:environment].nil?
380
+ cmd << sprintf(' --certname %s', opts[:certname]) unless opts[:certname].nil?
381
+ cmd << ' --pluginsync' if opts[:pluginsync]
382
+ cmd << opts[:additional_options] unless opts[:additional_options].nil?
383
+ cmd << sprintf(' %s', m)
384
+
385
+ self.run(cmd, opts[:expected_exitcode])
386
+ end
387
+
388
+ end
389
+ end
390
+
391
+ else
392
+ raise InternalError.new(sprintf('unknown mode [%s]', mode))
393
+ end
394
+
395
+
396
+ end
397
+
398
+ end
@@ -0,0 +1,743 @@
1
+ require sprintf('%s/../../%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
2
+ require 'rouster/deltas'
3
+
4
+ # TODO better document keys :constrain and :version
5
+
6
+ class Rouster
7
+
8
+ ##
9
+ # validate_file
10
+ #
11
+ # given a filename and a hash of expectations, returns true|false whether file matches expectations
12
+ #
13
+ # parameters
14
+ # * <name> - full file name or relative (to ~vagrant)
15
+ # * <expectations> - hash of expectations, see examples
16
+ # * <fail_fast> - return false immediately on any failure (default is false)
17
+ #
18
+ # example expectations:
19
+ # '/sys/kernel/mm/redhat_transparent_hugepage/enabled', {
20
+ # :contains => 'never',
21
+ # },
22
+ #
23
+ # '/etc/fstab', {
24
+ # :contains => '/dev/fioa*/iodata*xfs',
25
+ # :constrain => 'is_virtual false' # syntax is '<fact> <expected>', file is only tested if <expected> matches <actual>
26
+ # :exists => 'file',
27
+ # :mode => '0644'
28
+ # },
29
+ #
30
+ # '/etc/hosts', {
31
+ # :constrain => ['! is_virtual true', 'is_virtual false'],
32
+ # :mode => '0644'
33
+ # }
34
+ #
35
+ # '/etc/nrpe.cfg', {
36
+ # :ensure => 'file',
37
+ # :contains => ['dont_blame_nrpe=1', 'allowed_hosts=' ]
38
+ # }
39
+ #
40
+ # supported keys:
41
+ # * :exists|:ensure -- defaults to file if not specified
42
+ # * :file
43
+ # * :directory
44
+ # * :contains (string or array)
45
+ # * :mode/:permissions
46
+ # * :size
47
+ # * :owner
48
+ # * :group
49
+ # * :constrain
50
+ def validate_file(name, expectations, fail_fast=false, cache=false)
51
+
52
+ if expectations[:ensure].nil? and expectations[:exists].nil? and expectations[:directory].nil? and expectations[:file?].nil?
53
+ expectations[:ensure] = 'file'
54
+ end
55
+
56
+ if expectations.has_key?(:constrain)
57
+ expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]
58
+
59
+ expectations[:constrain].each do |constraint|
60
+ fact, expectation = constraint.split("\s")
61
+ unless meets_constraint?(fact, expectation)
62
+ @log.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
63
+ return true
64
+ end
65
+ end
66
+
67
+ expectations.delete(:constrain)
68
+ end
69
+
70
+ properties = (expectations[:ensure].eql?('file')) ? self.file(name, cache) : self.dir(name, cache)
71
+ results = Hash.new()
72
+ local = nil
73
+
74
+ expectations.each do |k,v|
75
+
76
+ case k
77
+ when :ensure, :exists
78
+ if properties.nil? and v.to_s.match(/absent|false/)
79
+ local = true
80
+ elsif properties.nil?
81
+ local = false
82
+ else
83
+ case v
84
+ when 'dir', 'directory'
85
+ local = properties[:directory?]
86
+ else
87
+ local = properties[:file?]
88
+ end
89
+ end
90
+ when :file
91
+ if properties.nil?
92
+ if v.to_s.match(/absent|false/)
93
+ local = true
94
+ else
95
+ local = false
96
+ end
97
+ elsif properties[:file?].true?
98
+ local = ! v.to_s.match(/absent|false/)
99
+ else
100
+ false
101
+ end
102
+ when :dir, :directory
103
+ if properties.nil?
104
+ if v.to_s.match(/absent|false/)
105
+ local = true
106
+ else
107
+ local = false
108
+ end
109
+ elsif properties.has_key?(:directory?)
110
+ if properties[:directory?]
111
+ local = v.to_s.match(/absent|false/).nil?
112
+ else
113
+ local = ! v.to_s.match(/absent|false/).nil?
114
+ end
115
+ else
116
+ local = false
117
+ end
118
+ when :contains
119
+ v = v.class.eql?(Array) ? v : [v]
120
+ v.each do |regex|
121
+ local = true
122
+ begin
123
+ self.run(sprintf("grep -c '%s' %s", regex, name))
124
+ rescue
125
+ local = false
126
+ end
127
+ next if local.false?
128
+ end
129
+ when :mode, :permissions
130
+ if properties.nil?
131
+ local = false
132
+ elsif v.to_s.match(/#{properties[:mode].to_s}/)
133
+ local = true
134
+ else
135
+ local = false
136
+ end
137
+ when :size
138
+ if properties.nil?
139
+ local = false
140
+ else
141
+ local = v.to_i.eql?(properties[:size].to_i)
142
+ end
143
+ when :owner
144
+ if properties.nil?
145
+ local = false
146
+ elsif v.to_s.match(/#{properties[:owner].to_s}/)
147
+ local = true
148
+ else
149
+ local = false
150
+ end
151
+ when :group
152
+ if properties.nil?
153
+ local = false
154
+ elsif v.match(/#{properties[:group]}/)
155
+ local = true
156
+ else
157
+ local = false
158
+ end
159
+ when :type
160
+ # noop allowing parse_catalog() output to be passed directly
161
+ else
162
+ raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
163
+ end
164
+
165
+ return false if local.eql?(false) and fail_fast.eql?(true)
166
+ results[k] = local
167
+ end
168
+
169
+ @log.info(results)
170
+ results.find{|k,v| v.false? }.nil?
171
+
172
+ end
173
+
174
+ ##
175
+ # validate_group
176
+ #
177
+ # given a group and a hash of expectations, returns true|false whether group matches expectations
178
+ #
179
+ # paramaters
180
+ # * <name> - group name
181
+ # * <expectations> - hash of expectations, see examples
182
+ # * <fail_fast> - return false immediately on any failure (default is false)
183
+ #
184
+ # example expectations:
185
+ # 'root', {
186
+ # # if ensure is not specified, 'present' is implied
187
+ # :gid => 0,
188
+ # :user => 'root'
189
+ # }
190
+ # 'sys', {
191
+ # :ensure => 'present',
192
+ # :user => ['root', 'bin', 'daemon']
193
+ # },
194
+ #
195
+ # 'fizz', {
196
+ # :exists => false
197
+ # },
198
+ #
199
+ # supported keys:
200
+ # * :exists|:ensure
201
+ # * :gid
202
+ # * :user|:users (string or array)
203
+ # * :constrain
204
+ def validate_group(name, expectations, fail_fast=false)
205
+ groups = self.get_groups(true)
206
+
207
+ if expectations[:ensure].nil? and expectations[:exists].nil?
208
+ expectations[:ensure] = 'present'
209
+ end
210
+
211
+ if expectations.has_key?(:constrain)
212
+ expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]
213
+
214
+ expectations[:constrain].each do |constraint|
215
+ fact, expectation = constraint.split("\s")
216
+ unless meets_constraint?(fact, expectation)
217
+ @log.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
218
+ return true
219
+ end
220
+ end
221
+
222
+ expectations.delete(:constrain)
223
+ end
224
+
225
+ results = Hash.new()
226
+ local = nil
227
+
228
+ expectations.each do |k,v|
229
+ case k
230
+ when :ensure, :exists
231
+ if groups.has_key?(name)
232
+ if v.to_s.match(/absent|false/).nil?
233
+ local = true
234
+ else
235
+ local = false
236
+ end
237
+ else
238
+ local = v.to_s.match(/absent|false/).nil? ? false : true
239
+ end
240
+ when :gid
241
+ if groups[name].is_a?(Hash) and groups[name].has_key?(:gid)
242
+ local = v.to_s.eql?(groups[name][:gid].to_s)
243
+ else
244
+ local = false
245
+ end
246
+ when :user, :users
247
+ v = v.class.eql?(Array) ? v : [v]
248
+ v.each do |user|
249
+ if groups[name].is_a?(Hash) and groups[name].has_key?(:users)
250
+ local = groups[name][:users].member?(user)
251
+ else
252
+ local = false
253
+ end
254
+ break unless local.true? # need to make the return value smarter if we want to store data on which user failed
255
+ end
256
+ when :type
257
+ # noop allowing parse_catalog() output to be passed directly
258
+ else
259
+ raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
260
+ end
261
+
262
+ return false if local.eql?(false) and fail_fast.eql?(true)
263
+ results[k] = local
264
+ end
265
+
266
+ @log.info(results)
267
+ results.find{|k,v| v.false? }.nil?
268
+ end
269
+
270
+ ##
271
+ # validate_package
272
+ #
273
+ # given a package name and a hash of expectations, returns true|false whether package meets expectations
274
+ #
275
+ # parameters
276
+ # * <name> - package name
277
+ # * <expectations> - hash of expectations, see examples
278
+ # * <fail_fast> - return false immediately on any failure (default is false)
279
+ #
280
+ # example expectations:
281
+ # 'perl-Net-SNMP', {
282
+ # :ensure => 'absent'
283
+ # },
284
+ #
285
+ # 'pixman', {
286
+ # :ensure => 'present',
287
+ # :version => '1.0',
288
+ # },
289
+ #
290
+ # 'rrdtool', {
291
+ # # if ensure is not specified, 'present' is implied
292
+ # :version => '> 2.1',
293
+ # :constrain => 'is_virtual false',
294
+ # },
295
+ # supported keys:
296
+ # * :exists|ensure
297
+ # * :version (literal or basic comparison)
298
+ # * :constrain
299
+ def validate_package(name, expectations, fail_fast=false)
300
+ packages = self.get_packages(true)
301
+
302
+ if expectations[:ensure].nil? and expectations[:exists].nil?
303
+ expectations[:ensure] = 'present'
304
+ end
305
+
306
+ if expectations.has_key?(:constrain)
307
+ expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]
308
+
309
+ expectations[:constrain].each do |constraint|
310
+ fact, expectation = constraint.split("\s")
311
+ unless meets_constraint?(fact, expectation)
312
+ @log.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
313
+ return true
314
+ end
315
+ end
316
+
317
+ expectations.delete(:constrain)
318
+ end
319
+
320
+ results = Hash.new()
321
+ local = nil
322
+
323
+ expectations.each do |k,v|
324
+ case k
325
+ when :ensure, :exists
326
+ if packages.has_key?(name)
327
+ if v.to_s.match(/absent|false/).nil?
328
+ local = true
329
+ else
330
+ local = false
331
+ end
332
+ else
333
+ local = v.to_s.match(/absent|false/).nil? ? false : true
334
+ end
335
+ when :version
336
+ 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?
343
+ end
344
+ else
345
+ local = false
346
+ end
347
+ when :type
348
+ # noop allowing parse_catalog() output to be passed directly
349
+ else
350
+ raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
351
+ end
352
+
353
+ return false if local.eql?(false) and fail_fast.eql?(true)
354
+ results[k] = local
355
+ end
356
+
357
+ # 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
+
360
+ results.find{|k,v| v.false? }.nil?
361
+ end
362
+
363
+ # given a port nnumber and a hash of expectations, returns true|false whether port meets expectations
364
+ #
365
+ # parameters
366
+ # * <number> - port number
367
+ # * <expectations> - hash of expectations, see examples
368
+ #
369
+ # example expectations:
370
+ # '22', {
371
+ # :ensure => 'active',
372
+ # :protocol => 'tcp',
373
+ # :address => '0.0.0.0'
374
+ # },
375
+ #
376
+ # '1234', {
377
+ # :ensure => 'open',
378
+ # :address => '*',
379
+ # :constrain => 'is_virtual false'
380
+ # }
381
+ #
382
+ # supported keys:
383
+ # * :exists|ensure|state
384
+ # * :address
385
+ # * :protocol|proto
386
+ # * :constrain
387
+ def validate_port(number, expectations, fail_fast=false)
388
+ number = number.to_s
389
+ ports = self.get_ports(true)
390
+
391
+ if expectations[:ensure].nil? and expectations[:exists].nil? and expectations[:state].nil?
392
+ expectations[:ensure] = 'present'
393
+ end
394
+
395
+ if expectations[:protocol].nil? and expectations[:proto].nil?
396
+ expectations[:protocol] = 'tcp'
397
+ elsif ! expectations[:proto].nil?
398
+ expectations[:protocol] = expectations[:proto]
399
+ end
400
+
401
+ if expectations.has_key?(:constrain)
402
+ expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]
403
+
404
+ expectations[:constrain].each do |constraint|
405
+ fact, expectation = constraint.split("\s")
406
+ unless meets_constraint?(fact, expectation)
407
+ @log.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
408
+ return true
409
+ end
410
+ end
411
+
412
+ expectations.delete(:constrain)
413
+ end
414
+
415
+ results = Hash.new()
416
+ local = nil
417
+
418
+ expectations.each do |k,v|
419
+ case k
420
+ when :ensure, :exists, :state
421
+ if v.to_s.match(/absent|false|open/)
422
+ local = ports[expectations[:protocol]][number].nil?
423
+ else
424
+ local = ! ports[expectations[:protocol]][number].nil?
425
+ end
426
+ when :protocol, :proto
427
+ # TODO rewrite this in a less hacky way
428
+ if expectations[:ensure].to_s.match(/absent|false|open/) or expectations[:exists].to_s.match(/absent|false|open/) or expectations[:state].to_s.match(/absent|false|open/)
429
+ local = true
430
+ else
431
+ local = ports[v].has_key?(number)
432
+ end
433
+
434
+ when :address
435
+ 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
442
+
443
+ else
444
+ raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
445
+ end
446
+
447
+ return false if local.eql?(false) and fail_fast.eql?(true)
448
+ results[k] = local
449
+ end
450
+
451
+ @log.info(results)
452
+
453
+ results.find{|k,v| v.false? }.nil?
454
+ end
455
+
456
+ ##
457
+ # validate_service
458
+ #
459
+ # given a service name and a hash of expectations, returns true|false whether package meets expectations
460
+ #
461
+ # parameters
462
+ # * <name> - service name
463
+ # * <expectations> - hash of expectations, see examples
464
+ # * <fail_fast> - return false immediately on any failure (default is false)
465
+ #
466
+ # example expectations:
467
+ # 'ntp', {
468
+ # :ensure => 'present',
469
+ # :state => 'started'
470
+ # },
471
+ #
472
+ # 'ypbind', {
473
+ # :state => 'stopped',
474
+ # }
475
+ #
476
+ # supported keys:
477
+ # * :exists|:ensure
478
+ # * :state,:status
479
+ # * :constrain
480
+ def validate_service(name, expectations, fail_fast=false)
481
+ services = self.get_services(true)
482
+
483
+ if expectations[:ensure].nil? and expectations[:exists].nil?
484
+ expectations[:ensure] = 'present'
485
+ end
486
+
487
+ if expectations.has_key?(:constrain)
488
+ expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]
489
+
490
+ expectations[:constrain].each do |constraint|
491
+ fact, expectation = constraint.split("\s")
492
+ unless meets_constraint?(fact, expectation)
493
+ @log.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
494
+ return true
495
+ end
496
+ end
497
+
498
+ expectations.delete(:constrain)
499
+ end
500
+
501
+ results = Hash.new()
502
+ local = nil
503
+
504
+ expectations.each do |k,v|
505
+ case k
506
+ when :ensure, :exists
507
+ if services.has_key?(name)
508
+ if v.to_s.match(/absent|false/)
509
+ local = false
510
+ else
511
+ local = true
512
+ end
513
+ else
514
+ local = v.to_s.match(/absent|false/).nil? ? false : true
515
+ end
516
+ when :state, :status
517
+ if services.has_key?(name)
518
+ local = ! v.match(/#{services[name]}/).nil?
519
+ else
520
+ local = false
521
+ end
522
+ when :type
523
+ # noop allowing parse_catalog() output to be passed directly
524
+ else
525
+ raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
526
+ end
527
+
528
+ return false if local.eql?(false) and fail_fast.eql?(true)
529
+ results[k] = local
530
+ end
531
+
532
+ @log.info(results)
533
+ results.find{|k,v| v.false? }.nil?
534
+
535
+ end
536
+
537
+ ##
538
+ # validate_user
539
+ #
540
+ # given a user name and a hash of expectations, returns true|false whether user meets expectations
541
+ #
542
+ # parameters
543
+ # * <name> - user name
544
+ # * <expectations> - hash of expectations, see examples
545
+ # * <fail_fast> - return false immediately on any failure (default is false)
546
+ #
547
+ # example expectations:
548
+ # 'root' => {
549
+ # :uid => 0
550
+ # },
551
+ #
552
+ # 'ftp' => {
553
+ # :exists => true,
554
+ # :home => '/var/ftp',
555
+ # :shell => 'nologin'
556
+ # },
557
+ #
558
+ # 'developer' => {
559
+ # :exists => 'false',
560
+ # :constrain => 'environment != production'
561
+ # }
562
+ #
563
+ # supported keys:
564
+ # * :exists|ensure
565
+ # * :home
566
+ # * :group
567
+ # * :shell
568
+ # * :uid
569
+ # * :gid
570
+ # * :constrain
571
+ def validate_user(name, expectations, fail_fast=false)
572
+ users = self.get_users(true)
573
+
574
+ if expectations[:ensure].nil? and expectations[:exists].nil?
575
+ expectations[:ensure] = 'present'
576
+ end
577
+
578
+ if expectations.has_key?(:constrain)
579
+ expectations[:constrain] = expectations[:constrain].class.eql?(Array) ? expectations[:constrain] : [expectations[:constrain]]
580
+
581
+ expectations[:constrain].each do |constraint|
582
+ fact, expectation = constraint.split("\s")
583
+ unless meets_constraint?(fact, expectation)
584
+ @log.info(sprintf('returning true for expectation [%s], did not meet constraint[%s/%s]', name, fact, expectation))
585
+ return true
586
+ end
587
+ end
588
+
589
+ expectations.delete(:constrain)
590
+ end
591
+
592
+ results = Hash.new()
593
+ local = nil
594
+
595
+ expectations.each do |k,v|
596
+ case k
597
+ when :ensure, :exists
598
+ if users.has_key?(name)
599
+ if v.to_s.match(/absent|false/).nil?
600
+ local = true
601
+ else
602
+ local = false
603
+ end
604
+ else
605
+ local = v.to_s.match(/absent|false/).nil? ? false : true
606
+ end
607
+ when :group
608
+ v = v.class.eql?(Array) ? v : [v]
609
+ v.each do |group|
610
+ local = is_user_in_group?(name, group)
611
+ break unless local.true?
612
+ end
613
+ when :gid
614
+ if users[name].is_a?(Hash) and users[name].has_key?(:gid)
615
+ local = v.to_i.eql?(users[name][:gid].to_i)
616
+ else
617
+ local = false
618
+ end
619
+ when :home
620
+ if users[name].is_a?(Hash) and users[name].has_key?(:home)
621
+ local = ! v.match(/#{users[name][:home]}/).nil?
622
+ else
623
+ local = false
624
+ end
625
+ when :home_exists
626
+ if users[name].is_a?(Hash) and users[name].has_key?(:home_exists)
627
+ local = ! v.to_s.match(/#{users[name][:home_exists].to_s}/).nil?
628
+ else
629
+ local = false
630
+ end
631
+ when :shell
632
+ if users[name].is_a?(Hash) and users[name].has_key?(:shell)
633
+ local = ! v.match(/#{users[name][:shell]}/).nil?
634
+ else
635
+ local = false
636
+ end
637
+ when :uid
638
+ if users[name].is_a?(Hash) and users[name].has_key?(:uid)
639
+ local = v.to_i.eql?(users[name][:uid].to_i)
640
+ else
641
+ local = false
642
+ end
643
+ when :type
644
+ # noop allowing parse_catalog() output to be passed directly
645
+ else
646
+ raise InternalError.new(sprintf('unknown expectation[%s / %s]', k, v))
647
+ end
648
+
649
+ return false if local.eql?(false) and fail_fast.eql?(true)
650
+ results[k] = local
651
+ end
652
+
653
+ @log.info(results)
654
+ results.find{|k,v| v.false? }.nil?
655
+
656
+ end
657
+
658
+ ## internal methods
659
+ private
660
+
661
+ ##
662
+ # meets_constraint?
663
+ #
664
+ # powers the :constrain value in expectations passed to validate_*
665
+ # gets facts from node, and if fact expectation regex matches actual fact, returns true
666
+ #
667
+ # 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')
676
+ return false
677
+ end
678
+
679
+ expectation = expectation.to_s
680
+ facts = self.facter(cache)
681
+
682
+ res = nil
683
+ if expectation.split("\s").size > 1
684
+ ## generic comparator functionality
685
+ comp, expectation = expectation.split("\s")
686
+
687
+ res = generic_comparator(facts[fact], comp, expectation)
688
+
689
+ else
690
+ res = ! expectation.match(/#{facts[fact]}/).nil?
691
+ @log.debug(sprintf('meets_constraint?(%s, %s): %s', fact, expectation, res.nil?))
692
+ end
693
+
694
+ res
695
+ end
696
+
697
+ ##
698
+ # generic_comparator
699
+ #
700
+ # powers the 3 argument form of constraint (i.e. 'is_virtual != true', '<package_version> > 3.0', etc)
701
+ #
702
+ # should really be an eval{} of some sort (or would be in the perl world)
703
+ #
704
+ # parameters
705
+ # * <comparand1> - left side of the comparison
706
+ # * <comparator> - comparison to make
707
+ # * <comparand2> - right side of the comparison
708
+ def generic_comparator(comparand1, comparator, comparand2)
709
+
710
+ # TODO rewrite this as an eval so we don't have to support everything..
711
+ case comparator
712
+ when '!='
713
+ # ugh
714
+ if comparand1.to_s.match(/\d/) or comparand2.to_s.match(/\d/)
715
+ res = ! comparand1.to_i.eql?(comparand2.to_i)
716
+ else
717
+ res = ! comparand1.eql?(comparand2)
718
+ end
719
+ when '<'
720
+ res = comparand1.to_i < comparand2.to_i
721
+ when '<='
722
+ res = comparand1.to_i <= comparand2.to_i
723
+ when '>'
724
+ res = comparand1.to_i > comparand2.to_i
725
+ when '>='
726
+ res = comparand1.to_i >= comparand2.to_i
727
+ when '=='
728
+ # ugh ugh
729
+ if comparand1.to_s.match(/\d/) or comparand2.to_s.match(/\d/)
730
+ res = comparand1.to_i.eql?(comparand2.to_i)
731
+ else
732
+ res = comparand1.eql?(comparand2)
733
+ end
734
+ else
735
+ raise NotImplementedError.new(sprintf('unknown comparator[%s]', comparator))
736
+ end
737
+
738
+
739
+
740
+ res
741
+ end
742
+
743
+ end