choria-mcorpc-support 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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