choria-mcorpc-support 0.0.1

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 (133) hide show
  1. checksums.yaml +7 -0
  2. data/bin/mco +64 -0
  3. data/lib/mcollective.rb +63 -0
  4. data/lib/mcollective/agent.rb +5 -0
  5. data/lib/mcollective/agents.rb +149 -0
  6. data/lib/mcollective/aggregate.rb +85 -0
  7. data/lib/mcollective/aggregate/average.ddl +33 -0
  8. data/lib/mcollective/aggregate/average.rb +29 -0
  9. data/lib/mcollective/aggregate/base.rb +40 -0
  10. data/lib/mcollective/aggregate/result.rb +9 -0
  11. data/lib/mcollective/aggregate/result/base.rb +25 -0
  12. data/lib/mcollective/aggregate/result/collection_result.rb +19 -0
  13. data/lib/mcollective/aggregate/result/numeric_result.rb +13 -0
  14. data/lib/mcollective/aggregate/sum.ddl +33 -0
  15. data/lib/mcollective/aggregate/sum.rb +18 -0
  16. data/lib/mcollective/aggregate/summary.ddl +33 -0
  17. data/lib/mcollective/aggregate/summary.rb +53 -0
  18. data/lib/mcollective/application.rb +365 -0
  19. data/lib/mcollective/application/completion.rb +104 -0
  20. data/lib/mcollective/application/describe_filter.rb +87 -0
  21. data/lib/mcollective/application/facts.rb +62 -0
  22. data/lib/mcollective/application/find.rb +23 -0
  23. data/lib/mcollective/application/help.rb +28 -0
  24. data/lib/mcollective/application/inventory.rb +344 -0
  25. data/lib/mcollective/application/ping.rb +82 -0
  26. data/lib/mcollective/application/plugin.rb +369 -0
  27. data/lib/mcollective/application/rpc.rb +111 -0
  28. data/lib/mcollective/applications.rb +134 -0
  29. data/lib/mcollective/cache.rb +145 -0
  30. data/lib/mcollective/client.rb +353 -0
  31. data/lib/mcollective/config.rb +245 -0
  32. data/lib/mcollective/connector.rb +18 -0
  33. data/lib/mcollective/connector/base.rb +26 -0
  34. data/lib/mcollective/data.rb +91 -0
  35. data/lib/mcollective/data/agent_data.ddl +22 -0
  36. data/lib/mcollective/data/agent_data.rb +17 -0
  37. data/lib/mcollective/data/base.rb +67 -0
  38. data/lib/mcollective/data/collective_data.ddl +20 -0
  39. data/lib/mcollective/data/collective_data.rb +9 -0
  40. data/lib/mcollective/data/fact_data.ddl +28 -0
  41. data/lib/mcollective/data/fact_data.rb +55 -0
  42. data/lib/mcollective/data/fstat_data.ddl +89 -0
  43. data/lib/mcollective/data/fstat_data.rb +56 -0
  44. data/lib/mcollective/data/result.rb +45 -0
  45. data/lib/mcollective/ddl.rb +113 -0
  46. data/lib/mcollective/ddl/agentddl.rb +253 -0
  47. data/lib/mcollective/ddl/base.rb +217 -0
  48. data/lib/mcollective/ddl/dataddl.rb +56 -0
  49. data/lib/mcollective/ddl/discoveryddl.rb +52 -0
  50. data/lib/mcollective/ddl/validatorddl.rb +6 -0
  51. data/lib/mcollective/discovery.rb +143 -0
  52. data/lib/mcollective/discovery/flatfile.ddl +11 -0
  53. data/lib/mcollective/discovery/flatfile.rb +48 -0
  54. data/lib/mcollective/discovery/mc.ddl +11 -0
  55. data/lib/mcollective/discovery/mc.rb +30 -0
  56. data/lib/mcollective/discovery/stdin.ddl +11 -0
  57. data/lib/mcollective/discovery/stdin.rb +68 -0
  58. data/lib/mcollective/exceptions.rb +28 -0
  59. data/lib/mcollective/facts.rb +39 -0
  60. data/lib/mcollective/facts/base.rb +100 -0
  61. data/lib/mcollective/facts/yaml_facts.rb +65 -0
  62. data/lib/mcollective/generators.rb +7 -0
  63. data/lib/mcollective/generators/agent_generator.rb +51 -0
  64. data/lib/mcollective/generators/base.rb +46 -0
  65. data/lib/mcollective/generators/data_generator.rb +51 -0
  66. data/lib/mcollective/generators/templates/action_snippet.erb +13 -0
  67. data/lib/mcollective/generators/templates/data_input_snippet.erb +7 -0
  68. data/lib/mcollective/generators/templates/ddl.erb +8 -0
  69. data/lib/mcollective/generators/templates/plugin.erb +7 -0
  70. data/lib/mcollective/log.rb +118 -0
  71. data/lib/mcollective/logger.rb +5 -0
  72. data/lib/mcollective/logger/base.rb +77 -0
  73. data/lib/mcollective/logger/console_logger.rb +61 -0
  74. data/lib/mcollective/logger/file_logger.rb +53 -0
  75. data/lib/mcollective/logger/syslog_logger.rb +53 -0
  76. data/lib/mcollective/matcher.rb +224 -0
  77. data/lib/mcollective/matcher/parser.rb +128 -0
  78. data/lib/mcollective/matcher/scanner.rb +241 -0
  79. data/lib/mcollective/message.rb +248 -0
  80. data/lib/mcollective/monkey_patches.rb +152 -0
  81. data/lib/mcollective/optionparser.rb +197 -0
  82. data/lib/mcollective/pluginmanager.rb +180 -0
  83. data/lib/mcollective/pluginpackager.rb +98 -0
  84. data/lib/mcollective/pluginpackager/agent_definition.rb +94 -0
  85. data/lib/mcollective/pluginpackager/debpackage_packager.rb +237 -0
  86. data/lib/mcollective/pluginpackager/modulepackage_packager.rb +127 -0
  87. data/lib/mcollective/pluginpackager/ospackage_packager.rb +59 -0
  88. data/lib/mcollective/pluginpackager/rpmpackage_packager.rb +180 -0
  89. data/lib/mcollective/pluginpackager/standard_definition.rb +69 -0
  90. data/lib/mcollective/pluginpackager/templates/debian/Makefile.erb +7 -0
  91. data/lib/mcollective/pluginpackager/templates/debian/changelog.erb +5 -0
  92. data/lib/mcollective/pluginpackager/templates/debian/compat.erb +1 -0
  93. data/lib/mcollective/pluginpackager/templates/debian/control.erb +15 -0
  94. data/lib/mcollective/pluginpackager/templates/debian/copyright.erb +8 -0
  95. data/lib/mcollective/pluginpackager/templates/debian/rules.erb +6 -0
  96. data/lib/mcollective/pluginpackager/templates/module/Modulefile.erb +5 -0
  97. data/lib/mcollective/pluginpackager/templates/module/README.md.erb +37 -0
  98. data/lib/mcollective/pluginpackager/templates/module/_manifest.pp.erb +9 -0
  99. data/lib/mcollective/pluginpackager/templates/redhat/rpm_spec.erb +63 -0
  100. data/lib/mcollective/registration/base.rb +91 -0
  101. data/lib/mcollective/rpc.rb +182 -0
  102. data/lib/mcollective/rpc/actionrunner.rb +158 -0
  103. data/lib/mcollective/rpc/agent.rb +374 -0
  104. data/lib/mcollective/rpc/audit.rb +38 -0
  105. data/lib/mcollective/rpc/client.rb +1066 -0
  106. data/lib/mcollective/rpc/helpers.rb +321 -0
  107. data/lib/mcollective/rpc/progress.rb +63 -0
  108. data/lib/mcollective/rpc/reply.rb +87 -0
  109. data/lib/mcollective/rpc/request.rb +86 -0
  110. data/lib/mcollective/rpc/result.rb +90 -0
  111. data/lib/mcollective/rpc/stats.rb +294 -0
  112. data/lib/mcollective/runnerstats.rb +90 -0
  113. data/lib/mcollective/security.rb +26 -0
  114. data/lib/mcollective/security/base.rb +244 -0
  115. data/lib/mcollective/shell.rb +126 -0
  116. data/lib/mcollective/ssl.rb +285 -0
  117. data/lib/mcollective/util.rb +579 -0
  118. data/lib/mcollective/validator.rb +85 -0
  119. data/lib/mcollective/validator/array_validator.ddl +7 -0
  120. data/lib/mcollective/validator/array_validator.rb +9 -0
  121. data/lib/mcollective/validator/ipv4address_validator.ddl +7 -0
  122. data/lib/mcollective/validator/ipv4address_validator.rb +16 -0
  123. data/lib/mcollective/validator/ipv6address_validator.ddl +7 -0
  124. data/lib/mcollective/validator/ipv6address_validator.rb +16 -0
  125. data/lib/mcollective/validator/length_validator.ddl +7 -0
  126. data/lib/mcollective/validator/length_validator.rb +11 -0
  127. data/lib/mcollective/validator/regex_validator.ddl +7 -0
  128. data/lib/mcollective/validator/regex_validator.rb +9 -0
  129. data/lib/mcollective/validator/shellsafe_validator.ddl +7 -0
  130. data/lib/mcollective/validator/shellsafe_validator.rb +13 -0
  131. data/lib/mcollective/validator/typecheck_validator.ddl +7 -0
  132. data/lib/mcollective/validator/typecheck_validator.rb +28 -0
  133. metadata +215 -0
@@ -0,0 +1,285 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+ require 'digest/sha1'
4
+
5
+ module MCollective
6
+ # A class that assists in encrypting and decrypting data using a
7
+ # combination of RSA and AES
8
+ #
9
+ # Data will be AES encrypted for speed, the Key used in # the AES
10
+ # stage will be encrypted using RSA
11
+ #
12
+ # ssl = SSL.new(public_key, private_key, passphrase)
13
+ #
14
+ # data = File.read("largefile.dat")
15
+ #
16
+ # crypted_data = ssl.encrypt_with_private(data)
17
+ #
18
+ # pp crypted_data
19
+ #
20
+ # This will result in a hash of data like:
21
+ #
22
+ # crypted = {:key => "crd4NHvG....=",
23
+ # :data => "XWXlqN+i...=="}
24
+ #
25
+ # The key and data will all be base 64 encoded already by default
26
+ # you can pass a 2nd parameter as false to encrypt_with_private and
27
+ # counterparts that will prevent the base 64 encoding
28
+ #
29
+ # You can pass the data hash into ssl.decrypt_with_public which
30
+ # should return your original data
31
+ #
32
+ # There are matching methods for using a public key to encrypt
33
+ # data to be decrypted using a private key
34
+ class SSL
35
+ attr_reader :public_key_file, :private_key_file, :ssl_cipher
36
+
37
+ def initialize(pubkey=nil, privkey=nil, passphrase=nil, cipher=nil)
38
+ @public_key_file = pubkey
39
+ @private_key_file = privkey
40
+
41
+ @public_key = read_key(:public, pubkey)
42
+ @private_key = read_key(:private, privkey, passphrase)
43
+
44
+ @ssl_cipher = "aes-256-cbc"
45
+ @ssl_cipher = Config.instance.ssl_cipher if Config.instance.ssl_cipher
46
+ @ssl_cipher = cipher if cipher
47
+
48
+ raise "The supplied cipher '#{@ssl_cipher}' is not supported" unless OpenSSL::Cipher.ciphers.include?(@ssl_cipher)
49
+ end
50
+
51
+ # Encrypts supplied data using AES and then encrypts using RSA
52
+ # the key and IV
53
+ #
54
+ # Return a hash with everything optionally base 64 encoded
55
+ def encrypt_with_public(plain_text, base64=true)
56
+ crypted = aes_encrypt(plain_text)
57
+
58
+ if base64
59
+ key = base64_encode(rsa_encrypt_with_public(crypted[:key]))
60
+ data = base64_encode(crypted[:data])
61
+ else
62
+ key = rsa_encrypt_with_public(crypted[:key])
63
+ data = crypted[:data]
64
+ end
65
+
66
+ {:key => key, :data => data}
67
+ end
68
+
69
+ # Encrypts supplied data using AES and then encrypts using RSA
70
+ # the key and IV
71
+ #
72
+ # Return a hash with everything optionally base 64 encoded
73
+ def encrypt_with_private(plain_text, base64=true)
74
+ crypted = aes_encrypt(plain_text)
75
+
76
+ if base64
77
+ key = base64_encode(rsa_encrypt_with_private(crypted[:key]))
78
+ data = base64_encode(crypted[:data])
79
+ else
80
+ key = rsa_encrypt_with_private(crypted[:key])
81
+ data = crypted[:data]
82
+ end
83
+
84
+ {:key => key, :data => data}
85
+ end
86
+
87
+ # Decrypts data, expects a hash as create with crypt_with_public
88
+ def decrypt_with_private(crypted, base64=true)
89
+ raise "Crypted data should include a key" unless crypted.include?(:key)
90
+ raise "Crypted data should include data" unless crypted.include?(:data)
91
+
92
+ if base64
93
+ key = rsa_decrypt_with_private(base64_decode(crypted[:key]))
94
+ aes_decrypt(key, base64_decode(crypted[:data]))
95
+ else
96
+ key = rsa_decrypt_with_private(crypted[:key])
97
+ aes_decrypt(key, crypted[:data])
98
+ end
99
+ end
100
+
101
+ # Decrypts data, expects a hash as create with crypt_with_private
102
+ def decrypt_with_public(crypted, base64=true)
103
+ raise "Crypted data should include a key" unless crypted.include?(:key)
104
+ raise "Crypted data should include data" unless crypted.include?(:data)
105
+
106
+ if base64
107
+ key = rsa_decrypt_with_public(base64_decode(crypted[:key]))
108
+ aes_decrypt(key, base64_decode(crypted[:data]))
109
+ else
110
+ key = rsa_decrypt_with_public(crypted[:key])
111
+ aes_decrypt(key, crypted[:data])
112
+ end
113
+ end
114
+
115
+ # Use the public key to RSA encrypt data
116
+ def rsa_encrypt_with_public(plain_string)
117
+ raise "No public key set" unless @public_key
118
+
119
+ @public_key.public_encrypt(plain_string)
120
+ end
121
+
122
+ # Use the private key to RSA decrypt data
123
+ def rsa_decrypt_with_private(crypt_string)
124
+ raise "No private key set" unless @private_key
125
+
126
+ @private_key.private_decrypt(crypt_string)
127
+ end
128
+
129
+ # Use the private key to RSA encrypt data
130
+ def rsa_encrypt_with_private(plain_string)
131
+ raise "No private key set" unless @private_key
132
+
133
+ @private_key.private_encrypt(plain_string)
134
+ end
135
+
136
+ # Use the public key to RSA decrypt data
137
+ def rsa_decrypt_with_public(crypt_string)
138
+ raise "No public key set" unless @public_key
139
+
140
+ @public_key.public_decrypt(crypt_string)
141
+ end
142
+
143
+ # encrypts a string, returns a hash of key, iv and data
144
+ def aes_encrypt(plain_string)
145
+ cipher = OpenSSL::Cipher.new(ssl_cipher)
146
+ cipher.encrypt
147
+
148
+ key = cipher.random_key
149
+
150
+ cipher.key = key
151
+ cipher.pkcs5_keyivgen(key)
152
+ encrypted_data = cipher.update(plain_string) + cipher.final
153
+
154
+ {:key => key, :data => encrypted_data}
155
+ end
156
+
157
+ # decrypts a string given key, iv and data
158
+ def aes_decrypt(key, crypt_string)
159
+ cipher = OpenSSL::Cipher.new(ssl_cipher)
160
+
161
+ cipher.decrypt
162
+ cipher.key = key
163
+ cipher.pkcs5_keyivgen(key)
164
+ decrypted_data = cipher.update(crypt_string) + cipher.final
165
+ end
166
+
167
+ # Signs a string using the private key
168
+ def sign(string, base64=false)
169
+ sig = @private_key.sign(OpenSSL::Digest::SHA1.new, string)
170
+
171
+ base64 ? base64_encode(sig) : sig
172
+ end
173
+
174
+ # Using the public key verifies that a string was signed using the private key
175
+ def verify_signature(signature, string, base64=false)
176
+ signature = base64_decode(signature) if base64
177
+
178
+ @public_key.verify(OpenSSL::Digest::SHA1.new, signature, string)
179
+ end
180
+
181
+ # base 64 encode a string
182
+ def base64_encode(string)
183
+ SSL.base64_encode(string)
184
+ end
185
+
186
+ def self.base64_encode(string)
187
+ Base64.encode64(string)
188
+ end
189
+
190
+ # base 64 decode a string
191
+ def base64_decode(string)
192
+ SSL.base64_decode(string)
193
+ end
194
+
195
+ def self.base64_decode(string)
196
+ # The Base 64 character set is A-Z a-z 0-9 + / =
197
+ # Also allow for whitespace, but raise if we get anything else
198
+ if string !~ /^[A-Za-z0-9+\/=\s]+$/
199
+ raise ArgumentError, 'invalid base64'
200
+ end
201
+ Base64.decode64(string)
202
+ end
203
+
204
+ def md5(string)
205
+ SSL.md5(string)
206
+ end
207
+
208
+ def self.md5(string)
209
+ Digest::MD5.hexdigest(string)
210
+ end
211
+
212
+ # Creates a RFC 4122 version 5 UUID. If string is supplied it will produce repeatable
213
+ # UUIDs for that string else a random 128bit string will be used from OpenSSL::BN
214
+ #
215
+ # Code used with permission from:
216
+ # https://github.com/kwilczynski/puppet-functions/blob/master/lib/puppet/parser/functions/uuid.rb
217
+ #
218
+ def self.uuid(string=nil)
219
+ string ||= OpenSSL::Random.random_bytes(16).unpack('H*').shift
220
+
221
+ uuid_name_space_dns = [0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8].map {|b| b.chr}.join
222
+
223
+ sha1 = Digest::SHA1.new
224
+ sha1.update(uuid_name_space_dns)
225
+ sha1.update(string)
226
+
227
+ # first 16 bytes..
228
+ bytes = sha1.digest[0, 16].bytes.to_a
229
+
230
+ # version 5 adjustments
231
+ bytes[6] &= 0x0f
232
+ bytes[6] |= 0x50
233
+
234
+ # variant is DCE 1.1
235
+ bytes[8] &= 0x3f
236
+ bytes[8] |= 0x80
237
+
238
+ bytes = [4, 2, 2, 2, 6].collect do |i|
239
+ bytes.slice!(0, i).pack('C*').unpack('H*')
240
+ end
241
+
242
+ bytes.join('-')
243
+ end
244
+
245
+ # Reads either a :public or :private key from disk, uses an
246
+ # optional passphrase to read the private key
247
+ def read_key(type, key=nil, passphrase=nil)
248
+ return key if key.nil?
249
+
250
+ raise "Could not find key #{key}" unless File.exist?(key)
251
+ raise "#{type} key file '#{key}' is empty" if File.zero?(key)
252
+
253
+ if type == :public
254
+ begin
255
+ key = OpenSSL::PKey::RSA.new(File.read(key))
256
+ rescue OpenSSL::PKey::RSAError
257
+ key = OpenSSL::X509::Certificate.new(File.read(key)).public_key
258
+ end
259
+
260
+ # Ruby < 1.9.3 had a bug where it does not correctly clear the
261
+ # queue of errors while reading a key. It tries various ways
262
+ # to read the key and each failing attempt pushes an error onto
263
+ # the queue. With pubkeys only the 3rd attempt pass leaving 2
264
+ # stale errors on the error queue.
265
+ #
266
+ # In 1.9.3 they fixed this by simply discarding the errors after
267
+ # every attempt. So we simulate this fix here for older rubies
268
+ # as without it we get SSL_read errors from the Stomp+TLS sessions
269
+ #
270
+ # We do this only on 1.8 relying on 1.9.3 to do the right thing
271
+ # and we do not support 1.9 less than 1.9.3
272
+ #
273
+ # See http://bugs.ruby-lang.org/issues/4550
274
+ OpenSSL.errors if Util.ruby_version =~ /^1.8/
275
+
276
+ return key
277
+ elsif type == :private
278
+ return OpenSSL::PKey::RSA.new(File.read(key), passphrase)
279
+ else
280
+ raise "Can only load :public or :private keys"
281
+ end
282
+ end
283
+
284
+ end
285
+ end
@@ -0,0 +1,579 @@
1
+ module MCollective
2
+ # Some basic utility helper methods useful to clients, agents, runner etc.
3
+ module Util
4
+ # Finds out if this MCollective has an agent by the name passed
5
+ #
6
+ # If the passed name starts with a / it's assumed to be regex
7
+ # and will use regex to match
8
+ def self.has_agent?(agent)
9
+ agent = Regexp.new(agent.gsub("\/", "")) if agent.match("^/")
10
+
11
+ if agent.is_a?(Regexp)
12
+ if Agents.agentlist.grep(agent).size > 0
13
+ return true
14
+ else
15
+ return false
16
+ end
17
+ else
18
+ return Agents.agentlist.include?(agent)
19
+ end
20
+
21
+ false
22
+ end
23
+
24
+ # On windows ^c can't interrupt the VM if its blocking on
25
+ # IO, so this sets up a dummy thread that sleeps and this
26
+ # will have the end result of being interruptable at least
27
+ # once a second. This is a common pattern found in Rails etc
28
+ def self.setup_windows_sleeper
29
+ Thread.new { loop { sleep 1 } } if Util.windows?
30
+ end
31
+
32
+ # Checks if this node has a configuration management class by parsing the
33
+ # a text file with just a list of classes, recipes, roles etc. This is
34
+ # ala the classes.txt from puppet.
35
+ #
36
+ # If the passed name starts with a / it's assumed to be regex
37
+ # and will use regex to match
38
+ def self.has_cf_class?(klass)
39
+ klass = Regexp.new(klass.gsub("\/", "")) if klass.match("^/")
40
+ cfile = Config.instance.classesfile
41
+
42
+ Log.debug("Looking for configuration management classes in #{cfile}")
43
+
44
+ begin
45
+ File.readlines(cfile).each do |k|
46
+ if klass.is_a?(Regexp)
47
+ return true if k.chomp.match(klass)
48
+ else
49
+ return true if k.chomp == klass
50
+ end
51
+ end
52
+ rescue Exception => e
53
+ Log.warn("Parsing classes file '#{cfile}' failed: #{e.class}: #{e}")
54
+ end
55
+
56
+ false
57
+ end
58
+
59
+ # Gets the value of a specific fact, mostly just a duplicate of MCollective::Facts.get_fact
60
+ # but it kind of goes with the other classes here
61
+ def self.get_fact(fact)
62
+ Facts.get_fact(fact)
63
+ end
64
+
65
+ # Compares fact == value,
66
+ #
67
+ # If the passed value starts with a / it's assumed to be regex
68
+ # and will use regex to match
69
+ def self.has_fact?(fact, value, operator)
70
+
71
+ Log.debug("Comparing #{fact} #{operator} #{value}")
72
+ Log.debug("where :fact = '#{fact}', :operator = '#{operator}', :value = '#{value}'")
73
+
74
+ fact = Facts[fact]
75
+ return false if fact.nil?
76
+
77
+ fact = fact.clone
78
+ case fact
79
+ when Array
80
+ return fact.any? { |element| test_fact_value(element, value, operator)}
81
+ when Hash
82
+ return fact.keys.any? { |element| test_fact_value(element, value, operator)}
83
+ else
84
+ return test_fact_value(fact, value, operator)
85
+ end
86
+ end
87
+
88
+ def self.test_fact_value(fact, value, operator)
89
+ if operator == '=~'
90
+ # to maintain backward compat we send the value
91
+ # as /.../ which is what 1.0.x needed. this strips
92
+ # off the /'s which is what we need here
93
+ if value =~ /^\/(.+)\/$/
94
+ value = $1
95
+ end
96
+
97
+ return true if fact.match(Regexp.new(value))
98
+
99
+ elsif operator == "=="
100
+ return true if fact == value
101
+
102
+ elsif ['<=', '>=', '<', '>', '!='].include?(operator)
103
+ # Yuk - need to type cast, but to_i and to_f are overzealous
104
+ if value =~ /^[0-9]+$/ && fact =~ /^[0-9]+$/
105
+ fact = Integer(fact)
106
+ value = Integer(value)
107
+ elsif value =~ /^[0-9]+.[0-9]+$/ && fact =~ /^[0-9]+.[0-9]+$/
108
+ fact = Float(fact)
109
+ value = Float(value)
110
+ end
111
+
112
+ return true if eval("fact #{operator} value")
113
+ end
114
+
115
+ false
116
+ end
117
+ private_class_method :test_fact_value
118
+
119
+ # Checks if the configured identity matches the one supplied
120
+ #
121
+ # If the passed name starts with a / it's assumed to be regex
122
+ # and will use regex to match
123
+ def self.has_identity?(identity)
124
+ identity = Regexp.new(identity.gsub("\/", "")) if identity.match("^/")
125
+
126
+ if identity.is_a?(Regexp)
127
+ return Config.instance.identity.match(identity)
128
+ else
129
+ return true if Config.instance.identity == identity
130
+ end
131
+
132
+ false
133
+ end
134
+
135
+ # Checks if the passed in filter is an empty one
136
+ def self.empty_filter?(filter)
137
+ filter == empty_filter || filter == {}
138
+ end
139
+
140
+ # Creates an empty filter
141
+ def self.empty_filter
142
+ {"fact" => [],
143
+ "cf_class" => [],
144
+ "agent" => [],
145
+ "identity" => [],
146
+ "compound" => []}
147
+ end
148
+
149
+ # Returns the PuppetLabs mcollective path for windows
150
+ def self.windows_prefix
151
+ require 'win32/dir'
152
+ prefix = File.join(Dir::COMMON_APPDATA, "PuppetLabs", "mcollective")
153
+ end
154
+
155
+ # Picks a config file defaults to ~/.mcollective
156
+ # else /etc/mcollective/client.cfg
157
+ def self.config_file_for_user
158
+ # the set of acceptable config files
159
+ config_paths = []
160
+
161
+ # user dotfile
162
+ begin
163
+ # File.expand_path will raise if HOME isn't set, catch it
164
+ user_path = File.expand_path("~/.mcollective")
165
+ config_paths << user_path
166
+ rescue Exception
167
+ end
168
+
169
+ # standard locations
170
+ if self.windows?
171
+ config_paths << File.join(self.windows_prefix, 'etc', 'client.cfg')
172
+ else
173
+ config_paths << '/etc/puppetlabs/mcollective/client.cfg'
174
+ config_paths << '/etc/mcollective/client.cfg'
175
+ end
176
+
177
+ # use the first readable config file, or if none are the first listed
178
+ found = config_paths.find_index { |file| File.readable?(file) } || 0
179
+ return config_paths[found]
180
+ end
181
+
182
+ # Creates a standard options hash
183
+ def self.default_options
184
+ {:verbose => false,
185
+ :disctimeout => nil,
186
+ :timeout => 5,
187
+ :config => config_file_for_user,
188
+ :collective => nil,
189
+ :discovery_method => nil,
190
+ :discovery_options => Config.instance.default_discovery_options,
191
+ :filter => empty_filter}
192
+ end
193
+
194
+ def self.make_subscriptions(agent, type, collective=nil)
195
+ config = Config.instance
196
+
197
+ raise("Unknown target type #{type}") unless [:broadcast, :directed, :reply].include?(type)
198
+
199
+ if collective.nil?
200
+ config.collectives.map do |c|
201
+ {:agent => agent, :type => type, :collective => c}
202
+ end
203
+ else
204
+ raise("Unknown collective '#{collective}' known collectives are '#{config.collectives.join ', '}'") unless config.collectives.include?(collective)
205
+
206
+ [{:agent => agent, :type => type, :collective => collective}]
207
+ end
208
+ end
209
+
210
+ # Helper to subscribe to a topic on multiple collectives or just one
211
+ def self.subscribe(targets)
212
+ connection = PluginManager["connector_plugin"]
213
+
214
+ targets = [targets].flatten
215
+
216
+ targets.each do |target|
217
+ connection.subscribe(target[:agent], target[:type], target[:collective])
218
+ end
219
+ end
220
+
221
+ # Helper to unsubscribe to a topic on multiple collectives or just one
222
+ def self.unsubscribe(targets)
223
+ connection = PluginManager["connector_plugin"]
224
+
225
+ targets = [targets].flatten
226
+
227
+ targets.each do |target|
228
+ connection.unsubscribe(target[:agent], target[:type], target[:collective])
229
+ end
230
+ end
231
+
232
+ # Wrapper around PluginManager.loadclass
233
+ def self.loadclass(klass)
234
+ PluginManager.loadclass(klass)
235
+ end
236
+
237
+ # Parse a fact filter string like foo=bar into the tuple hash thats needed
238
+ def self.parse_fact_string(fact)
239
+ if fact =~ /^([^ ]+?)[ ]*=>[ ]*(.+)/
240
+ return {:fact => $1, :value => $2, :operator => '>=' }
241
+ elsif fact =~ /^([^ ]+?)[ ]*=<[ ]*(.+)/
242
+ return {:fact => $1, :value => $2, :operator => '<=' }
243
+ elsif fact =~ /^([^ ]+?)[ ]*(<=|>=|<|>|!=|==|=~)[ ]*(.+)/
244
+ return {:fact => $1, :value => $3, :operator => $2 }
245
+ elsif fact =~ /^(.+?)[ ]*=[ ]*\/(.+)\/$/
246
+ return {:fact => $1, :value => "/#{$2}/", :operator => '=~' }
247
+ elsif fact =~ /^([^= ]+?)[ ]*=[ ]*(.+)/
248
+ return {:fact => $1, :value => $2, :operator => '==' }
249
+ else
250
+ raise "Could not parse fact #{fact} it does not appear to be in a valid format"
251
+ end
252
+ end
253
+
254
+ # Escapes a string so it's safe to use in system() or backticks
255
+ #
256
+ # Taken from Shellwords#shellescape since it's only in a few ruby versions
257
+ def self.shellescape(str)
258
+ return "''" if str.empty?
259
+
260
+ str = str.dup
261
+
262
+ # Process as a single byte sequence because not all shell
263
+ # implementations are multibyte aware.
264
+ str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
265
+
266
+ # A LF cannot be escaped with a backslash because a backslash + LF
267
+ # combo is regarded as line continuation and simply ignored.
268
+ str.gsub!(/\n/, "'\n'")
269
+
270
+ return str
271
+ end
272
+
273
+ def self.windows?
274
+ !!(RbConfig::CONFIG['host_os'] =~ /mswin|win32|dos|mingw|cygwin/i)
275
+ end
276
+
277
+ # Return color codes, if the config color= option is false
278
+ # just return a empty string
279
+ def self.color(code)
280
+ colorize = Config.instance.color
281
+
282
+ colors = {:red => "",
283
+ :green => "",
284
+ :yellow => "",
285
+ :cyan => "",
286
+ :bold => "",
287
+ :reset => ""}
288
+
289
+ if colorize
290
+ return colors[code] || ""
291
+ else
292
+ return ""
293
+ end
294
+ end
295
+
296
+ # Helper to return a string in specific color
297
+ def self.colorize(code, msg)
298
+ "%s%s%s" % [ color(code), msg, color(:reset) ]
299
+ end
300
+
301
+ # Returns the current ruby version as per RUBY_VERSION, mostly
302
+ # doing this here to aid testing
303
+ def self.ruby_version
304
+ RUBY_VERSION
305
+ end
306
+
307
+ def self.mcollective_version
308
+ MCollective::VERSION
309
+ end
310
+
311
+ # Returns an aligned_string of text relative to the size of the terminal
312
+ # window. If a line in the string exceeds the width of the terminal window
313
+ # the line will be chopped off at the whitespace chacter closest to the
314
+ # end of the line and prepended to the next line, keeping all indentation.
315
+ #
316
+ # The terminal size is detected by default, but custom line widths can
317
+ # passed. All strings will also be left aligned with 5 whitespace characters
318
+ # by default.
319
+ def self.align_text(text, console_cols = nil, preamble = 5)
320
+ unless console_cols
321
+ console_cols = terminal_dimensions[0]
322
+
323
+ # if unknown size we default to the typical unix default
324
+ console_cols = 80 if console_cols == 0
325
+ end
326
+
327
+ console_cols -= preamble
328
+
329
+ # Return unaligned text if console window is too small
330
+ return text if console_cols <= 0
331
+
332
+ # If console is 0 this implies unknown so we assume the common
333
+ # minimal unix configuration of 80 characters
334
+ console_cols = 80 if console_cols <= 0
335
+
336
+ text = text.split("\n")
337
+ piece = ''
338
+ whitespace = 0
339
+
340
+ text.each_with_index do |line, i|
341
+ whitespace = 0
342
+
343
+ while whitespace < line.length && line[whitespace].chr == ' '
344
+ whitespace += 1
345
+ end
346
+
347
+ # If the current line is empty, indent it so that a snippet
348
+ # from the previous line is aligned correctly.
349
+ if line == ""
350
+ line = (" " * whitespace)
351
+ end
352
+
353
+ # If text was snipped from the previous line, prepend it to the
354
+ # current line after any current indentation.
355
+ if piece != ''
356
+ # Reset whitespaces to 0 if there are more whitespaces than there are
357
+ # console columns
358
+ whitespace = 0 if whitespace >= console_cols
359
+
360
+ # If the current line is empty and being prepended to, create a new
361
+ # empty line in the text so that formatting is preserved.
362
+ if text[i + 1] && line == (" " * whitespace)
363
+ text.insert(i + 1, "")
364
+ end
365
+
366
+ # Add the snipped text to the current line
367
+ line.insert(whitespace, "#{piece} ")
368
+ end
369
+
370
+ piece = ''
371
+
372
+ # Compare the line length to the allowed line length.
373
+ # If it exceeds it, snip the offending text from the line
374
+ # and store it so that it can be prepended to the next line.
375
+ if line.length > (console_cols + preamble)
376
+ reverse = console_cols
377
+
378
+ while line[reverse].chr != ' '
379
+ reverse -= 1
380
+ end
381
+
382
+ piece = line.slice!(reverse, (line.length - 1)).lstrip
383
+ end
384
+
385
+ # If a snippet exists when all the columns in the text have been
386
+ # updated, create a new line and append the snippet to it, using
387
+ # the same left alignment as the last line in the text.
388
+ if piece != '' && text[i+1].nil?
389
+ text[i+1] = "#{' ' * (whitespace)}#{piece}"
390
+ piece = ''
391
+ end
392
+
393
+ # Add the preamble to the line and add it to the text
394
+ line = ((' ' * preamble) + line)
395
+ text[i] = line
396
+ end
397
+
398
+ text.join("\n")
399
+ end
400
+
401
+ # Figures out the columns and lines of the current tty
402
+ #
403
+ # Returns [0, 0] if it can't figure it out or if you're
404
+ # not running on a tty
405
+ def self.terminal_dimensions(stdout = STDOUT, environment = ENV)
406
+ return [0, 0] unless stdout.tty?
407
+
408
+ return [80, 40] if Util.windows?
409
+
410
+ if environment["COLUMNS"] && environment["LINES"]
411
+ return [environment["COLUMNS"].to_i, environment["LINES"].to_i]
412
+
413
+ elsif environment["TERM"] && command_in_path?("tput")
414
+ return [`tput cols`.to_i, `tput lines`.to_i]
415
+
416
+ elsif command_in_path?('stty')
417
+ return `stty size`.scan(/\d+/).map {|s| s.to_i }
418
+ else
419
+ return [0, 0]
420
+ end
421
+ rescue
422
+ [0, 0]
423
+ end
424
+
425
+ # Checks in PATH returns true if the command is found
426
+ def self.command_in_path?(command)
427
+ found = ENV["PATH"].split(File::PATH_SEPARATOR).map do |p|
428
+ File.exist?(File.join(p, command))
429
+ end
430
+
431
+ found.include?(true)
432
+ end
433
+
434
+ # compare two software versions as commonly found in
435
+ # package versions.
436
+ #
437
+ # returns 0 if a == b
438
+ # returns -1 if a < b
439
+ # returns 1 if a > b
440
+ #
441
+ # Code originally from Puppet
442
+ def self.versioncmp(version_a, version_b)
443
+ vre = /[-.]|\d+|[^-.\d]+/
444
+ ax = version_a.scan(vre)
445
+ bx = version_b.scan(vre)
446
+
447
+ while (ax.length>0 && bx.length>0)
448
+ a = ax.shift
449
+ b = bx.shift
450
+
451
+ if( a == b ) then next
452
+ elsif (a == '-' && b == '-') then next
453
+ elsif (a == '-') then return -1
454
+ elsif (b == '-') then return 1
455
+ elsif (a == '.' && b == '.') then next
456
+ elsif (a == '.' ) then return -1
457
+ elsif (b == '.' ) then return 1
458
+ elsif (a =~ /^\d+$/ && b =~ /^\d+$/) then
459
+ if( a =~ /^0/ or b =~ /^0/ ) then
460
+ return a.to_s.upcase <=> b.to_s.upcase
461
+ end
462
+ return a.to_i <=> b.to_i
463
+ else
464
+ return a.upcase <=> b.upcase
465
+ end
466
+ end
467
+
468
+ version_a <=> version_b;
469
+ end
470
+
471
+ # we should really use Pathname#absolute? but it's not in all the
472
+ # ruby versions we support and it comes down to roughly this
473
+ def self.absolute_path?(path, separator=File::SEPARATOR, alt_separator=File::ALT_SEPARATOR)
474
+ if alt_separator
475
+ path_matcher = /^([a-zA-Z]:){0,1}[#{Regexp.quote alt_separator}#{Regexp.quote separator}]/
476
+ else
477
+ path_matcher = /^#{Regexp.quote separator}/
478
+ end
479
+
480
+ !!path.match(path_matcher)
481
+ end
482
+
483
+ # Converts a string into a boolean value
484
+ # Strings matching 1,y,yes,true or t will return TrueClass
485
+ # Any other value will return FalseClass
486
+ def self.str_to_bool(val)
487
+ clean_val = val.to_s.strip
488
+ if clean_val =~ /^(1|yes|true|y|t)$/i
489
+ return true
490
+ elsif clean_val =~ /^(0|no|false|n|f)$/i
491
+ return false
492
+ else
493
+ raise("Cannot convert string value '#{clean_val}' into a boolean.")
494
+ end
495
+ end
496
+
497
+ # Looks up the template directory and returns its full path
498
+ def self.templatepath(template_file)
499
+ config_dir = File.dirname(Config.instance.configfile)
500
+ template_path = File.join(config_dir, template_file)
501
+ return template_path if File.exists?(template_path)
502
+
503
+ template_path = File.join("/etc/mcollective", template_file)
504
+ return template_path
505
+ end
506
+
507
+ # subscribe to the direct addressing queue
508
+ def self.subscribe_to_direct_addressing_queue
509
+ subscribe(make_subscriptions("mcollective", :directed))
510
+ end
511
+
512
+ # Get field size for printing
513
+ def self.field_size(elements, min_size=40)
514
+ max_length = elements.max_by { |e| e.length }.length
515
+ max_length > min_size ? max_length : min_size
516
+ end
517
+
518
+ # Calculate number of fields for printing
519
+ def self.field_number(field_size, max_size=90)
520
+ number = (max_size/field_size).to_i
521
+ (number == 0) ? 1 : number
522
+ end
523
+
524
+ def self.get_hidden_input_on_windows()
525
+ require 'Win32API'
526
+ # Hook into getch from crtdll. Keep reading all keys till return
527
+ # or newline is hit.
528
+ # If key is backspace or delete, then delete the character and update
529
+ # the buffer.
530
+ input = ''
531
+ while char = Win32API.new("crtdll", "_getch", [ ], "I").Call do
532
+ break if char == 10 || char == 13 # return or newline
533
+ if char == 127 || char == 8 # backspace and delete
534
+ if input.length > 0
535
+ input.slice!(-1, 1)
536
+ end
537
+ else
538
+ input << char.chr
539
+ end
540
+ end
541
+ char = ''
542
+ input
543
+ end
544
+
545
+ def self.get_hidden_input_on_unix()
546
+ unless $stdin.tty?
547
+ raise 'Could not hook to stdin to hide input. If using SSH, try using -t flag while connecting to server.'
548
+ end
549
+ unless system 'stty -echo -icanon'
550
+ raise 'Could not hide input using stty command.'
551
+ end
552
+ input = $stdin.gets
553
+ ensure
554
+ unless system 'stty echo icanon'
555
+ raise 'Could not enable echoing of input. Try executing `stty echo icanon` to debug.'
556
+ end
557
+ input
558
+ end
559
+
560
+ def self.get_hidden_input(message='Please enter data: ')
561
+ unless message.nil?
562
+ print message
563
+ end
564
+ if versioncmp(ruby_version, '1.9.3') >= 0
565
+ require 'io/console'
566
+ input = $stdin.noecho(&:gets)
567
+ else
568
+ # Use hacks to get hidden input on Ruby <1.9.3
569
+ if self.windows?
570
+ input = self.get_hidden_input_on_windows()
571
+ else
572
+ input = self.get_hidden_input_on_unix()
573
+ end
574
+ end
575
+ input.chomp! if input
576
+ input
577
+ end
578
+ end
579
+ end