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,481 @@
1
+ require sprintf('%s/../../%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
2
+
3
+ # deltas.rb - get information about groups, packages, services and users inside a Vagrant VM
4
+ require 'rouster'
5
+ require 'rouster/tests'
6
+
7
+ # TODO use @cache_timeout to invalidate data cached here
8
+
9
+ class Rouster
10
+
11
+ ##
12
+ # get_crontab
13
+ #
14
+ # runs `crontab -l <user>` and parses output, returns hash:
15
+ # {
16
+ # user => {
17
+ # logicalOrderInt => {
18
+ # :minute => minute,
19
+ # :hour => hour,
20
+ # :dom => dom, # day of month
21
+ # :mon => mon, # month
22
+ # :dow => dow, # day of week
23
+ # :command => command,
24
+ # }
25
+ # }
26
+ # }
27
+ #
28
+ # the hash will contain integers (not strings) for numerical values -- all but '*'
29
+ #
30
+ # parameters
31
+ # * <user> - name of user who owns crontab for examination -- or '*' to determine list of users and iterate over them to find all cron jobs
32
+ # * [cache] - boolean controlling whether or not retrieved/parsed data is cached, defaults to true
33
+ def get_crontab(user='root', cache=true)
34
+
35
+ if cache and self.deltas[:crontab].class.eql?(Hash)
36
+ if self.deltas[:crontab].has_key?(user)
37
+ return self.deltas[:crontab][user]
38
+ else
39
+ # noop fallthrough to gather data to cache
40
+ end
41
+ elsif cache and self.deltas[:crontab].class.eql?(Hash) and user.eql?('*')
42
+ return self.deltas[:crontab]
43
+ end
44
+
45
+ i = 0
46
+ res = Hash.new
47
+ users = nil
48
+
49
+ if user.eql?('*')
50
+ users = self.get_users().keys
51
+ else
52
+ users = [user]
53
+ end
54
+
55
+ users.each do |u|
56
+ begin
57
+ raw = self.run(sprintf('crontab -u %s -l', u))
58
+ rescue RemoteExecutionError => e
59
+ # crontab throws a non-0 exit code if there is no crontab for the specified user
60
+ res[u] ||= Hash.new
61
+ next
62
+ end
63
+
64
+ raw.split("\n").each do |line|
65
+ elements = line.split("\s")
66
+
67
+ 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
+
78
+ i += 1
79
+ end
80
+
81
+ if cache
82
+ if ! user.eql?('*')
83
+ self.deltas[:crontab] ||= Hash.new
84
+ self.deltas[:crontab][user] ||= Hash.new
85
+ self.deltas[:crontab][user] = res[user]
86
+ else
87
+ self.deltas[:crontab] ||= Hash.new
88
+ self.deltas[:crontab] = res
89
+ end
90
+ end
91
+
92
+ return user.eql?('*') ? res : res[user]
93
+ end
94
+
95
+ ##
96
+ # get_groups
97
+ #
98
+ # cats /etc/group and parses output, returns hash:
99
+ # {
100
+ # groupN => {
101
+ # :gid => gid,
102
+ # :users => [user1, userN]
103
+ # }
104
+ # }
105
+ #
106
+ # parameters
107
+ # * [cache] - boolean controlling whether data retrieved/parsed is cached, defaults to true
108
+ # * [deep] - boolean controlling whether get_users() is called in order to correctly populate res[group][:users]
109
+ def get_groups(cache=true, deep=true)
110
+ if cache and ! self.deltas[:groups].nil?
111
+ return self.deltas[:groups]
112
+ end
113
+
114
+ res = Hash.new()
115
+
116
+ raw = self.run('cat /etc/group')
117
+
118
+ raw.split("\n").each do |line|
119
+ next unless line.match(/\w+:\w+:\w+/)
120
+
121
+ data = line.split(':')
122
+
123
+ group = data[0]
124
+ gid = data[2]
125
+
126
+ # this works in some cases, deep functionality picks up the others
127
+ users = data[3].nil? ? ['NONE'] : data[3].split(',')
128
+
129
+ res[group] = Hash.new() # i miss autovivification
130
+ res[group][:gid] = gid
131
+ res[group][:users] = users
132
+ end
133
+
134
+ groups = res
135
+
136
+ if deep
137
+ users = self.get_users(cache)
138
+
139
+ # TODO better, much better -- since the number of users/groups is finite and usually small, this is a low priority
140
+ users.each_key do |user|
141
+ # iterate over each user to get their gid
142
+ gid = users[user][:gid]
143
+
144
+ groups.each_key do |group|
145
+ # iterate over each group to find the matching gid
146
+ if gid.eql?(groups[group][:gid])
147
+ if groups[group][:users].eql?(['NONE'])
148
+ groups[group][:users] = []
149
+ end
150
+ groups[group][:users] << user unless groups[group][:users].member?(user)
151
+ end
152
+
153
+ # TODO throw an error if we find a user with a GID that we don't know about from get_groups()
154
+ end
155
+
156
+ end
157
+ end
158
+
159
+ if cache
160
+ self.deltas[:groups] = groups
161
+ end
162
+
163
+ groups
164
+ end
165
+
166
+ ##
167
+ # get_packages
168
+ #
169
+ # runs an OS appropriate command to gather list of packages, returns hash:
170
+ # {
171
+ # packageN => {
172
+ # package => version|? # if 'deep', attempts to parse version numbers
173
+ # }
174
+ # }
175
+ #
176
+ # parameters
177
+ # * [cache] - boolean controlling whether data retrieved/parsed is cached, defaults to true
178
+ # * [deep] - boolean controlling whether to attempt to parse extended information (see supported OS), defaults to true
179
+ #
180
+ # supported OS
181
+ # * OSX - runs `pkgutil --pkgs` and `pkgutil --pkg-info=<package>` (if deep)
182
+ # * RedHat - runs `rpm -qa`
183
+ # * Solaris - runs `pkginfo` and `pkginfo -l <package>` (if deep)
184
+ # * Ubuntu - runs `dpkg --get-selections` and `dpkg -s <package>` (if deep)
185
+ #
186
+ # raises InternalError if unsupported operating system
187
+ def get_packages(cache=true, deep=true)
188
+ if cache and ! self.deltas[:packages].nil?
189
+ return self.deltas[:packages]
190
+ end
191
+
192
+ res = Hash.new()
193
+
194
+ os = self.os_type
195
+
196
+ if os.eql?(:osx)
197
+
198
+ raw = self.run('pkgutil --pkgs')
199
+ raw.split("\n").each do |line|
200
+ version = '?'
201
+
202
+ if deep
203
+ # 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+(.*?)$/)
206
+ end
207
+
208
+ res[line] = version
209
+ end
210
+
211
+ elsif os.eql?(:solaris)
212
+ raw = self.run('pkginfo')
213
+ raw.split("\n").each do |line|
214
+ next if line.match(/(.*?)\s+(.*?)\s(.*)$/).empty?
215
+ name = $2
216
+ version = '?'
217
+
218
+ if deep
219
+ local_res = self.run(sprintf('pkginfo -l %s', name))
220
+ version = $1 if local_res.match(/VERSION\:\s+(.*?)$/i)
221
+ end
222
+
223
+ res[name] = version
224
+ end
225
+
226
+ elsif os.eql?(:ubuntu) or os.eql?(:debian)
227
+ raw = self.run('dpkg --get-selections')
228
+ raw.split("\n").each do |line|
229
+ next if line.match(/^(.*?)\s/).nil?
230
+ 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(.*?)$/)
236
+ end
237
+
238
+ res[name] = version
239
+ end
240
+
241
+ elsif os.eql?(:redhat)
242
+ raw = self.run('rpm -qa')
243
+ 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
246
+ 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*)/)
253
+ end
254
+
255
+ res[name] = version
256
+ end
257
+
258
+ else
259
+ raise InternalError.new(sprintf('VM operating system[%s] not currently supported', os))
260
+ end
261
+
262
+ if cache
263
+ self.deltas[:packages] = res
264
+ end
265
+
266
+ res
267
+ end
268
+
269
+ ##
270
+ # get_ports
271
+ #
272
+ # runs an OS appropriate command to gather port information, returns hash:
273
+ # {
274
+ # protocolN => {
275
+ # portN => {
276
+ # :addressN => state
277
+ # }
278
+ # }
279
+ # }
280
+ #
281
+ # parameters
282
+ # * [cache] - boolean controlling whether data retrieved/parsed is cached, defaults to true
283
+ #
284
+ # supported OS
285
+ # * RedHat, Ubuntu - runs `netstat -ln`
286
+ #
287
+ # raises InternalError if unsupported operating system
288
+ def get_ports(cache=false)
289
+ # TODO add unix domain sockets
290
+ # TODO improve ipv6 support
291
+
292
+ if cache and ! self.deltas[:ports].nil?
293
+ return self.deltas[:ports]
294
+ end
295
+
296
+ res = Hash.new()
297
+ os = self.os_type()
298
+
299
+ if os.eql?(:redhat) or os.eql?(:ubuntu) or os.eql?(:debian)
300
+
301
+ raw = self.run('netstat -ln')
302
+
303
+ raw.split("\n").each do |line|
304
+
305
+ next unless line.match(/(\w+)\s+\d+\s+\d+\s+([\S\:]*)\:(\w*)\s.*?(\w+)\s/) or line.match(/(\w+)\s+\d+\s+\d+\s+([\S\:]*)\:(\w*)\s.*?(\w*)\s/)
306
+
307
+ protocol = $1
308
+ address = $2
309
+ port = $3
310
+ state = protocol.eql?('udp') ? 'you_might_not_get_it' : $4
311
+
312
+ res[protocol] = Hash.new if res[protocol].nil?
313
+ res[protocol][port] = Hash.new if res[protocol][port].nil?
314
+ res[protocol][port][:address] = Hash.new if res[protocol][port][:address].nil?
315
+ res[protocol][port][:address][address] = state
316
+
317
+ end
318
+ else
319
+ raise InternalError.new(sprintf('unable to get port information from VM operating system[%s]', os))
320
+ end
321
+
322
+ if cache
323
+ self.deltas[:ports] = res
324
+ end
325
+
326
+ res
327
+ end
328
+
329
+ ##
330
+ # get_services
331
+ #
332
+ # runs an OS appropriate command to gather service information, returns hash:
333
+ # {
334
+ # serviceN => mode # running|stopped|unsure
335
+ # }
336
+ #
337
+ # parameters
338
+ # * [cache] - boolean controlling whether data retrieved/parsed is cached, defaults to true
339
+ #
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`
345
+ #
346
+ # raises InternalError if unsupported operating system
347
+ def get_services(cache=true)
348
+ if cache and ! self.deltas[:services].nil?
349
+ return self.deltas[:services]
350
+ end
351
+
352
+ res = Hash.new()
353
+
354
+ os = self.os_type
355
+
356
+ if os.eql?(:osx)
357
+
358
+ raw = self.run('launchctl list')
359
+ raw.split("\n").each do |line|
360
+ next if line.match(/(?:\S*?)\s+(\S*?)\s+(\S*)$/).nil?
361
+
362
+ service = $2
363
+ mode = $1
364
+
365
+ if mode.match(/^\d/)
366
+ mode = 'running'
367
+ else
368
+ mode = 'stopped'
369
+ end
370
+
371
+ res[service] = mode
372
+ end
373
+
374
+ elsif os.eql?(:solaris)
375
+
376
+ raw = self.run('svcs')
377
+ raw.split("\n").each do |line|
378
+ next if line.match(/(.*?)\s+(?:.*?)\s+(.*?)$/).nil?
379
+
380
+ service = $2
381
+ mode = $1
382
+
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
+ end
390
+
391
+ res[service] = mode
392
+
393
+ end
394
+
395
+ elsif os.eql?(:ubuntu) or os.eql?(:debian)
396
+
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
402
+
403
+ mode = 'stopped' if mode.match('-')
404
+ mode = 'running' if mode.match('\+')
405
+ mode = 'unsure' if mode.match('\?')
406
+
407
+ res[service] = mode
408
+ end
409
+
410
+ elsif os.eql?(:redhat)
411
+
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
423
+ end
424
+
425
+ else
426
+ raise InternalError.new(sprintf('unable to get service information from VM operating system[%s]', os))
427
+ end
428
+
429
+ if cache
430
+ self.deltas[:services] = res
431
+ end
432
+
433
+ res
434
+ end
435
+
436
+ ##
437
+ # get_users
438
+ #
439
+ # cats /etc/passwd and parses output, returns hash:
440
+ # {
441
+ # userN => {
442
+ # :gid => gid,
443
+ # :home => path_of_homedir,
444
+ # :home_exists => boolean_of_is_dir?(:home),
445
+ # :shell => path_to_shell,
446
+ # :uid => uid
447
+ # }
448
+ # }
449
+ # parameters
450
+ # * [cache] - boolean controlling whether data retrieved/parsed is cached, defaults to true
451
+ def get_users(cache=true)
452
+ if cache and ! self.deltas[:users].nil?
453
+ return self.deltas[:users]
454
+ end
455
+
456
+ res = Hash.new()
457
+
458
+ raw = self.run('cat /etc/passwd')
459
+
460
+ raw.split("\n").each do |line|
461
+ next if line.match(/(\w+)(?::\w+){3,}/).nil?
462
+
463
+ user = $1
464
+ data = line.split(':')
465
+
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]
472
+ end
473
+
474
+ if cache
475
+ self.deltas[:users] = res
476
+ end
477
+
478
+ res
479
+ end
480
+
481
+ end