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
@@ -1,11 +1,9 @@
1
1
  require sprintf('%s/../../%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
2
2
 
3
- # deltas.rb - get information about groups, packages, services and users inside a Vagrant VM
3
+ # deltas.rb - get information about crontabs, groups, packages, ports, services and users inside a Vagrant VM
4
4
  require 'rouster'
5
5
  require 'rouster/tests'
6
6
 
7
- # TODO use @cache_timeout to invalidate data cached here
8
-
9
7
  class Rouster
10
8
 
11
9
  ##
@@ -14,13 +12,12 @@ class Rouster
14
12
  # runs `crontab -l <user>` and parses output, returns hash:
15
13
  # {
16
14
  # user => {
17
- # logicalOrderInt => {
15
+ # command => {
18
16
  # :minute => minute,
19
17
  # :hour => hour,
20
18
  # :dom => dom, # day of month
21
19
  # :mon => mon, # month
22
20
  # :dow => dow, # day of week
23
- # :command => command,
24
21
  # }
25
22
  # }
26
23
  # }
@@ -33,16 +30,24 @@ class Rouster
33
30
  def get_crontab(user='root', cache=true)
34
31
 
35
32
  if cache and self.deltas[:crontab].class.eql?(Hash)
36
- if self.deltas[:crontab].has_key?(user)
33
+
34
+ if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:crontab]) > self.cache_timeout
35
+ @logger.debug(sprintf('invalidating [crontab] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:crontab]), self.cache_timeout))
36
+ self.deltas.delete(:crontab)
37
+ end
38
+
39
+ if self.deltas.has_key?(:crontab) and self.deltas[:crontab].has_key?(user)
40
+ @logger.debug(sprintf('using cached [crontab] from [%s]', self.cache[:crontab]))
37
41
  return self.deltas[:crontab][user]
42
+ elsif self.deltas.has_key?(:crontab) and user.eql?('*')
43
+ @logger.debug(sprintf('using cached [crontab] from [%s]', self.cache[:crontab]))
44
+ return self.deltas[:crontab]
38
45
  else
39
46
  # noop fallthrough to gather data to cache
40
47
  end
41
- elsif cache and self.deltas[:crontab].class.eql?(Hash) and user.eql?('*')
42
- return self.deltas[:crontab]
48
+
43
49
  end
44
50
 
45
- i = 0
46
51
  res = Hash.new
47
52
  users = nil
48
53
 
@@ -62,23 +67,37 @@ class Rouster
62
67
  end
63
68
 
64
69
  raw.split("\n").each do |line|
70
+ next if line.match(/^#|^\s+$/)
65
71
  elements = line.split("\s")
66
72
 
73
+ if elements.size < 5
74
+ # this is usually (only?) caused by ENV_VARIABLE=VALUE directives
75
+ @logger.debug(sprintf('line [%s] did not match format expectations for a crontab entry, skipping', line))
76
+ next
77
+ end
78
+
79
+ command = elements[5..elements.size].join(' ')
80
+
67
81
  res[u] ||= Hash.new
68
- res[u][i] ||= Hash.new
69
-
70
- res[u][i][:minute] = elements[0]
71
- res[u][i][:hour] = elements[1]
72
- res[u][i][:dom] = elements[2]
73
- res[u][i][:mon] = elements[3]
74
- res[u][i][:dow] = elements[4]
75
- res[u][i][:command] = elements[5..elements.size].join(" ")
76
- end
77
82
 
78
- i += 1
83
+ if res[u][command].class.eql?(Hash)
84
+ unique = elements.join('')
85
+ command = sprintf('%s-duplicate.%s', command, unique)
86
+ @logger.info(sprintf('duplicate crontab command found, adding hash key[%s]', command))
87
+ end
88
+
89
+ res[u][command] = Hash.new
90
+ res[u][command][:minute] = elements[0]
91
+ res[u][command][:hour] = elements[1]
92
+ res[u][command][:dom] = elements[2]
93
+ res[u][command][:mon] = elements[3]
94
+ res[u][command][:dow] = elements[4]
95
+ end
79
96
  end
80
97
 
81
98
  if cache
99
+ @logger.debug(sprintf('caching [crontab] at [%s]', Time.now.asctime))
100
+
82
101
  if ! user.eql?('*')
83
102
  self.deltas[:crontab] ||= Hash.new
84
103
  self.deltas[:crontab][user] ||= Hash.new
@@ -87,6 +106,9 @@ class Rouster
87
106
  self.deltas[:crontab] ||= Hash.new
88
107
  self.deltas[:crontab] = res
89
108
  end
109
+
110
+ self.cache[:crontab] = Time.now.to_i
111
+
90
112
  end
91
113
 
92
114
  return user.eql?('*') ? res : res[user]
@@ -107,28 +129,48 @@ class Rouster
107
129
  # * [cache] - boolean controlling whether data retrieved/parsed is cached, defaults to true
108
130
  # * [deep] - boolean controlling whether get_users() is called in order to correctly populate res[group][:users]
109
131
  def get_groups(cache=true, deep=true)
132
+
110
133
  if cache and ! self.deltas[:groups].nil?
111
- return self.deltas[:groups]
134
+
135
+ if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:groups]) > self.cache_timeout
136
+ @logger.debug(sprintf('invalidating [groups] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:groups]), self.cache_timeout))
137
+ self.deltas.delete(:groups)
138
+ else
139
+ @logger.debug(sprintf('using cached [groups] from [%s]', self.cache[:groups]))
140
+ return self.deltas[:groups]
141
+ end
142
+
112
143
  end
113
144
 
114
145
  res = Hash.new()
115
146
 
116
- raw = self.run('cat /etc/group')
147
+ {
148
+ :file => self.run('cat /etc/group'),
149
+ :dynamic => self.run('getent group', [0,127]),
150
+ }.each_pair do |source, raw|
117
151
 
118
- raw.split("\n").each do |line|
119
- next unless line.match(/\w+:\w+:\w+/)
152
+ raw.split("\n").each do |line|
153
+ next unless line.match(/\w+:\w+:\w+/)
120
154
 
121
- data = line.split(':')
155
+ data = line.split(':')
122
156
 
123
- group = data[0]
124
- gid = data[2]
157
+ group = data[0]
158
+ gid = data[2]
125
159
 
126
- # this works in some cases, deep functionality picks up the others
127
- users = data[3].nil? ? ['NONE'] : data[3].split(',')
160
+ # this works in some cases, deep functionality picks up the others
161
+ users = data[3].nil? ? ['NONE'] : data[3].split(',')
162
+
163
+ if res.has_key?(group)
164
+ @logger.debug(sprintf('for[%s] old GID[%s] new GID[%s]', group, gid, res[group][:users])) unless gid.eql?(res[group][:gid])
165
+ @logger.debug(sprintf('for[%s] old users[%s] new users[%s]', group, users)) unless users.eql?(res[group][:users])
166
+ end
167
+
168
+ res[group] = Hash.new() # i miss autovivification
169
+ res[group][:gid] = gid
170
+ res[group][:users] = users
171
+ res[group][:source] = source
172
+ end
128
173
 
129
- res[group] = Hash.new() # i miss autovivification
130
- res[group][:gid] = gid
131
- res[group][:users] = users
132
174
  end
133
175
 
134
176
  groups = res
@@ -136,11 +178,19 @@ class Rouster
136
178
  if deep
137
179
  users = self.get_users(cache)
138
180
 
181
+ known_valid_gids = groups.keys.map { |g| groups[g][:gid] } # no need to calculate this in every loop
182
+
139
183
  # TODO better, much better -- since the number of users/groups is finite and usually small, this is a low priority
140
184
  users.each_key do |user|
141
185
  # iterate over each user to get their gid
142
186
  gid = users[user][:gid]
143
187
 
188
+ unless known_valid_gids.member?(gid)
189
+ @logger.warn(sprintf('found user[%s] with unknown GID[%s], known GIDs[%s]', user, gid, known_valid_gids))
190
+ next
191
+ end
192
+
193
+ ## do this more efficiently
144
194
  groups.each_key do |group|
145
195
  # iterate over each group to find the matching gid
146
196
  if gid.eql?(groups[group][:gid])
@@ -150,14 +200,15 @@ class Rouster
150
200
  groups[group][:users] << user unless groups[group][:users].member?(user)
151
201
  end
152
202
 
153
- # TODO throw an error if we find a user with a GID that we don't know about from get_groups()
154
203
  end
155
204
 
156
205
  end
157
206
  end
158
207
 
159
208
  if cache
209
+ @logger.debug(sprintf('caching [groups] at [%s]', Time.now.asctime))
160
210
  self.deltas[:groups] = groups
211
+ self.cache[:groups] = Time.now.to_i
161
212
  end
162
213
 
163
214
  groups
@@ -179,14 +230,22 @@ class Rouster
179
230
  #
180
231
  # supported OS
181
232
  # * OSX - runs `pkgutil --pkgs` and `pkgutil --pkg-info=<package>` (if deep)
182
- # * RedHat - runs `rpm -qa`
233
+ # * RedHat - runs `rpm -qa --qf "%{n}@%{v}@%{arch}\n"` (does not support deep)
183
234
  # * Solaris - runs `pkginfo` and `pkginfo -l <package>` (if deep)
184
- # * Ubuntu - runs `dpkg --get-selections` and `dpkg -s <package>` (if deep)
235
+ # * Ubuntu - runs `dpkg-query -W -f='${Package}\@${Version}\@${Architecture}\n'` (does not support deep)
185
236
  #
186
237
  # raises InternalError if unsupported operating system
187
238
  def get_packages(cache=true, deep=true)
188
239
  if cache and ! self.deltas[:packages].nil?
189
- return self.deltas[:packages]
240
+
241
+ if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:packages]) > self.cache_timeout
242
+ @logger.debug(sprintf('invalidating [packages] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:packages]), self.cache_timeout))
243
+ self.deltas.delete(:packages)
244
+ else
245
+ @logger.debug(sprintf('using cached [packages] from [%s]', self.cache[:packages]))
246
+ return self.deltas[:packages]
247
+ end
248
+
190
249
  end
191
250
 
192
251
  res = Hash.new()
@@ -197,62 +256,93 @@ class Rouster
197
256
 
198
257
  raw = self.run('pkgutil --pkgs')
199
258
  raw.split("\n").each do |line|
259
+ name = line
260
+ arch = '?'
200
261
  version = '?'
201
262
 
202
263
  if deep
203
264
  # can get install time, volume and location as well
204
- local_res = self.run(sprintf('pkgutil --pkg-info=%s', line))
205
- version = $1 if local_res.match(/version\:\s+(.*?)$/)
265
+ local_res = self.run(sprintf('pkgutil --pkg-info=%s', name))
266
+ version = $1 if local_res.match(/version\:\s+(.*?)$/)
267
+ end
268
+
269
+ if res.has_key?(name)
270
+ # different architecture of an already known package
271
+ @logger.debug(sprintf('found package with already known name[%s], value[%s], new line[%s], turning into array', name, res[name], line))
272
+ new_element = { :version => version, :arch => arch }
273
+ res[name] = [ res[name], new_element ]
274
+ else
275
+ res[name] = { :version => version, :arch => arch }
206
276
  end
207
277
 
208
- res[line] = version
209
278
  end
210
279
 
211
280
  elsif os.eql?(:solaris)
212
281
  raw = self.run('pkginfo')
213
282
  raw.split("\n").each do |line|
214
- next if line.match(/(.*?)\s+(.*?)\s(.*)$/).empty?
283
+ next if line.match(/(.*?)\s+(.*?)\s(.*)$/).nil?
215
284
  name = $2
285
+ arch = '?'
216
286
  version = '?'
217
287
 
218
288
  if deep
219
- local_res = self.run(sprintf('pkginfo -l %s', name))
220
- version = $1 if local_res.match(/VERSION\:\s+(.*?)$/i)
289
+ begin
290
+ # who throws non-0 exit codes when querying for legit package information? solaris does.
291
+ local_res = self.run(sprintf('pkginfo -l %s', name))
292
+ arch = $1 if local_res.match(/ARCH\:\s+(.*?)$/)
293
+ version = $1 if local_res.match(/VERSION\:\s+(.*?)$/)
294
+ rescue
295
+ arch = '?' if arch.nil?
296
+ version = '?' if arch.nil?
297
+ end
221
298
  end
222
299
 
223
- res[name] = version
300
+ if res.has_key?(name)
301
+ # different architecture of an already known package
302
+ @logger.debug(sprintf('found package with already known name[%s], value[%s], new line[%s], turning into array', name, res[name], line))
303
+ new_element = { :version => version, :arch => arch }
304
+ res[name] = [ res[name], new_element ]
305
+ else
306
+ res[name] = { :version => version, :arch => arch }
307
+ end
224
308
  end
225
309
 
226
310
  elsif os.eql?(:ubuntu) or os.eql?(:debian)
227
- raw = self.run('dpkg --get-selections')
311
+ raw = self.run("dpkg-query -W -f='${Package}@${Version}@${Architecture}\n'")
228
312
  raw.split("\n").each do |line|
229
- next if line.match(/^(.*?)\s/).nil?
313
+ next if line.match(/(.*?)\@(.*?)\@(.*)/).nil?
230
314
  name = $1
231
- version = '?'
232
-
233
- if deep
234
- local_res = self.run(sprintf('dpkg -s %s', name))
235
- version = $1 if local_res.match(/Version\:\s(.*?)$/)
315
+ version = $2
316
+ arch = $3
317
+
318
+ if res.has_key?(name)
319
+ # different architecture of an already known package
320
+ @logger.debug(sprintf('found package with already known name[%s], value[%s], new line[%s], turning into array', name, res[name], line))
321
+ new_element = { :version => version, :arch => arch }
322
+ res[name] = [ res[name], new_element ]
323
+ else
324
+ res[name] = { :version => version, :arch => arch }
236
325
  end
237
326
 
238
- res[name] = version
239
327
  end
240
328
 
241
- elsif os.eql?(:redhat)
242
- raw = self.run('rpm -qa')
329
+ elsif os.eql?(:rhel)
330
+ raw = self.run('rpm -qa --qf "%{n}@%{v}@%{arch}\n"')
243
331
  raw.split("\n").each do |line|
244
- next if line.match(/(.*?)-(\d*\..*)/).nil? # ht petersen.allen
245
- #next if line.match(/(.*)-(\d+\.\d+.*)/).nil? # another alternate, but still not perfect
332
+ next if line.match(/(.*?)\@(.*?)\@(.*)/).nil?
246
333
  name = $1
247
- version = '?' # we could use $2, but we don't trust it
248
-
249
- if deep
250
- local_res = self.run(sprintf('rpm -qi %s', line))
251
- name = $1 if local_res.match(/Name\s+:\s(\S*)/)
252
- version = $1 if local_res.match(/Version\s+:\s(\S*)/)
334
+ version = $2
335
+ arch = $3
336
+
337
+ if res.has_key?(name)
338
+ # different architecture of an already known package
339
+ @logger.debug(sprintf('found package with already known name[%s], value[%s], new line[%s], turning into array', name, res[name], line))
340
+ new_element = { :version => version, :arch => arch }
341
+ res[name] = [ res[name], new_element ]
342
+ else
343
+ res[name] = { :version => version, :arch => arch }
253
344
  end
254
345
 
255
- res[name] = version
256
346
  end
257
347
 
258
348
  else
@@ -260,7 +350,9 @@ class Rouster
260
350
  end
261
351
 
262
352
  if cache
353
+ @logger.debug(sprintf('caching [packages] at [%s]', Time.now.asctime))
263
354
  self.deltas[:packages] = res
355
+ self.cache[:packages] = Time.now.to_i
264
356
  end
265
357
 
266
358
  res
@@ -290,13 +382,19 @@ class Rouster
290
382
  # TODO improve ipv6 support
291
383
 
292
384
  if cache and ! self.deltas[:ports].nil?
293
- return self.deltas[:ports]
385
+ if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:ports]) > self.cache_timeout
386
+ @logger.debug(sprintf('invalidating [ports] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:ports]), self.cache_timeout))
387
+ self.deltas.delete(:ports)
388
+ else
389
+ @logger.debug(sprintf('using cached [ports] from [%s]', self.cache[:ports]))
390
+ return self.deltas[:ports]
391
+ end
294
392
  end
295
393
 
296
394
  res = Hash.new()
297
395
  os = self.os_type()
298
396
 
299
- if os.eql?(:redhat) or os.eql?(:ubuntu) or os.eql?(:debian)
397
+ if os.eql?(:rhel) or os.eql?(:ubuntu) or os.eql?(:debian)
300
398
 
301
399
  raw = self.run('netstat -ln')
302
400
 
@@ -320,7 +418,9 @@ class Rouster
320
418
  end
321
419
 
322
420
  if cache
421
+ @logger.debug(sprintf('caching [ports] at [%s]', Time.now.asctime))
323
422
  self.deltas[:ports] = res
423
+ self.cache[:ports] = Time.now.to_i
324
424
  end
325
425
 
326
426
  res
@@ -331,103 +431,306 @@ class Rouster
331
431
  #
332
432
  # runs an OS appropriate command to gather service information, returns hash:
333
433
  # {
334
- # serviceN => mode # running|stopped|unsure
434
+ # serviceN => mode # exists|installed|operational|running|stopped|unsure
335
435
  # }
336
436
  #
337
437
  # parameters
338
- # * [cache] - boolean controlling whether data retrieved/parsed is cached, defaults to true
438
+ # * [cache] - boolean controlling whether data retrieved/parsed is cached, defaults to true
439
+ # * [humanize] - boolean controlling whether data retrieved is massaged into simplified names or returned mostly as retrieved
440
+ # * [type] - symbol indicating which service controller to query, defaults to :all
441
+ # * [seed] - test hook to seed the output of service commands
339
442
  #
340
- # supported OS
341
- # * OSX - runs `launchctl list`
342
- # * RedHat - runs `/sbin/service --status-all`
343
- # * Solaris - runs `svcs`
344
- # * Ubuntu - runs `service --status-all`
443
+ # supported OS and types
444
+ # * OSX - :launchd
445
+ # * RedHat - :systemv or :upstart
446
+ # * Solaris - :smf
447
+ # * Ubuntu - :systemv or :upstart
345
448
  #
346
- # raises InternalError if unsupported operating system
347
- def get_services(cache=true)
449
+ # notes
450
+ # * raises InternalError if unsupported operating system
451
+ # * OSX, Solaris and Ubuntu/Debian will only return running|stopped|unsure, the exists|installed|operational modes are RHEL/CentOS only
452
+
453
+ def get_services(cache=true, humanize=true, type=:all, seed=nil)
348
454
  if cache and ! self.deltas[:services].nil?
349
- return self.deltas[:services]
455
+
456
+ if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:services]) > self.cache_timeout
457
+ @logger.debug(sprintf('invalidating [services] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:services]), self.cache_timeout))
458
+ self.deltas.delete(:services)
459
+ else
460
+ @logger.debug(sprintf('using cached [services] from [%s]', self.cache[:services]))
461
+ return self.deltas[:services]
462
+ end
463
+
350
464
  end
351
465
 
352
466
  res = Hash.new()
467
+ os = self.os_type
468
+
469
+ commands = {
470
+ :osx => {
471
+ :launchd => 'launchctl list',
472
+ },
473
+ :solaris => {
474
+ :smf => 'svcs -a',
475
+ },
476
+
477
+ # TODO we really need to implement something like osfamily
478
+ :ubuntu => {
479
+ :systemv => 'service --status-all 2>&1',
480
+ :upstart => 'initctl list',
481
+ },
482
+ :debian => {
483
+ :systemv => 'service --status-all 2>&1',
484
+ :upstart => 'initctl list',
485
+ },
486
+ :rhel => {
487
+ :systemd => 'systemctl list-units --type=service --no-pager',
488
+ :systemv => 'service --status-all',
489
+ :upstart => 'initctl list',
490
+ },
491
+
492
+ :invalid => {
493
+ :invalid => 'invalid',
494
+ },
495
+ }
496
+
497
+ if type.eql?(:all)
498
+ type = commands[os].keys
499
+ end
353
500
 
354
- os = self.os_type
501
+ type = type.class.eql?(Array) ? type : [ type ]
355
502
 
356
- if os.eql?(:osx)
503
+ type.each do |provider|
357
504
 
358
- raw = self.run('launchctl list')
359
- raw.split("\n").each do |line|
360
- next if line.match(/(?:\S*?)\s+(\S*?)\s+(\S*)$/).nil?
505
+ raise InternalError.new(sprintf('unable to get service information from VM operating system[%s]', os)) if provider.eql?(:invalid)
506
+ raise ArgumentError.new(sprintf('unable to find command provider[%s] for [%s]', provider, os)) if commands[os][provider].nil?
507
+
508
+ unless seed or self.is_in_path?(commands[os][provider].split(' ').first)
509
+ @logger.info(sprintf('skipping provider[%s], not in $PATH[%s]', provider, commands[os][provider]))
510
+ next
511
+ end
361
512
 
362
- service = $2
363
- mode = $1
513
+ @logger.info(sprintf('get_services using provider [%s] on [%s]', provider, os))
364
514
 
365
- if mode.match(/^\d/)
366
- mode = 'running'
367
- else
368
- mode = 'stopped'
515
+ # TODO while this is true, what if self.user is 'root'.. -- the problem is we don't have self.user, and we store this data differently depending on self.passthrough?
516
+ @logger.warn('gathering service information typically works better with sudo, which is currently not being used') unless self.uses_sudo?
517
+
518
+ # TODO come up with a better test hook -- real problem is that we can't seed 'raw' with different values per iteration
519
+ raw = seed.nil? ? self.run(commands[os][provider]) : seed
520
+
521
+ if os.eql?(:osx)
522
+
523
+ raw.split("\n").each do |line|
524
+ next if line.match(/(?:\S*?)\s+(\S*?)\s+(\S+)$/).nil?
525
+ tokens = line.split("\s")
526
+ service = tokens[-1]
527
+ mode = tokens[0]
528
+
529
+ if humanize # should we do this with a .freeze instead?
530
+ if mode.match(/^\d/)
531
+ mode = 'running'
532
+ elsif mode.match(/-/)
533
+ mode = 'stopped'
534
+ else
535
+ next # this should handle the banner "PID Status Label"
536
+ end
537
+ end
538
+
539
+ res[service] = mode
369
540
  end
370
541
 
371
- res[service] = mode
372
- end
542
+ elsif os.eql?(:solaris)
373
543
 
374
- elsif os.eql?(:solaris)
544
+ raw.split("\n").each do |line|
545
+ next if line.match(/(.*?)\s+(?:.*?)\s+(.*?)$/).nil?
375
546
 
376
- raw = self.run('svcs')
377
- raw.split("\n").each do |line|
378
- next if line.match(/(.*?)\s+(?:.*?)\s+(.*?)$/).nil?
547
+ service = $2
548
+ mode = $1
379
549
 
380
- service = $2
381
- mode = $1
550
+ if humanize
551
+ if mode.match(/^online/)
552
+ mode = 'running'
553
+ elsif mode.match(/^legacy_run/)
554
+ mode = 'running'
555
+ elsif mode.match(/^disabled/)
556
+ mode = 'stopped'
557
+ end
558
+
559
+ if service.match(/^svc:\/.*\/(.*?):.*/)
560
+ # turning 'svc:/network/cswpuppetd:default' into 'cswpuppetd'
561
+ service = $1
562
+ elsif service.match(/^lrc:\/.*?\/.*\/(.*)/)
563
+ # turning 'lrc:/etc/rcS_d/S50sk98Sol' into 'S50sk98Sol'
564
+ service = $1
565
+ end
566
+ end
567
+
568
+ res[service] = mode
382
569
 
383
- if mode.match(/online/)
384
- mode = 'running'
385
- elsif mode.match(/legacy_run/)
386
- mode = 'running'
387
- elsif mode.match(//)
388
- mode = 'stopped'
389
570
  end
390
571
 
391
- res[service] = mode
572
+ elsif os.eql?(:ubuntu) or os.eql?(:debian)
573
+
574
+ raw.split("\n").each do |line|
575
+ if provider.eql?(:systemv)
576
+ next if line.match(/\[(.*?)\]\s+(.*)$/).nil?
577
+ mode = $1
578
+ service = $2
579
+
580
+ if humanize
581
+ mode = 'stopped' if mode.match('-')
582
+ mode = 'running' if mode.match('\+')
583
+ mode = 'unsure' if mode.match('\?')
584
+ end
585
+
586
+ res[service] = mode
587
+ elsif provider.eql?(:upstart)
588
+ if line.match(/(.*?)\s.*?(.*?),/)
589
+ # tty (/dev/tty3) start/running, process 1601
590
+ # named start/running, process 8959
591
+ service = $1
592
+ mode = $2
593
+ elsif line.match(/(.*?)\s(.*)/)
594
+ # rcS stop/waiting
595
+ service = $1
596
+ mode = $2
597
+ else
598
+ @logger.warn("unable to process upstart line[#{line}], skipping")
599
+ next
600
+ end
601
+
602
+ if humanize
603
+ mode = 'stopped' if mode.match('stop/waiting')
604
+ mode = 'running' if mode.match('start/running')
605
+ mode = 'unsure' unless mode.eql?('stopped') or mode.eql?('running')
606
+ end
607
+
608
+ res[service] = mode
609
+ end
610
+ end
392
611
 
393
- end
612
+ elsif os.eql?(:rhel)
613
+
614
+ raw.split("\n").each do |line|
615
+ if provider.eql?(:systemv)
616
+ if humanize
617
+ if line.match(/^(\w+?)\sis\s(.*)$/)
618
+ # <service> is <state>
619
+ name = $1
620
+ state = $2
621
+ res[name] = state
622
+
623
+ if state.match(/^not/)
624
+ # this catches 'Kdump is not operational'
625
+ res[name] = 'stopped'
626
+ end
627
+
628
+ elsif line.match(/^(\w+?)\s\(pid.*?\)\sis\s(\w+)$/)
629
+ # <service> (pid <pid> [pid]) is <state>...
630
+ res[$1] = $2
631
+ elsif line.match(/^(\w+?)\sis\s(\w+)\.*$/) # not sure this is actually needed
632
+ @logger.debug('triggered supposedly unnecessary regex')
633
+ # <service> is <state>. whatever
634
+ res[$1] = $2
635
+ elsif line.match(/razor_daemon:\s(\w+).*$/)
636
+ # razor_daemon: running [pid 11325]
637
+ # razor_daemon: no instances running
638
+ res['razor_daemon'] = $1.eql?('running') ? $1 : 'stopped'
639
+ elsif line.match(/^(\w+?)\:.*?(\w+)$/)
640
+ # <service>: whatever <state>
641
+ res[$1] = $2
642
+ elsif line.match(/^(\w+?):.*?\sis\snot\srunning\.$/)
643
+ # ip6tables: Firewall is not running.
644
+ res[$1] = 'stopped'
645
+ elsif line.match(/^(\w+?)\s.*?\s(.*)$/)
646
+ # netconsole module not loaded
647
+ state = $2
648
+ res[$1] = $2.match(/not/) ? 'stopped' : 'running'
649
+ elsif line.match(/^(\w+)\s(\w+).*$/)
650
+ # <process> <state> whatever
651
+ res[$1] = $2
652
+ else
653
+ # original regex implementation, if we didn't match anything else, failover to this
654
+ next if line.match(/^([^\s:]*).*\s(\w*)(?:\.?){3}$/).nil?
655
+ res[$1] = $2
656
+ end
657
+ else
658
+ next if line.match(/^([^\s:]*).*\s(\w*)(?:\.?){3}$/).nil?
659
+ res[$1] = $2
660
+ end
661
+ elsif provider.eql?(:upstart)
662
+
663
+ if line.match(/(.*?)\s.*?(.*?),/)
664
+ # tty (/dev/tty3) start/running, process 1601
665
+ # named start/running, process 8959
666
+ service = $1
667
+ mode = $2
668
+ elsif line.match(/(.*?)\s(.*)/)
669
+ # rcS stop/waiting
670
+ service = $1
671
+ mode = $2
672
+ else
673
+ @logger.warn("unable to process upstart line[#{line}], skipping")
674
+ next
675
+ end
394
676
 
395
- elsif os.eql?(:ubuntu) or os.eql?(:debian)
677
+ if humanize
678
+ mode = 'stopped' if mode.match('stop/waiting')
679
+ mode = 'running' if mode.match('start/running')
680
+ mode = 'unsure' unless mode.eql?('stopped') or mode.eql?('running')
681
+ end
396
682
 
397
- raw = self.run('service --status-all 2>&1')
398
- raw.split("\n").each do |line|
399
- next if line.match(/\[(.*?)\]\s+(.*)$/).nil?
400
- mode = $1
401
- service = $2
683
+ res[service] = mode unless res.has_key?(service)
402
684
 
403
- mode = 'stopped' if mode.match('-')
404
- mode = 'running' if mode.match('\+')
405
- mode = 'unsure' if mode.match('\?')
685
+ elsif provider.eql?(:systemd)
686
+ # UNIT LOAD ACTIVE SUB DESCRIPTION
687
+ # nfs-utils.service loaded inactive dead NFS server and client services
688
+ # crond.service loaded active running Command Scheduler
406
689
 
407
- res[service] = mode
408
- end
690
+ if line.match(/^\W*(.*?)\.service\s+(?:.*?)\s+(.*?)\s+(.*?)\s+(?:.*?)$/) # 5 space separated characters
691
+ service = $1
692
+ active = $2
693
+ sub = $3
409
694
 
410
- elsif os.eql?(:redhat)
695
+ if humanize
696
+ mode = sub.match('running') ? 'running' : 'stopped'
697
+ mode = 'unsure' unless mode.eql?('stopped') or mode.eql?('running')
698
+ end
411
699
 
412
- raw = self.run('/sbin/service --status-all')
413
- raw.split("\n").each do |line|
414
- # TODO support:
415
- # <service> is <state>
416
- # <service> (pid <pid> [pid]) is <state>...
417
- # <service> is <state>. whatever
418
- # <service>: whatever <state>
419
- # <process> <state> whatever
420
-
421
- next if line.match(/^([^\s:]*).*\s(\w*)(?:\.?){3}$/).nil?
422
- res[$1] = $2
700
+ res[service] = mode
701
+ else
702
+ # not logging here, there is a bunch of garbage output at the end of the output that we can't seem to suppress
703
+ next
704
+ end
705
+
706
+ end
707
+
708
+ end
709
+ else
710
+ raise InternalError.new(sprintf('unable to get service information from VM operating system[%s]', os))
423
711
  end
424
712
 
425
- else
426
- raise InternalError.new(sprintf('unable to get service information from VM operating system[%s]', os))
713
+
714
+ # end of provider processing
715
+ end
716
+
717
+ # issue #63 handling
718
+ # TODO should we consider using symbols here instead?
719
+ allowed_modes = %w(exists installed operational running stopped unsure)
720
+ failover_mode = 'unsure'
721
+
722
+ if humanize
723
+ res.each_pair do |k,v|
724
+ next if allowed_modes.member?(v)
725
+ @logger.debug(sprintf('replacing service[%s] status of [%s] with [%s] for uniformity', k, v, failover_mode))
726
+ res[k] = failover_mode
727
+ end
427
728
  end
428
729
 
429
730
  if cache
731
+ @logger.debug(sprintf('caching [services] at [%s]', Time.now.asctime))
430
732
  self.deltas[:services] = res
733
+ self.cache[:services] = Time.now.to_i
431
734
  end
432
735
 
433
736
  res
@@ -450,29 +753,58 @@ class Rouster
450
753
  # * [cache] - boolean controlling whether data retrieved/parsed is cached, defaults to true
451
754
  def get_users(cache=true)
452
755
  if cache and ! self.deltas[:users].nil?
453
- return self.deltas[:users]
756
+
757
+ if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:users]) > self.cache_timeout
758
+ @logger.debug(sprintf('invalidating [users] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:users]), self.cache_timeout))
759
+ self.deltas.delete(:users)
760
+ else
761
+ @logger.debug(sprintf('using cached [users] from [%s]', self.cache[:users]))
762
+ return self.deltas[:users]
763
+ end
764
+
454
765
  end
455
766
 
456
767
  res = Hash.new()
457
768
 
458
- raw = self.run('cat /etc/passwd')
459
-
460
- raw.split("\n").each do |line|
461
- next if line.match(/(\w+)(?::\w+){3,}/).nil?
769
+ {
770
+ :file => self.run('cat /etc/passwd'),
771
+ :dynamic => self.run('getent passwd', [0,127]),
772
+ }.each do |source, raw|
462
773
 
463
- user = $1
464
- data = line.split(':')
774
+ raw.split("\n").each do |line|
775
+ next if line.match(/(\w+)(?::\w+){3,}/).nil?
776
+
777
+ user = $1
778
+ data = line.split(':')
779
+
780
+ shell = data[-1]
781
+ home = data[-2]
782
+ home_exists = self.is_dir?(data[-2])
783
+ uid = data[2]
784
+ gid = data[3]
785
+
786
+ if res.has_key?(user)
787
+ @logger.info(sprintf('for[%s] old shell[%s], new shell[%s]', res[user][:shell], shell)) unless shell.eql?(res[user][:shell])
788
+ @logger.info(sprintf('for[%s] old home[%s], new home[%s]', res[user][:home], home)) unless home.eql?(res[user][:home])
789
+ @logger.info(sprintf('for[%s] old home_exists[%s], new home_exists[%s]', res[user][:home_exists], home_exists)) unless home_exists.eql?(res[user][:home_exists])
790
+ @logger.info(sprintf('for[%s] old UID[%s], new UID[%s]', res[user][:uid], uid)) unless uid.eql?(res[user][:uid])
791
+ @logger.info(sprintf('for[%s] old GID[%s], new GID[%s]', res[user][:gid], gid)) unless gid.eql?(res[user][:gid])
792
+ end
465
793
 
466
- res[user] = Hash.new()
467
- res[user][:shell] = data[-1]
468
- res[user][:home] = data[-2]
469
- res[user][:home_exists] = self.is_dir?(data[-2])
470
- res[user][:uid] = data[2]
471
- res[user][:gid] = data[3]
794
+ res[user] = Hash.new()
795
+ res[user][:shell] = shell
796
+ res[user][:home] = home
797
+ res[user][:home_exists] = home_exists
798
+ res[user][:uid] = uid
799
+ res[user][:gid] = gid
800
+ res[user][:source] = source
801
+ end
472
802
  end
473
803
 
474
804
  if cache
805
+ @logger.debug(sprintf('caching [users] at [%s]', Time.now.asctime))
475
806
  self.deltas[:users] = res
807
+ self.cache[:users] = Time.now.to_i
476
808
  end
477
809
 
478
810
  res