rouster 0.42 → 0.53

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5d74bfb451922e3b85675d75df8eab08adcbb8f1
4
- data.tar.gz: 8e16766740fb82932308c8e9ca1ff44e2ece3c70
3
+ metadata.gz: 76526d9f9fbaca98b3ea2bfad51a769f67df6647
4
+ data.tar.gz: 6f9bc1a95bec7be30e00eccb1cd0ca28c6d24a72
5
5
  SHA512:
6
- metadata.gz: b5149f6ecef16db649b445602383708dc7a2f39b30dc8475edf408286dbe4c60105828af134d1a177e1c5a960e5e4515786798fe31fff351c9c1de604af1847c
7
- data.tar.gz: c4b51b717eab079455f3cac15eab60d93c49403b8efe59c1886b0f300e8231143110140d99d3f62bac439cd792768386a5ddbc541a3f2aaa402f3e937feecbc9
6
+ metadata.gz: 345f5b6e4ef69a8a023e4bf3aebbb517e69a0ff7eee1f95b330bb1c373becfa1c231931f5c189099ec44acc355efe5a23b1c07cf057dc9cd6a297c7651343391
7
+ data.tar.gz: 92be5fcb79d74870bea301a722a539459656a89b9a7c139dba1c02e5fcbff5d12207f6dab4720952be5e58ab6606e27781885dfea29ffef1a117de757e713e56
data/.gitignore CHANGED
@@ -4,3 +4,4 @@ doc/*
4
4
  *.swp
5
5
  *tmp*
6
6
  *.gem
7
+ .rakeTasks
data/Rakefile CHANGED
@@ -24,6 +24,10 @@ task :examples do
24
24
  end
25
25
  end
26
26
 
27
+ task :reek do
28
+ sh "reek lib/**/*.rb"
29
+ end
30
+
27
31
  Rake::TestTask.new do |t|
28
32
  t.name = 'test'
29
33
  t.libs << 'lib'
@@ -44,3 +48,18 @@ Rake::TestTask.new do |t|
44
48
  t.test_files = FileList['test/functional/**/test_*.rb']
45
49
  t.verbose = true
46
50
  end
51
+
52
+ Rake::TestTask.new do |t|
53
+ t.name = 'deltas'
54
+ t.libs << 'lib'
55
+ t.test_files = FileList['test/functional/deltas/test_*.rb']
56
+ t.verbose = true
57
+ end
58
+
59
+ Rake::TestTask.new do |t|
60
+ t.name = 'puppet'
61
+ t.libs << 'lib'
62
+ t.test_files = FileList['test/puppet/test*.rb']
63
+ t.verbose = true
64
+ end
65
+
data/Vagrantfile CHANGED
@@ -1,7 +1,10 @@
1
1
  # stripped down example piab Vagrantfile for rouster
2
2
 
3
+ #box_url = 'http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210.box'
4
+ #box_name = 'ubuntu12'
3
5
  box_url = 'http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210.box'
4
6
  box_name = 'centos6'
7
+
5
8
  boxes = [:ppm, :app]
6
9
 
7
10
  Vagrant::Config.run do |config|
@@ -11,7 +14,7 @@ Vagrant::Config.run do |config|
11
14
  worker.vm.box = box_name
12
15
  worker.vm.box_url = box_url
13
16
  worker.vm.host_name = box.to_s
14
- worker.vm.network :hostonly, sprintf('10.0.1.%s', rand(254))
17
+ worker.vm.network :hostonly, sprintf('10.0.1.%s', rand(253).to_i + 2)
15
18
  worker.ssh.forward_agent = true
16
19
 
17
20
  if box.to_s.eql?('ppm') and File.directory?('../puppet')
data/lib/rouster.rb CHANGED
@@ -11,9 +11,10 @@ require 'rouster/tests'
11
11
  class Rouster
12
12
 
13
13
  # sporadically updated version number
14
- VERSION = 0.42
14
+ VERSION = 0.53
15
15
 
16
16
  # custom exceptions -- what else do we want them to include/do?
17
+ class ArgumentError < StandardError; end # thrown by methods that take parameters from users
17
18
  class FileTransferError < StandardError; end # thrown by get() and put()
18
19
  class InternalError < StandardError; end # thrown by most (if not all) Rouster methods
19
20
  class ExternalError < StandardError; end # thrown when external dependencies do not respond as expected
@@ -60,7 +61,8 @@ class Rouster
60
61
  @deltas = Hash.new
61
62
 
62
63
  @exitcode = nil
63
- @ssh_info = nil # will be hash containing connection information
64
+ @ssh = nil # hash containing the SSH connection object
65
+ @ssh_info = nil # hash containing connection information
64
66
 
65
67
  # set up logging
66
68
  require 'log4r/config'
@@ -75,17 +77,23 @@ class Rouster
75
77
  raise InternalError.new(sprintf('specified Vagrantfile [%s] does not exist', @vagrantfile))
76
78
  end
77
79
 
78
- raise InternalError.new() if @name.nil?
80
+ raise InternalError.new('name of Vagrant VM not specified') if @name.nil?
79
81
 
80
82
  return if opts[:unittest].eql?(true) # quick return if we're a unit test
81
83
 
84
+ # this is breaking test/functional/test_caching.rb test_ssh_caching (if the VM was not running when the test started)
85
+ # it slows down object instantiation, but is a good test to ensure the machine name is valid..
82
86
  begin
83
87
  self.status()
84
- rescue Rouster::LocalExecutionError
85
- raise InternalError.new()
88
+ rescue Rouster::LocalExecutionError => e
89
+ raise InternalError.new(sprintf('caught non-0 exitcode from status(): %s', e.message))
86
90
  end
87
91
 
88
- ## TODO ensure 'vagrant' is in path
92
+ begin
93
+ self._run('which vagrant')
94
+ rescue
95
+ raise ExternalError.new('vagrant not found in path')
96
+ end
89
97
 
90
98
  @log.debug('SSH key discovery and viability tests..')
91
99
  if @sshkey.nil?
@@ -107,12 +115,7 @@ class Rouster
107
115
  end
108
116
 
109
117
  if @sshtunnel
110
- unless self.status.eql?('running')
111
- @log.info(sprintf('upping machine[%s] in order to open SSH tunnel', @name))
112
- self.up()
113
- end
114
-
115
- self.connect_ssh_tunnel()
118
+ self.up()
116
119
  end
117
120
 
118
121
  @log.info('Rouster object successfully instantiated')
@@ -142,7 +145,7 @@ class Rouster
142
145
  # if :sshtunnel is passed to the object during instantiation, the tunnel is created here as well
143
146
  def up
144
147
  @log.info('up()')
145
- self._run(sprintf('cd %s; vagrant up %s', File.dirname(@vagrantfile), @name))
148
+ self.vagrant(sprintf('up %s', @name))
146
149
 
147
150
  @ssh_info = nil # in case the ssh-info has changed, a la destroy/rebuild
148
151
  self.connect_ssh_tunnel() if @sshtunnel
@@ -153,7 +156,8 @@ class Rouster
153
156
  # runs `vagrant destroy <name>` from the Vagrantfile path
154
157
  def destroy
155
158
  @log.info('destroy()')
156
- self._run(sprintf('cd %s; vagrant destroy -f %s', File.dirname(@vagrantfile), @name))
159
+ disconnect_ssh_tunnel
160
+ self.vagrant(sprintf('destroy -f %s', @name))
157
161
  end
158
162
 
159
163
  ##
@@ -174,11 +178,14 @@ class Rouster
174
178
  end
175
179
 
176
180
  @log.info('status()')
177
- self._run(sprintf('cd %s; vagrant status %s', File.dirname(@vagrantfile), @name))
181
+ self.vagrant(sprintf('status %s', @name))
178
182
 
179
183
  # else case here is handled by non-0 exit code
180
- if self.get_output().match(/^#{@name}\s*(.*\s?\w+)\s(.+)$/)
181
- # $1 = name, $2 = provider
184
+ if self.get_output().match(/^#{@name}\s*(.*\s?\w+)\s\((.+)\)$/)
185
+ # vagrant 1.2+, $1 = status, $2 = provider
186
+ status = $1
187
+ elsif self.get_output().match(/^#{@name}\s+(.+)$/)
188
+ # vagrant 1.2-, $1 = status
182
189
  status = $1
183
190
  end
184
191
 
@@ -198,7 +205,8 @@ class Rouster
198
205
  # runs `vagrant suspend <name>` from the Vagrantfile path
199
206
  def suspend
200
207
  @log.info('suspend()')
201
- self._run(sprintf('cd %s; vagrant suspend %s', File.dirname(@vagrantfile), @name))
208
+ disconnect_ssh_tunnel()
209
+ self.vagrant(sprintf('suspend %s', @name))
202
210
  end
203
211
 
204
212
  ## internal methods
@@ -293,6 +301,80 @@ class Rouster
293
301
  res
294
302
  end
295
303
 
304
+ ##
305
+ # sandbox_available?
306
+ #
307
+ # returns true or false after attempting to find out if the sandbox
308
+ # subcommand is available
309
+ def sandbox_available?
310
+ if @cache.has_key?(:sandbox_available?)
311
+ @log.debug(sprintf('using cached sandbox_available?[%s]', @cache[:sandbox_available?]))
312
+ return @cache[:sandbox_available?]
313
+ end
314
+
315
+ @log.info('sandbox_available()')
316
+ self._run(sprintf('cd %s; vagrant', File.dirname(@vagrantfile))) # calling 'vagrant' without parameters to determine available faces
317
+
318
+ sandbox_available = false
319
+ if self.get_output().match(/^\s+sandbox$/)
320
+ sandbox_available = true
321
+ end
322
+
323
+ @cache[:sandbox_available?] = sandbox_available
324
+ @log.debug(sprintf('caching sandbox_available?[%s]', @cache[:sandbox_available?]))
325
+ @log.error('sandbox support is not available, please install the "sahara" gem first, https://github.com/jedi4ever/sahara') unless sandbox_available
326
+
327
+ return sandbox_available
328
+ end
329
+
330
+ ##
331
+ # sandbox_on
332
+ # runs `vagrant sandbox on` from the Vagrantfile path
333
+ def sandbox_on
334
+ if self.sandbox_available?
335
+ return self.vagrant(sprintf('sandbox on %s', @name))
336
+ else
337
+ raise ExternalError.new('sandbox plugin not installed')
338
+ end
339
+ end
340
+
341
+ ##
342
+ # sandbox_off
343
+ # runs `vagrant sandbox off` from the Vagrantfile path
344
+ def sandbox_off
345
+ if self.sandbox_available?
346
+ return self.vagrant(sprintf('sandbox off %s', @name))
347
+ else
348
+ raise ExternalError.new('sandbox plugin not installed')
349
+ end
350
+ end
351
+
352
+ ##
353
+ # sandbox_rollback
354
+ # runs `vagrant sandbox rollback` from the Vagrantfile path
355
+ def sandbox_rollback
356
+ if self.sandbox_available?
357
+ self.disconnect_ssh_tunnel
358
+ self.vagrant(sprintf('sandbox rollback %s', @name))
359
+ self.connect_ssh_tunnel
360
+ else
361
+ raise ExternalError.new('sandbox plugin not installed')
362
+ end
363
+ end
364
+
365
+ ##
366
+ # sandbox_commit
367
+ # runs `vagrant sandbox commit` from the Vagrantfile path
368
+ def sandbox_commit
369
+ if self.sandbox_available?
370
+ self.disconnect_ssh_tunnel
371
+ self.vagrant(sprintf('sandbox commit %s', @name))
372
+ self.connect_ssh_tunnel
373
+ else
374
+ raise ExternalError.new('sandbox plugin not installed')
375
+ end
376
+ end
377
+
296
378
  ##
297
379
  # get_ssh_info
298
380
  #
@@ -304,10 +386,11 @@ class Rouster
304
386
  h = Hash.new()
305
387
 
306
388
  if @ssh_info.class.eql?(Hash)
389
+ @log.debug('using cached SSH info')
307
390
  h = @ssh_info
308
391
  else
309
392
 
310
- res = self._run(sprintf('cd %s; vagrant ssh-config %s', File.dirname(@vagrantfile), @name))
393
+ res = self.vagrant(sprintf('ssh-config %s', @name))
311
394
 
312
395
  res.split("\n").each do |line|
313
396
  if line.match(/HostName (.*?)$/)
@@ -317,8 +400,14 @@ class Rouster
317
400
  elsif line.match(/Port (\d*?)$/)
318
401
  h[:ssh_port] = $1
319
402
  elsif line.match(/IdentityFile (.*?)$/)
320
- # TODO what to do if the user has specified @sshkey ?
321
- h[:identity_file] = $1
403
+ key = $1
404
+ unless @sshkey.eql?(key)
405
+ h[:identity_file] = key
406
+ else
407
+ @log.info(sprintf('using specified key[%s] instead of discovered key[%s]', @sshkey, key))
408
+ h[:identity_file] = @sshkey
409
+ end
410
+
322
411
  end
323
412
  end
324
413
 
@@ -337,16 +426,29 @@ class Rouster
337
426
  def connect_ssh_tunnel
338
427
  @log.debug('opening SSH tunnel..')
339
428
 
340
- if self.status.eql?('running')
429
+ status = self.status()
430
+ if status.eql?('running')
341
431
  self.get_ssh_info()
342
432
  @ssh = Net::SSH.start(@ssh_info[:hostname], @ssh_info[:user], :port => @ssh_info[:ssh_port], :keys => [@sshkey], :paranoid => false)
343
433
  else
344
- raise InternalError.new('VM is not running, unable open SSH tunnel')
434
+ raise InternalError.new(sprintf('VM is not running[%s], unable open SSH tunnel', status))
345
435
  end
346
436
 
347
437
  @ssh
348
438
  end
349
439
 
440
+ ##
441
+ # disconnect_ssh_tunnel
442
+ #
443
+ # shuts down the persistent Net::SSH tunnel
444
+ #
445
+ def disconnect_ssh_tunnel
446
+ @log.debug('closing SSH tunnel..')
447
+
448
+ @ssh.shutdown! unless @ssh.nil?
449
+ @ssh = nil
450
+ end
451
+
350
452
  ##
351
453
  # os_type
352
454
  #
@@ -367,6 +469,8 @@ class Rouster
367
469
  res =:solaris
368
470
  when /Ubuntu/i
369
471
  res = :ubuntu
472
+ when /Debian/i
473
+ res = :debian
370
474
  else
371
475
  if self.is_file?('/etc/redhat-release')
372
476
  res = :redhat
@@ -389,20 +493,21 @@ class Rouster
389
493
  # * [local_file] - full or relative path (based on $PWD) of file to download to
390
494
  #
391
495
  # if no local_file is specified, will be downloaded to $PWD with the same shortname as it had in the VM
496
+ #
497
+ # returns true on successful download, false if the file DNE and raises a FileTransferError.. well, you know
392
498
  def get(remote_file, local_file=nil)
393
499
  # TODO what happens when we pass a wildcard as remote_file?
394
500
 
395
501
  local_file = local_file.nil? ? File.basename(remote_file) : local_file
396
502
  @log.debug(sprintf('scp from VM[%s] to host[%s]', remote_file, local_file))
397
503
 
398
- # TODO should we do a self.file?(remote_file) test before trying to download?
399
-
400
504
  begin
401
505
  @ssh.scp.download!(remote_file, local_file)
402
506
  rescue => e
403
507
  raise FileTransferError.new(sprintf('unable to get[%s], exception[%s]', remote_file, e.message()))
404
508
  end
405
509
 
510
+ return true
406
511
  end
407
512
 
408
513
  ##
@@ -471,7 +576,7 @@ class Rouster
471
576
  case os_type
472
577
  when :osx
473
578
  self.run('shutdown -r now')
474
- when :redhat, :ubuntu
579
+ when :redhat, :ubuntu, :debian
475
580
  self.run('/sbin/shutdown -rf now')
476
581
  when :solaris
477
582
  self.run('shutdown -y -i5 -g0')
@@ -479,18 +584,20 @@ class Rouster
479
584
  raise InternalError.new(sprintf('unsupported OS[%s]', @ostype))
480
585
  end
481
586
 
587
+ @ssh, @ssh_info = nil # severing the SSH tunnel, getting ready in case this box is brought back up on a different port
588
+
482
589
  if wait
483
590
  inc = wait.to_i / 10
484
591
  0..wait.each do |e|
485
592
  @log.debug(sprintf('waiting for reboot: round[%s], step[%s], total[%s]', e, inc, wait))
486
- true if self.is_available_via_ssh?()
593
+ return true if self.is_available_via_ssh?()
487
594
  sleep inc
488
595
  end
489
596
 
490
- false
597
+ return false
491
598
  end
492
599
 
493
- @ssh, @ssh_info = nil, nil
600
+ return true
494
601
  end
495
602
 
496
603
  ##
@@ -504,7 +611,7 @@ class Rouster
504
611
  # parameters
505
612
  # * <command> - command to be run
506
613
  def _run(command)
507
- tmp_file = sprintf('/tmp/rouster.%s.%s', Time.now.to_i, $$)
614
+ tmp_file = sprintf('/tmp/rouster-cmd_output.%s.%s', Time.now.to_i, $$)
508
615
  cmd = sprintf('%s > %s 2> %s', command, tmp_file, tmp_file) # this is a holdover from Salesforce::Vagrant, can we use '2&>1' here?
509
616
  res = `#{cmd}` # what does this actually hold?
510
617
 
@@ -524,6 +631,22 @@ class Rouster
524
631
  output
525
632
  end
526
633
 
634
+ ##
635
+ # vagrant
636
+ #
637
+ # abstraction layer to call vagrant faces
638
+ #
639
+ # parameters
640
+ # * <face> - vagrant face to call (include arguments)
641
+ def vagrant(face)
642
+ if self.is_passthrough?
643
+ @log.info(sprintf('calling [vagrant %s] on a passthrough host is a noop', face))
644
+ return nil
645
+ end
646
+
647
+ self._run(sprintf('cd %s; vagrant %s', File.dirname(@vagrantfile), face))
648
+ end
649
+
527
650
  ##
528
651
  # get_output
529
652
  #
@@ -561,7 +684,6 @@ class Rouster
561
684
  # * [filename] - filename you are looking for
562
685
  # * [levels] - number of directory levels to examine, default is 10
563
686
  def traverse_up(startdir=Dir.pwd, filename=nil, levels=10)
564
- # TODO not sure this signature is exactly right..
565
687
  raise InternalError.new('must specify a filename') if filename.nil?
566
688
 
567
689
  @log.debug(sprintf('traverse_up() looking for [%s] in [%s], up to [%s] levels', filename, startdir, levels)) unless @log.nil?
@@ -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
  ##
@@ -33,13 +31,22 @@ class Rouster
33
31
  def get_crontab(user='root', cache=true)
34
32
 
35
33
  if cache and self.deltas[:crontab].class.eql?(Hash)
36
- if self.deltas[:crontab].has_key?(user)
34
+
35
+ if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:crontab]) > self.cache_timeout
36
+ @log.debug(sprintf('invalidating [crontab] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:crontab]), self.cache_timeout))
37
+ self.deltas.delete(:crontab)
38
+ end
39
+
40
+ if self.deltas.has_key?(:crontab) and self.deltas[:crontab].has_key?(user)
41
+ @log.debug(sprintf('using cached [crontab] from [%s]', self.cache[:crontab]))
37
42
  return self.deltas[:crontab][user]
43
+ elsif self.deltas.has_key?(:crontab) and user.eql?('*')
44
+ @log.debug(sprintf('using cached [crontab] from [%s]', self.cache[:crontab]))
45
+ return self.deltas[:crontab]
38
46
  else
39
47
  # noop fallthrough to gather data to cache
40
48
  end
41
- elsif cache and self.deltas[:crontab].class.eql?(Hash) and user.eql?('*')
42
- return self.deltas[:crontab]
49
+
43
50
  end
44
51
 
45
52
  i = 0
@@ -62,6 +69,7 @@ class Rouster
62
69
  end
63
70
 
64
71
  raw.split("\n").each do |line|
72
+ next if line.match(/^#/)
65
73
  elements = line.split("\s")
66
74
 
67
75
  res[u] ||= Hash.new
@@ -72,13 +80,15 @@ class Rouster
72
80
  res[u][i][:dom] = elements[2]
73
81
  res[u][i][:mon] = elements[3]
74
82
  res[u][i][:dow] = elements[4]
75
- res[u][i][:command] = elements[5..elements.size].join(" ")
83
+ res[u][i][:command] = elements[5..elements.size].join(' ')
76
84
  end
77
85
 
78
86
  i += 1
79
87
  end
80
88
 
81
89
  if cache
90
+ @log.debug(sprintf('caching [crontab] at [%s]', Time.now.asctime))
91
+
82
92
  if ! user.eql?('*')
83
93
  self.deltas[:crontab] ||= Hash.new
84
94
  self.deltas[:crontab][user] ||= Hash.new
@@ -87,6 +97,9 @@ class Rouster
87
97
  self.deltas[:crontab] ||= Hash.new
88
98
  self.deltas[:crontab] = res
89
99
  end
100
+
101
+ self.cache[:crontab] = Time.now.to_i
102
+
90
103
  end
91
104
 
92
105
  return user.eql?('*') ? res : res[user]
@@ -105,9 +118,19 @@ class Rouster
105
118
  #
106
119
  # parameters
107
120
  # * [cache] - boolean controlling whether data retrieved/parsed is cached, defaults to true
108
- def get_groups(cache=true)
121
+ # * [deep] - boolean controlling whether get_users() is called in order to correctly populate res[group][:users]
122
+ def get_groups(cache=true, deep=true)
123
+
109
124
  if cache and ! self.deltas[:groups].nil?
110
- return self.deltas[:groups]
125
+
126
+ if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:groups]) > self.cache_timeout
127
+ @log.debug(sprintf('invalidating [groups] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:groups]), self.cache_timeout))
128
+ self.deltas.delete(:groups)
129
+ else
130
+ @log.debug(sprintf('using cached [groups] from [%s]', self.cache[:groups]))
131
+ return self.deltas[:groups]
132
+ end
133
+
111
134
  end
112
135
 
113
136
  res = Hash.new()
@@ -121,6 +144,8 @@ class Rouster
121
144
 
122
145
  group = data[0]
123
146
  gid = data[2]
147
+
148
+ # this works in some cases, deep functionality picks up the others
124
149
  users = data[3].nil? ? ['NONE'] : data[3].split(',')
125
150
 
126
151
  res[group] = Hash.new() # i miss autovivification
@@ -128,11 +153,45 @@ class Rouster
128
153
  res[group][:users] = users
129
154
  end
130
155
 
156
+ groups = res
157
+
158
+ if deep
159
+ users = self.get_users(cache)
160
+
161
+ known_valid_gids = groups.keys.map { |g| groups[g][:gid] } # no need to calculate this in every loop
162
+
163
+ # TODO better, much better -- since the number of users/groups is finite and usually small, this is a low priority
164
+ users.each_key do |user|
165
+ # iterate over each user to get their gid
166
+ gid = users[user][:gid]
167
+
168
+ unless known_valid_gids.member?(gid)
169
+ @log.warn(sprintf('found user[%s] with unknown GID[%s], known GIDs[%s]', user, gid, known_valid_gids))
170
+ next
171
+ end
172
+
173
+ ## do this more efficiently
174
+ groups.each_key do |group|
175
+ # iterate over each group to find the matching gid
176
+ if gid.eql?(groups[group][:gid])
177
+ if groups[group][:users].eql?(['NONE'])
178
+ groups[group][:users] = []
179
+ end
180
+ groups[group][:users] << user unless groups[group][:users].member?(user)
181
+ end
182
+
183
+ end
184
+
185
+ end
186
+ end
187
+
131
188
  if cache
132
- self.deltas[:groups] = res
189
+ @log.debug(sprintf('caching [groups] at [%s]', Time.now.asctime))
190
+ self.deltas[:groups] = groups
191
+ self.cache[:groups] = Time.now.to_i
133
192
  end
134
193
 
135
- res
194
+ groups
136
195
  end
137
196
 
138
197
  ##
@@ -158,7 +217,15 @@ class Rouster
158
217
  # raises InternalError if unsupported operating system
159
218
  def get_packages(cache=true, deep=true)
160
219
  if cache and ! self.deltas[:packages].nil?
161
- return self.deltas[:packages]
220
+
221
+ if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:packages]) > self.cache_timeout
222
+ @log.debug(sprintf('invalidating [packages] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:packages]), self.cache_timeout))
223
+ self.deltas.delete(:packages)
224
+ else
225
+ @log.debug(sprintf('using cached [packages] from [%s]', self.cache[:packages]))
226
+ return self.deltas[:packages]
227
+ end
228
+
162
229
  end
163
230
 
164
231
  res = Hash.new()
@@ -169,53 +236,62 @@ class Rouster
169
236
 
170
237
  raw = self.run('pkgutil --pkgs')
171
238
  raw.split("\n").each do |line|
239
+ version = '?'
172
240
 
173
241
  if deep
174
242
  # can get install time, volume and location as well
175
243
  local_res = self.run(sprintf('pkgutil --pkg-info=%s', line))
176
- local = $1 if local_res.match(/version\:\s+(.*?)$/)
177
- else
178
- local = '?'
244
+ version = $1 if local_res.match(/version\:\s+(.*?)$/)
179
245
  end
180
246
 
181
- res[line] = local
247
+ res[line] = version
182
248
  end
183
249
 
184
250
  elsif os.eql?(:solaris)
185
251
  raw = self.run('pkginfo')
186
252
  raw.split("\n").each do |line|
187
253
  next if line.match(/(.*?)\s+(.*?)\s(.*)$/).empty?
254
+ name = $2
255
+ version = '?'
188
256
 
189
257
  if deep
190
- local_res = self.run(sprintf('pkginfo -l %s', $2))
191
- local = $1 if local_res.match(/VERSION\:\s+(.*?)$/i)
192
- else
193
- local = '?'
258
+ local_res = self.run(sprintf('pkginfo -l %s', name))
259
+ version = $1 if local_res.match(/VERSION\:\s+(.*?)$/i)
194
260
  end
195
261
 
196
- res[$2] = local
262
+ res[name] = version
197
263
  end
198
264
 
199
- elsif os.eql?(:ubuntu)
265
+ elsif os.eql?(:ubuntu) or os.eql?(:debian)
200
266
  raw = self.run('dpkg --get-selections')
201
267
  raw.split("\n").each do |line|
202
268
  next if line.match(/^(.*?)\s/).nil?
269
+ name = $1
270
+ version = '?'
203
271
 
204
272
  if deep
205
- local_res = self.run(sprintf('dpkg -s %s', $1))
206
- local = $1 if local_res.match(/Version\:\s(.*?)$/)
207
- else
208
- local = '?'
273
+ local_res = self.run(sprintf('dpkg -s %s', name))
274
+ version = $1 if local_res.match(/Version\:\s(.*?)$/)
209
275
  end
210
276
 
211
- res[$1] = local
277
+ res[name] = version
212
278
  end
213
279
 
214
280
  elsif os.eql?(:redhat)
215
281
  raw = self.run('rpm -qa')
216
282
  raw.split("\n").each do |line|
217
283
  next if line.match(/(.*?)-(\d*\..*)/).nil? # ht petersen.allen
218
- res[$1] = $2
284
+ #next if line.match(/(.*)-(\d+\.\d+.*)/).nil? # another alternate, but still not perfect
285
+ name = $1
286
+ version = '?' # we could use $2, but we don't trust it
287
+
288
+ if deep
289
+ local_res = self.run(sprintf('rpm -qi %s', line))
290
+ name = $1 if local_res.match(/Name\s+:\s(\S*)/)
291
+ version = $1 if local_res.match(/Version\s+:\s(\S*)/)
292
+ end
293
+
294
+ res[name] = version
219
295
  end
220
296
 
221
297
  else
@@ -223,7 +299,9 @@ class Rouster
223
299
  end
224
300
 
225
301
  if cache
302
+ @log.debug(sprintf('caching [packages] at [%s]', Time.now.asctime))
226
303
  self.deltas[:packages] = res
304
+ self.cache[:packages] = Time.now.to_i
227
305
  end
228
306
 
229
307
  res
@@ -245,7 +323,7 @@ class Rouster
245
323
  # * [cache] - boolean controlling whether data retrieved/parsed is cached, defaults to true
246
324
  #
247
325
  # supported OS
248
- # * RedHat - runs `netstat -ln` -- TODO will this work on other operating systems too?
326
+ # * RedHat, Ubuntu - runs `netstat -ln`
249
327
  #
250
328
  # raises InternalError if unsupported operating system
251
329
  def get_ports(cache=false)
@@ -253,13 +331,19 @@ class Rouster
253
331
  # TODO improve ipv6 support
254
332
 
255
333
  if cache and ! self.deltas[:ports].nil?
256
- return self.deltas[:ports]
334
+ if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:ports]) > self.cache_timeout
335
+ @log.debug(sprintf('invalidating [ports] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:ports]), self.cache_timeout))
336
+ self.deltas.delete(:ports)
337
+ else
338
+ @log.debug(sprintf('using cached [ports] from [%s]', self.cache[:ports]))
339
+ return self.deltas[:ports]
340
+ end
257
341
  end
258
342
 
259
343
  res = Hash.new()
260
344
  os = self.os_type()
261
345
 
262
- if os.eql?(:redhat)
346
+ if os.eql?(:redhat) or os.eql?(:ubuntu) or os.eql?(:debian)
263
347
 
264
348
  raw = self.run('netstat -ln')
265
349
 
@@ -278,13 +362,14 @@ class Rouster
278
362
  res[protocol][port][:address][address] = state
279
363
 
280
364
  end
281
-
282
365
  else
283
366
  raise InternalError.new(sprintf('unable to get port information from VM operating system[%s]', os))
284
367
  end
285
368
 
286
369
  if cache
370
+ @log.debug(sprintf('caching [ports] at [%s]', Time.now.asctime))
287
371
  self.deltas[:ports] = res
372
+ self.cache[:ports] = Time.now.to_i
288
373
  end
289
374
 
290
375
  res
@@ -295,11 +380,12 @@ class Rouster
295
380
  #
296
381
  # runs an OS appropriate command to gather service information, returns hash:
297
382
  # {
298
- # serviceN => mode # running|stopped|unsure
383
+ # serviceN => mode # exists|installed|operational|running|stopped|unsure
299
384
  # }
300
385
  #
301
386
  # parameters
302
- # * [cache] - boolean controlling whether data retrieved/parsed is cached, defaults to true
387
+ # * [cache] - boolean controlling whether data retrieved/parsed is cached, defaults to true
388
+ # * [humanize] - boolean controlling whether data retrieved is massaged into simplified names or returned mostly as retrieved
303
389
  #
304
390
  # supported OS
305
391
  # * OSX - runs `launchctl list`
@@ -307,15 +393,27 @@ class Rouster
307
393
  # * Solaris - runs `svcs`
308
394
  # * Ubuntu - runs `service --status-all`
309
395
  #
310
- # raises InternalError if unsupported operating system
311
- def get_services(cache=true)
396
+ # notes
397
+ # * raises InternalError if unsupported operating system
398
+ # * OSX, Solaris and Ubuntu/Debian will only return running|stopped|unsure, the exists|installed|operational modes are RHEL/CentOS only
399
+ def get_services(cache=true, humanize=true)
312
400
  if cache and ! self.deltas[:services].nil?
313
- return self.deltas[:services]
401
+
402
+ if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:services]) > self.cache_timeout
403
+ @log.debug(sprintf('invalidating [services] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:services]), self.cache_timeout))
404
+ self.deltas.delete(:services)
405
+ else
406
+ @log.debug(sprintf('using cached [services] from [%s]', self.cache[:services]))
407
+ return self.deltas[:services]
408
+ end
409
+
314
410
  end
315
411
 
316
412
  res = Hash.new()
413
+ os = self.os_type
317
414
 
318
- os = self.os_type
415
+ allowed_modes = %w(exists installed operational running stopped unsure)
416
+ failover_mode = 'unsure'
319
417
 
320
418
  if os.eql?(:osx)
321
419
 
@@ -326,10 +424,12 @@ class Rouster
326
424
  service = $2
327
425
  mode = $1
328
426
 
329
- if mode.match(/^\d/)
330
- mode = 'running'
331
- else
332
- mode = 'stopped'
427
+ if humanize # should we do this with a .freeze instead?
428
+ if mode.match(/^\d/)
429
+ mode = 'running'
430
+ else
431
+ mode = 'stopped'
432
+ end
333
433
  end
334
434
 
335
435
  res[service] = mode
@@ -337,26 +437,36 @@ class Rouster
337
437
 
338
438
  elsif os.eql?(:solaris)
339
439
 
340
- raw = self.run('svcs')
440
+ raw = self.run('svcs -a')
341
441
  raw.split("\n").each do |line|
342
442
  next if line.match(/(.*?)\s+(?:.*?)\s+(.*?)$/).nil?
343
443
 
344
444
  service = $2
345
445
  mode = $1
346
446
 
347
- if mode.match(/online/)
348
- mode = 'running'
349
- elsif mode.match(/legacy_run/)
350
- mode = 'running'
351
- elsif mode.match(//)
352
- mode = 'stopped'
447
+ if humanize
448
+ if mode.match(/^online/)
449
+ mode = 'running'
450
+ elsif mode.match(/^legacy_run/)
451
+ mode = 'running'
452
+ elsif mode.match(/^disabled/)
453
+ mode = 'stopped'
454
+ end
455
+
456
+ if service.match(/^svc:\/.*\/(.*?):.*/)
457
+ # turning 'svc:/network/cswpuppetd:default' into 'cswpuppetd'
458
+ service = $1
459
+ elsif service.match(/^lrc:\/.*?\/.*\/(.*)/)
460
+ # turning 'lrc:/etc/rcS_d/S50sk98Sol' into 'S50sk98Sol'
461
+ service = $1
462
+ end
353
463
  end
354
464
 
355
465
  res[service] = mode
356
466
 
357
467
  end
358
468
 
359
- elsif os.eql?(:ubuntu)
469
+ elsif os.eql?(:ubuntu) or os.eql?(:debian)
360
470
 
361
471
  raw = self.run('service --status-all 2>&1')
362
472
  raw.split("\n").each do |line|
@@ -364,9 +474,11 @@ class Rouster
364
474
  mode = $1
365
475
  service = $2
366
476
 
367
- mode = 'stopped' if mode.match('-')
368
- mode = 'running' if mode.match('\+')
369
- mode = 'unsure' if mode.match('\?')
477
+ if humanize
478
+ mode = 'stopped' if mode.match('-')
479
+ mode = 'running' if mode.match('\+')
480
+ mode = 'unsure' if mode.match('\?')
481
+ end
370
482
 
371
483
  res[service] = mode
372
484
  end
@@ -375,9 +487,51 @@ class Rouster
375
487
 
376
488
  raw = self.run('/sbin/service --status-all')
377
489
  raw.split("\n").each do |line|
378
- # TODO tighten this up
379
- next if line.match(/^([^\s\:]*).*\s(\w*)(?:\.?){3}$/).nil?
380
- res[$1] = $2
490
+
491
+ if humanize
492
+
493
+ if line.match(/^(\w+?)\sis\s(.*)$/)
494
+ # <service> is <state>
495
+ res[$1] = $2
496
+
497
+ if $2.match(/^not/)
498
+ # this catches 'Kdump is not operational'
499
+ res[$1] = 'stopped'
500
+ end
501
+
502
+ elsif line.match(/^(\w+?)\s\(pid.*?\)\sis\s(\w+)$/)
503
+ # <service> (pid <pid> [pid]) is <state>...
504
+ res[$1] = $2
505
+ elsif line.match(/^(\w+?)\sis\s(\w+)\.*$/) # not sure this is actually needed
506
+ @log.debug('triggered supposedly unnecessary regex')
507
+ # <service> is <state>. whatever
508
+ res[$1] = $2
509
+ elsif line.match(/^(\w+?)\:.*?(\w+)$/)
510
+ # <service>: whatever <state>
511
+ res[$1] = $2
512
+ elsif line.match(/^(\w+)\s(\w+).*$/)
513
+ # <process> <state> whatever
514
+ res[$1] = $2
515
+ else
516
+ # original regex implementation, if we didn't match anything else, failover to this
517
+ next if line.match(/^([^\s:]*).*\s(\w*)(?:\.?){3}$/).nil?
518
+ res[$1] = $2
519
+ end
520
+
521
+ else
522
+ next if line.match(/^([^\s:]*).*\s(\w*)(?:\.?){3}$/).nil?
523
+ res[$1] = $2
524
+ end
525
+
526
+ end
527
+
528
+ # issue #63 handling
529
+ if humanize
530
+ res.each_pair do |k,v|
531
+ next if allowed_modes.member?(v)
532
+ @log.debug(sprintf('replacing service[%s] status of [%s] with [%s] for uniformity', k, v, failover_mode))
533
+ res[k] = failover_mode
534
+ end
381
535
  end
382
536
 
383
537
  else
@@ -385,7 +539,9 @@ class Rouster
385
539
  end
386
540
 
387
541
  if cache
542
+ @log.debug(sprintf('caching [services] at [%s]', Time.now.asctime))
388
543
  self.deltas[:services] = res
544
+ self.cache[:services] = Time.now.to_i
389
545
  end
390
546
 
391
547
  res
@@ -408,7 +564,15 @@ class Rouster
408
564
  # * [cache] - boolean controlling whether data retrieved/parsed is cached, defaults to true
409
565
  def get_users(cache=true)
410
566
  if cache and ! self.deltas[:users].nil?
411
- return self.deltas[:users]
567
+
568
+ if self.cache_timeout and self.cache_timeout.is_a?(Integer) and (Time.now.to_i - self.cache[:users]) > self.cache_timeout
569
+ @log.debug(sprintf('invalidating [users] cache, was [%s] old, allowed [%s]', (Time.now.to_i - self.cache[:users]), self.cache_timeout))
570
+ self.deltas.delete(:users)
571
+ else
572
+ @log.debug(sprintf('using cached [users] from [%s]', self.cache[:users]))
573
+ return self.deltas[:users]
574
+ end
575
+
412
576
  end
413
577
 
414
578
  res = Hash.new()
@@ -430,10 +594,12 @@ class Rouster
430
594
  end
431
595
 
432
596
  if cache
597
+ @log.debug(sprintf('caching [users] at [%s]', Time.now.asctime))
433
598
  self.deltas[:users] = res
599
+ self.cache[:users] = Time.now.to_i
434
600
  end
435
601
 
436
602
  res
437
603
  end
438
604
 
439
- end
605
+ end