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.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/.reek +63 -0
- data/.travis.yml +11 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +102 -0
- data/README.md +233 -7
- data/Rakefile +52 -34
- data/Vagrantfile +26 -8
- data/examples/aws.rb +85 -0
- data/examples/openstack.rb +61 -0
- data/examples/passthrough.rb +71 -0
- data/lib/rouster.rb +380 -262
- data/lib/rouster/deltas.rb +470 -138
- data/lib/rouster/puppet.rb +155 -26
- data/lib/rouster/testing.rb +205 -46
- data/lib/rouster/tests.rb +40 -11
- data/lib/rouster/vagrant.rb +311 -0
- data/path_helper.rb +3 -4
- data/plugins/aws.rb +347 -0
- data/plugins/openstack.rb +136 -0
- data/test/basic.rb +4 -1
- data/test/functional/deltas/test_get_crontab.rb +64 -2
- data/test/functional/deltas/test_get_groups.rb +74 -2
- data/test/functional/deltas/test_get_os.rb +68 -0
- data/test/functional/deltas/test_get_packages.rb +73 -6
- data/test/functional/deltas/test_get_ports.rb +26 -1
- data/test/functional/deltas/test_get_services.rb +43 -5
- data/test/functional/deltas/test_get_users.rb +35 -2
- data/test/functional/puppet/test_facter.rb +41 -1
- data/test/functional/test_caching.rb +2 -2
- data/test/functional/test_inspect.rb +1 -1
- data/test/functional/test_is_file.rb +17 -1
- data/test/functional/test_is_in_file.rb +40 -0
- data/test/functional/test_new.rb +233 -22
- data/test/functional/test_passthroughs.rb +94 -0
- data/test/functional/test_put.rb +2 -2
- data/test/functional/test_validate_file.rb +104 -3
- data/test/puppet/test_apply.rb +8 -6
- data/test/unit/puppet/resources/puppet_run_with_failed_exec +59 -0
- data/test/unit/puppet/resources/puppet_run_with_successful_exec +61 -0
- data/test/unit/puppet/test_get_puppet_star.rb +27 -4
- data/test/unit/puppet/test_puppet_parsing.rb +44 -0
- data/test/unit/test_new.rb +88 -0
- data/test/unit/test_parse_ls_string.rb +67 -0
- data/test/unit/testing/resources/osx-launchd +285 -0
- data/test/unit/testing/resources/rhel-systemd +46 -0
- data/test/unit/testing/resources/rhel-systemv +41 -0
- data/test/unit/testing/resources/rhel-upstart +20 -0
- data/test/unit/testing/test_get_services.rb +178 -0
- data/test/unit/testing/test_validate_cron.rb +78 -0
- data/test/unit/testing/test_validate_package.rb +36 -10
- data/test/unit/testing/test_validate_port.rb +5 -0
- metadata +42 -21
- data/test/puppet/test_roles.rb +0 -186
data/lib/rouster/deltas.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
152
|
+
raw.split("\n").each do |line|
|
|
153
|
+
next unless line.match(/\w+:\w+:\w+/)
|
|
120
154
|
|
|
121
|
-
|
|
155
|
+
data = line.split(':')
|
|
122
156
|
|
|
123
|
-
|
|
124
|
-
|
|
157
|
+
group = data[0]
|
|
158
|
+
gid = data[2]
|
|
125
159
|
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
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
|
-
|
|
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',
|
|
205
|
-
version
|
|
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(.*)$/).
|
|
283
|
+
next if line.match(/(.*?)\s+(.*?)\s(.*)$/).nil?
|
|
215
284
|
name = $2
|
|
285
|
+
arch = '?'
|
|
216
286
|
version = '?'
|
|
217
287
|
|
|
218
288
|
if deep
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
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(
|
|
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(
|
|
313
|
+
next if line.match(/(.*?)\@(.*?)\@(.*)/).nil?
|
|
230
314
|
name = $1
|
|
231
|
-
version =
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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?(:
|
|
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(/(.*?)
|
|
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 =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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?(:
|
|
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]
|
|
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
|
|
342
|
-
# * RedHat
|
|
343
|
-
# * Solaris -
|
|
344
|
-
# * Ubuntu
|
|
443
|
+
# supported OS and types
|
|
444
|
+
# * OSX - :launchd
|
|
445
|
+
# * RedHat - :systemv or :upstart
|
|
446
|
+
# * Solaris - :smf
|
|
447
|
+
# * Ubuntu - :systemv or :upstart
|
|
345
448
|
#
|
|
346
|
-
#
|
|
347
|
-
|
|
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
|
-
|
|
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
|
-
|
|
501
|
+
type = type.class.eql?(Array) ? type : [ type ]
|
|
355
502
|
|
|
356
|
-
|
|
503
|
+
type.each do |provider|
|
|
357
504
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
363
|
-
mode = $1
|
|
513
|
+
@logger.info(sprintf('get_services using provider [%s] on [%s]', provider, os))
|
|
364
514
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
372
|
-
end
|
|
542
|
+
elsif os.eql?(:solaris)
|
|
373
543
|
|
|
374
|
-
|
|
544
|
+
raw.split("\n").each do |line|
|
|
545
|
+
next if line.match(/(.*?)\s+(?:.*?)\s+(.*?)$/).nil?
|
|
375
546
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
next if line.match(/(.*?)\s+(?:.*?)\s+(.*?)$/).nil?
|
|
547
|
+
service = $2
|
|
548
|
+
mode = $1
|
|
379
549
|
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
769
|
+
{
|
|
770
|
+
:file => self.run('cat /etc/passwd'),
|
|
771
|
+
:dynamic => self.run('getent passwd', [0,127]),
|
|
772
|
+
}.each do |source, raw|
|
|
462
773
|
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|