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,596 @@
1
+ require sprintf('%s/../../%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
2
+
3
+ require 'rouster/deltas'
4
+
5
+ class Rouster
6
+
7
+ ##
8
+ # dir
9
+ #
10
+ # runs `ls -ld <dir>` and parses output, returns nil (if dir DNE or permission issue) or hash:
11
+ # {
12
+ # :directory? => boolean,
13
+ # :file? => boolean,
14
+ # :executable? => boolean, # based on user 'vagrant' context
15
+ # :writeable? => boolean, # based on user 'vagrant' context
16
+ # :readable? => boolean, # based on user 'vagrant' context
17
+ # :mode => mode, # 0-prefixed octal mode
18
+ # :name => name, # short name
19
+ # :owner => owner,
20
+ # :group => group,
21
+ # :size => size, # in bytes
22
+ # }
23
+ #
24
+ # parameters
25
+ # * <dir> - path of directory to act on, full path or relative to ~vagrant/
26
+ # * [cache] - boolean controlling whether to cache retrieved data, defaults to false
27
+ def dir(dir, cache=false)
28
+
29
+ if cache and self.deltas[:files].class.eql?(Hash) and ! self.deltas[:files][dir].nil?
30
+ return self.deltas[:files][dir]
31
+ end
32
+
33
+ if self.unittest and cache
34
+ # preventing a functional test fallthrough
35
+ return nil
36
+ end
37
+
38
+ begin
39
+ raw = self.run(sprintf('ls -ld %s', dir))
40
+ rescue Rouster::RemoteExecutionError
41
+ raw = self.get_output()
42
+ end
43
+
44
+ if raw.match(/No such file or directory/)
45
+ res = nil
46
+ elsif raw.match(/Permission denied/)
47
+ @log.info(sprintf('dir(%s) output[%s], try with sudo', dir, raw)) unless self.uses_sudo?
48
+ res = nil
49
+ else
50
+ res = parse_ls_string(raw)
51
+ end
52
+
53
+ if cache
54
+ self.deltas[:files] = Hash.new if self.deltas[:files].nil?
55
+ self.deltas[:files][dir] = res
56
+ end
57
+
58
+ res
59
+ end
60
+
61
+ ##
62
+ # dirs
63
+ #
64
+ # runs `find <dir> <recursive muckery> -type d -name '<wildcard>'`, and returns array of directories (fully qualified paths)
65
+ #
66
+ # parameters
67
+ # * <dir> - path to directory to act on, full path or relative to ~vagrant/
68
+ # * [wildcard] - glob of directories to match, defaults to '*'
69
+ # * [recursive] - boolean controlling whether or not to look in directories recursively, defaults to false
70
+ def dirs(dir, wildcard='*', insensitive=true, recursive=false)
71
+ # TODO use a numerical, not boolean value for 'recursive' -- and rename to 'depth' ?
72
+ raise InternalError.new(sprintf('invalid dir specified[%s]', dir)) unless self.is_dir?(dir)
73
+
74
+ raw = self.run(sprintf("find %s %s -type d %s '%s'", dir, recursive ? '' : '-maxdepth 1', insensitive ? '-iname' : '-name', wildcard))
75
+ res = Array.new
76
+
77
+ raw.split("\n").each do |line|
78
+ next if line.eql?(dir)
79
+ res.push(line)
80
+ end
81
+
82
+ res
83
+ end
84
+
85
+ ##
86
+ # file
87
+ #
88
+ # runs `ls -l <file>` and parses output, returns nil (if file DNE or permission issue) or hash:
89
+ # {
90
+ # :directory? => boolean,
91
+ # :file? => boolean,
92
+ # :executable? => boolean, # based on user 'vagrant' context
93
+ # :writeable? => boolean, # based on user 'vagrant' context
94
+ # :readable? => boolean, # based on user 'vagrant' context
95
+ # :mode => mode, # 0-prefixed octal mode
96
+ # :name => name, # short name
97
+ # :owner => owner,
98
+ # :group => group,
99
+ # :size => size, # in bytes
100
+ # }
101
+ #
102
+ # parameters
103
+ # * <file> - path of file to act on, full path or relative to ~vagrant/
104
+ # * [cache] - boolean controlling whether to cache retrieved data, defaults to false
105
+ def file(file, cache=false)
106
+
107
+ if cache and self.deltas[:files].class.eql?(Hash) and ! self.deltas[:files][file].nil?
108
+ return self.deltas[:files][file]
109
+ end
110
+
111
+ if self.unittest and cache
112
+ # preventing a functional test fallthrough
113
+ return nil
114
+ end
115
+
116
+ begin
117
+ raw = self.run(sprintf('ls -l %s', file))
118
+ rescue Rouster::RemoteExecutionError
119
+ raw = self.get_output()
120
+ end
121
+
122
+ if raw.match(/No such file or directory/)
123
+ @log.info(sprintf('is_file?(%s) output[%s], try with sudo', file, raw)) unless self.uses_sudo?
124
+ res = nil
125
+ elsif raw.match(/Permission denied/)
126
+ res = nil
127
+ else
128
+ res = parse_ls_string(raw)
129
+ end
130
+
131
+ if cache
132
+ self.deltas[:files] = Hash.new if self.deltas[:files].nil?
133
+ self.deltas[:files][file] = res
134
+ end
135
+
136
+ res
137
+ end
138
+
139
+ ##
140
+ # files
141
+ #
142
+ # runs `find <dir> <recursive muckery> -type f -name '<wildcard>'`, and reutns array of files (fullly qualified paths)
143
+ # parameters
144
+ # * <dir> - directory to look in, full path or relative to ~vagrant/
145
+ # * [wildcard] - glob of files to match, defaults to '*'
146
+ # * [recursive] - boolean controlling whether or not to look in directories recursively, defaults to false
147
+ def files(dir, wildcard='*', insensitive=true, recursive=false)
148
+ # TODO use a numerical, not boolean value for 'recursive'
149
+ raise InternalError.new(sprintf('invalid dir specified[%s]', dir)) unless self.is_dir?(dir)
150
+
151
+ raw = self.run(sprintf("find %s %s -type f %s '%s'", dir, recursive ? '' : '-maxdepth 1', insensitive ? '-iname' : '-name', wildcard))
152
+ res = Array.new
153
+
154
+ raw.split("\n").each do |line|
155
+ res.push(line)
156
+ end
157
+
158
+ res
159
+ end
160
+
161
+ ##
162
+ # is_dir?
163
+ #
164
+ # uses dir() to return boolean indicating whether parameter passed is a directory
165
+ #
166
+ # parameters
167
+ # * <dir> - path of directory to validate
168
+ def is_dir?(dir)
169
+ res = nil
170
+ begin
171
+ res = self.dir(dir)
172
+ rescue => e
173
+ return false
174
+ end
175
+
176
+ res.class.eql?(Hash) ? res[:directory?] : false
177
+ end
178
+
179
+ ##
180
+ # is_executable?
181
+ #
182
+ # uses file() to return boolean indicating whether parameter passed is an executable file
183
+ #
184
+ # parameters
185
+ # * <filename> - path of filename to validate
186
+ # * [level] - string indicating 'u'ser, 'g'roup or 'o'ther context, defaults to 'u'
187
+ def is_executable?(filename, level='u')
188
+ res = nil
189
+
190
+ begin
191
+ res = file(filename)
192
+ rescue Rouster::InternalError
193
+ res = dir(filename)
194
+ end
195
+
196
+ # for cases that are directories, but don't throw exceptions
197
+ if res.nil? or res[:directory?]
198
+ res = dir(filename)
199
+ end
200
+
201
+ if res
202
+ array = res[:executable?]
203
+
204
+ case level
205
+ when 'u', 'U', 'user'
206
+ array[0]
207
+ when 'g', 'G', 'group'
208
+ array[1]
209
+ when 'o', 'O', 'other'
210
+ array[2]
211
+ else
212
+ raise InternalError.new(sprintf('unknown level[%s]'))
213
+ end
214
+
215
+ else
216
+ false
217
+ end
218
+
219
+ end
220
+
221
+ ##
222
+ # is_file?
223
+ #
224
+ # uses file() to return boolean indicating whether parameter passed is a file
225
+ #
226
+ # parameters
227
+ # * <file> - path of filename to validate
228
+ def is_file?(file)
229
+ res = nil
230
+
231
+ begin
232
+ res = self.file(file)
233
+ rescue => e
234
+ return false
235
+ end
236
+
237
+ res.class.eql?(Hash) ? res[:file?] : false
238
+ end
239
+
240
+ ##
241
+ # is_group?
242
+ #
243
+ # uses get_groups() to return boolean indicating whether parameter passed is a group
244
+ #
245
+ # parameters
246
+ # * <group> - name of group to validate
247
+ def is_group?(group)
248
+ groups = self.get_groups()
249
+ groups.has_key?(group)
250
+ end
251
+
252
+ ##
253
+ # is_in_file?
254
+ #
255
+ # calls `grep -c '<regex>' <file>` and returns boolean for whether one or more matches are found in file
256
+ #
257
+ # parameters
258
+ # * <file> - path of filename to examine
259
+ # * <regex> - regular expression/string to be passed to grep
260
+ # * [scp] - downloads file to host machine before grepping (functionality not implemented, was planned when a new SSH connection was required for each run() command, not sure it is necessary any longer)
261
+ def is_in_file?(file, regex, scp=false)
262
+
263
+ res = nil
264
+
265
+ if scp
266
+ # download the file to a temporary directory
267
+ @log.warn('is_in_file? scp option not implemented yet')
268
+ end
269
+
270
+ begin
271
+ command = sprintf("grep -c '%s' %s", regex, file)
272
+ res = self.run(command)
273
+ rescue Rouster::RemoteExecutionError
274
+ return false
275
+ end
276
+
277
+ if res.nil?.false? and res.match(/^0/)
278
+ false
279
+ else
280
+ true
281
+ end
282
+
283
+ end
284
+
285
+ ##
286
+ # is_in_path?
287
+ #
288
+ # runs `which <filename>`, returns boolean of whether the filename is exectuable and in $PATH
289
+ #
290
+ # parameters
291
+ # * <filename> - name of executable to validate
292
+ def is_in_path?(filename)
293
+ begin
294
+ self.run(sprintf('which %s', filename))
295
+ rescue Rouster::RemoteExecutionError
296
+ return false
297
+ end
298
+
299
+ true
300
+ end
301
+
302
+ ##
303
+ # is_package?
304
+ #
305
+ # uses get_packages() to return boolean indicating whether passed parameter is an installed package
306
+ #
307
+ # parameters
308
+ # * <package> - name of package to validate
309
+ # * [cache] - boolean controlling whether to cache results from get_packages(), defaults to true (for performance)
310
+ def is_package?(package, cache=true)
311
+ # TODO should we implement something like is_package_version?()
312
+ packages = self.get_packages(cache)
313
+ packages.has_key?(package)
314
+ end
315
+
316
+ ##
317
+ # is_port_active?
318
+ #
319
+ # uses get_ports() to return boolean indicating whether passed port is in use
320
+ #
321
+ # parameters
322
+ # * <port> - port number to validate
323
+ # * [proto] - specification of protocol to examine, defaults to tcp
324
+ # * [cache] - boolean controlling whether to cache get_ports() data, defaults to false
325
+ def is_port_active?(port, proto='tcp', cache=false)
326
+ # TODO is this the right name?
327
+ ports = self.get_ports(cache)
328
+ port = port.to_s
329
+ if ports[proto].class.eql?(Hash) and ports[proto].has_key?(port)
330
+
331
+ if proto.eql?('tcp')
332
+ ['ACTIVE', 'ESTABLISHED', 'LISTEN']. each do |allowed|
333
+ return true if ports[proto][port][:address].values.member?(allowed)
334
+ end
335
+ else
336
+ return true
337
+ end
338
+
339
+ end
340
+
341
+ false
342
+ end
343
+
344
+ ##
345
+ # is_port_open?
346
+ #
347
+ # uses get_ports() to return boolean indicating whether passed port is open
348
+ #
349
+ # parameters
350
+ # * <port> - port number to validate
351
+ # * [proto] - specification of protocol to examine, defaults to tcp
352
+ # * [cache] - boolean controlling whether to cache get_ports() data, defaults to false
353
+ def is_port_open?(port, proto='tcp', cache=false)
354
+ ports = self.get_ports(cache)
355
+ port = port.to_s
356
+ if ports[proto].class.eql?(Hash) and ports[proto].has_key?(port)
357
+ return false
358
+ end
359
+
360
+ true
361
+ end
362
+
363
+ ##
364
+ # is_process_running?
365
+ #
366
+ # runs `ps ax | grep -c <process>` looking for more than 2 results
367
+ #
368
+ # parameters
369
+ # * <name> - name of process to look for
370
+ #
371
+ # supported OS
372
+ # * OSX
373
+ # * RedHat
374
+ # * Ubuntu
375
+ def is_process_running?(name)
376
+ # TODO support other flavors - this will work on RHEL and OSX
377
+ # TODO do better validation than just grepping for a matching filename
378
+ begin
379
+
380
+ os = self.os_type()
381
+
382
+ case os
383
+ when :redhat, :osx, :ubuntu, :debian
384
+ res = self.run(sprintf('ps ax | grep -c %s', name))
385
+ else
386
+ raise InternalError.new(sprintf('currently unable to determine running process list on OS[%s]', os))
387
+ end
388
+
389
+ rescue Rouster::RemoteExecutionError
390
+ return false
391
+ end
392
+
393
+ res.chomp.to_i > 2 # because of the weird way our process is run through the ssh tunnel
394
+ end
395
+
396
+ ##
397
+ # is_readable?
398
+ #
399
+ # uses file() to return boolean indicating whether parameter passed is an readable file
400
+ #
401
+ # parameters
402
+ # * <filename> - path of filename to validate
403
+ # * [level] - string indicating 'u'ser, 'g'roup or 'o'ther context, defaults to 'u'
404
+ def is_readable?(filename, level='u')
405
+ res = nil
406
+
407
+ begin
408
+ res = file(filename)
409
+ rescue Rouster::InternalError
410
+ res = dir(filename)
411
+ end
412
+
413
+ # for cases that are directories, but don't throw exceptions
414
+ if res.nil? or res[:directory?]
415
+ res = dir(filename)
416
+ end
417
+
418
+ if res
419
+ array = res[:readable?]
420
+
421
+ case level
422
+ when 'u', 'U', 'user'
423
+ array[0]
424
+ when 'g', 'G', 'group'
425
+ array[1]
426
+ when 'o', 'O', 'other'
427
+ array[2]
428
+ else
429
+ raise InternalError.new(sprintf('unknown level[%s]'))
430
+ end
431
+
432
+ else
433
+ false
434
+ end
435
+
436
+ end
437
+
438
+ ##
439
+ # is_service?
440
+ #
441
+ # uses get_services() to return boolean indicating whether passed parameter is an installed service
442
+ #
443
+ # parameters
444
+ # * <service> - name of service to validate
445
+ # * [cache] - boolean controlling whether to cache results from get_services(), defaults to true
446
+ def is_service?(service, cache=true)
447
+ services = self.get_services(cache)
448
+ services.has_key?(service)
449
+ end
450
+
451
+
452
+ ##
453
+ # is_service_running?
454
+ #
455
+ # uses get_services() to return boolean indicating whether passed parameter is a running service
456
+ #
457
+ # parameters
458
+ # * <service> - name of service to validate
459
+ # * [cache] - boolean controlling whether to cache results from get_services(), defaults to false
460
+ def is_service_running?(service, cache=false)
461
+ services = self.get_services(cache)
462
+
463
+ if services.has_key?(service)
464
+ services[service].eql?('running').true?
465
+ else
466
+ false
467
+ end
468
+ end
469
+
470
+ ##
471
+ # is_user?
472
+ #
473
+ # uses get_users() to return boolean indicating whether passed parameter is a user
474
+ #
475
+ # parameters
476
+ # * <user> - username to validate
477
+ # * [cache] - boolean controlling whether to cache results from get_users(), defaults to true
478
+ def is_user?(user, cache=true)
479
+ users = self.get_users(cache)
480
+ users.has_key?(user)
481
+ end
482
+
483
+ ##
484
+ # is_user_in_group?
485
+ #
486
+ # uses get_users() and get_groups() to return boolean indicating whether passed user is in passed group
487
+ #
488
+ # parameters
489
+ # * <user> - username to validate
490
+ # * <group> - group expected to contain user
491
+ # * [cache] - boolean controlling whether to cache results from get_users() and get_groups(), defaults to true
492
+ def is_user_in_group?(user, group, cache=true)
493
+ # TODO can we scope this down to just use get_groups?
494
+ users = self.get_users(cache)
495
+ groups = self.get_groups(cache)
496
+
497
+ users.has_key?(user) and groups.has_key?(group) and groups[group][:users].member?(user)
498
+ end
499
+
500
+ ##
501
+ # is_writeable?
502
+ #
503
+ # uses file() to return boolean indicating whether parameter passed is an executable file
504
+ #
505
+ # parameters
506
+ # * <filename> - path of filename to validate
507
+ # * [level] - string indicating 'u'ser, 'g'roup or 'o'ther context, defaults to 'u'
508
+ def is_writeable?(filename, level='u')
509
+ res = nil
510
+
511
+ begin
512
+ res = file(filename)
513
+ rescue Rouster::InternalError
514
+ res = dir(filename)
515
+ end
516
+
517
+ # for cases that are directories, but don't throw exceptions
518
+ if res.nil? or res[:directory?]
519
+ res = dir(filename)
520
+ end
521
+
522
+ if res
523
+ array = res[:writeable?]
524
+
525
+ case level
526
+ when 'u', 'U', 'user'
527
+ array[0]
528
+ when 'g', 'G', 'group'
529
+ array[1]
530
+ when 'o', 'O', 'other'
531
+ array[2]
532
+ else
533
+ raise InternalError.new(sprintf('unknown level[%s]'))
534
+ end
535
+
536
+ else
537
+ false
538
+ end
539
+
540
+ end
541
+
542
+ # non-test, helper methods
543
+ #private
544
+ def parse_ls_string(string)
545
+ # ht avaghti
546
+
547
+ res = Hash.new()
548
+
549
+ tokens = string.split(/\s+/)
550
+
551
+ # eww - do better here
552
+ modes = [ tokens[0][1..3], tokens[0][4..6], tokens[0][7..9] ]
553
+ mode = 0
554
+
555
+ # can't use modes.size here (or could, but would have to -1)
556
+ for i in 0..2 do
557
+ value = 0
558
+ element = modes[i]
559
+
560
+ for j in 0..2 do
561
+ chr = element[j].chr
562
+ case chr
563
+ when 'r'
564
+ value += 4
565
+ when 'w'
566
+ value += 2
567
+ when 'x', 't'
568
+ # is 't' really right here? copying Salesforce::Vagrant
569
+ value += 1
570
+ when '-'
571
+ # noop
572
+ else
573
+ raise InternalError.new(sprintf('unexpected character[%s] in string[%s]', chr, string))
574
+ end
575
+
576
+ end
577
+
578
+ mode = sprintf('%s%s', mode, value)
579
+ end
580
+
581
+ res[:mode] = mode
582
+ res[:name] = tokens[-1] # TODO better here: this does not support files/dirs with spaces
583
+ res[:owner] = tokens[2]
584
+ res[:group] = tokens[3]
585
+ res[:size] = tokens[4]
586
+
587
+ res[:directory?] = tokens[0][0].chr.eql?('d')
588
+ res[:file?] = ! res[:directory?]
589
+ res[:executable?] = [ tokens[0][3].chr.eql?('x'), tokens[0][6].chr.eql?('x'), tokens[0][9].chr.eql?('x') || tokens[0][9].chr.eql?('t') ]
590
+ res[:writeable?] = [ tokens[0][2].chr.eql?('w'), tokens[0][5].chr.eql?('w'), tokens[0][8].chr.eql?('w') ]
591
+ res[:readable?] = [ tokens[0][1].chr.eql?('r'), tokens[0][4].chr.eql?('r'), tokens[0][7].chr.eql?('r') ]
592
+
593
+ res
594
+ end
595
+
596
+ end