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.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/LICENSE +9 -0
- data/README.md +175 -0
- data/Rakefile +65 -0
- data/Vagrantfile +23 -0
- data/examples/bootstrap.rb +113 -0
- data/examples/demo.rb +71 -0
- data/examples/error.rb +30 -0
- data/lib/rouster.rb +737 -0
- data/lib/rouster/deltas.rb +481 -0
- data/lib/rouster/puppet.rb +398 -0
- data/lib/rouster/testing.rb +743 -0
- data/lib/rouster/tests.rb +596 -0
- data/path_helper.rb +21 -0
- data/rouster.gemspec +30 -0
- data/test/basic.rb +10 -0
- data/test/functional/deltas/test_get_crontab.rb +99 -0
- data/test/functional/deltas/test_get_groups.rb +48 -0
- data/test/functional/deltas/test_get_packages.rb +71 -0
- data/test/functional/deltas/test_get_ports.rb +119 -0
- data/test/functional/deltas/test_get_services.rb +43 -0
- data/test/functional/deltas/test_get_users.rb +45 -0
- data/test/functional/puppet/test_facter.rb +59 -0
- data/test/functional/test_caching.rb +124 -0
- data/test/functional/test_destroy.rb +51 -0
- data/test/functional/test_dirs.rb +88 -0
- data/test/functional/test_files.rb +64 -0
- data/test/functional/test_get.rb +76 -0
- data/test/functional/test_inspect.rb +31 -0
- data/test/functional/test_is_dir.rb +118 -0
- data/test/functional/test_is_file.rb +119 -0
- data/test/functional/test_new.rb +92 -0
- data/test/functional/test_put.rb +81 -0
- data/test/functional/test_rebuild.rb +49 -0
- data/test/functional/test_restart.rb +44 -0
- data/test/functional/test_run.rb +77 -0
- data/test/functional/test_status.rb +38 -0
- data/test/functional/test_suspend.rb +31 -0
- data/test/functional/test_up.rb +27 -0
- data/test/functional/test_validate_file.rb +30 -0
- data/test/puppet/manifests/default.pp +9 -0
- data/test/puppet/manifests/hiera.yaml +12 -0
- data/test/puppet/manifests/hieradata/common.json +3 -0
- data/test/puppet/manifests/hieradata/vagrant.json +3 -0
- data/test/puppet/manifests/manifest.pp +78 -0
- data/test/puppet/modules/role/manifests/ui.pp +5 -0
- data/test/puppet/test_apply.rb +149 -0
- data/test/puppet/test_roles.rb +186 -0
- data/test/tunnel_vs_scp.rb +41 -0
- data/test/unit/puppet/test_get_puppet_star.rb +68 -0
- data/test/unit/test_generate_unique_mac.rb +43 -0
- data/test/unit/test_new.rb +31 -0
- data/test/unit/test_parse_ls_string.rb +334 -0
- data/test/unit/test_traverse_up.rb +43 -0
- data/test/unit/testing/test_meets_constraint.rb +55 -0
- data/test/unit/testing/test_validate_file.rb +112 -0
- data/test/unit/testing/test_validate_group.rb +72 -0
- data/test/unit/testing/test_validate_package.rb +69 -0
- data/test/unit/testing/test_validate_port.rb +98 -0
- data/test/unit/testing/test_validate_service.rb +73 -0
- data/test/unit/testing/test_validate_user.rb +92 -0
- 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
|