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.
- checksums.yaml +7 -0
- data/bin/mco +64 -0
- data/lib/mcollective.rb +63 -0
- data/lib/mcollective/agent.rb +5 -0
- data/lib/mcollective/agents.rb +149 -0
- data/lib/mcollective/aggregate.rb +85 -0
- data/lib/mcollective/aggregate/average.ddl +33 -0
- data/lib/mcollective/aggregate/average.rb +29 -0
- data/lib/mcollective/aggregate/base.rb +40 -0
- data/lib/mcollective/aggregate/result.rb +9 -0
- data/lib/mcollective/aggregate/result/base.rb +25 -0
- data/lib/mcollective/aggregate/result/collection_result.rb +19 -0
- data/lib/mcollective/aggregate/result/numeric_result.rb +13 -0
- data/lib/mcollective/aggregate/sum.ddl +33 -0
- data/lib/mcollective/aggregate/sum.rb +18 -0
- data/lib/mcollective/aggregate/summary.ddl +33 -0
- data/lib/mcollective/aggregate/summary.rb +53 -0
- data/lib/mcollective/application.rb +365 -0
- data/lib/mcollective/application/completion.rb +104 -0
- data/lib/mcollective/application/describe_filter.rb +87 -0
- data/lib/mcollective/application/facts.rb +62 -0
- data/lib/mcollective/application/find.rb +23 -0
- data/lib/mcollective/application/help.rb +28 -0
- data/lib/mcollective/application/inventory.rb +344 -0
- data/lib/mcollective/application/ping.rb +82 -0
- data/lib/mcollective/application/plugin.rb +369 -0
- data/lib/mcollective/application/rpc.rb +111 -0
- data/lib/mcollective/applications.rb +134 -0
- data/lib/mcollective/cache.rb +145 -0
- data/lib/mcollective/client.rb +353 -0
- data/lib/mcollective/config.rb +245 -0
- data/lib/mcollective/connector.rb +18 -0
- data/lib/mcollective/connector/base.rb +26 -0
- data/lib/mcollective/data.rb +91 -0
- data/lib/mcollective/data/agent_data.ddl +22 -0
- data/lib/mcollective/data/agent_data.rb +17 -0
- data/lib/mcollective/data/base.rb +67 -0
- data/lib/mcollective/data/collective_data.ddl +20 -0
- data/lib/mcollective/data/collective_data.rb +9 -0
- data/lib/mcollective/data/fact_data.ddl +28 -0
- data/lib/mcollective/data/fact_data.rb +55 -0
- data/lib/mcollective/data/fstat_data.ddl +89 -0
- data/lib/mcollective/data/fstat_data.rb +56 -0
- data/lib/mcollective/data/result.rb +45 -0
- data/lib/mcollective/ddl.rb +113 -0
- data/lib/mcollective/ddl/agentddl.rb +253 -0
- data/lib/mcollective/ddl/base.rb +217 -0
- data/lib/mcollective/ddl/dataddl.rb +56 -0
- data/lib/mcollective/ddl/discoveryddl.rb +52 -0
- data/lib/mcollective/ddl/validatorddl.rb +6 -0
- data/lib/mcollective/discovery.rb +143 -0
- data/lib/mcollective/discovery/flatfile.ddl +11 -0
- data/lib/mcollective/discovery/flatfile.rb +48 -0
- data/lib/mcollective/discovery/mc.ddl +11 -0
- data/lib/mcollective/discovery/mc.rb +30 -0
- data/lib/mcollective/discovery/stdin.ddl +11 -0
- data/lib/mcollective/discovery/stdin.rb +68 -0
- data/lib/mcollective/exceptions.rb +28 -0
- data/lib/mcollective/facts.rb +39 -0
- data/lib/mcollective/facts/base.rb +100 -0
- data/lib/mcollective/facts/yaml_facts.rb +65 -0
- data/lib/mcollective/generators.rb +7 -0
- data/lib/mcollective/generators/agent_generator.rb +51 -0
- data/lib/mcollective/generators/base.rb +46 -0
- data/lib/mcollective/generators/data_generator.rb +51 -0
- data/lib/mcollective/generators/templates/action_snippet.erb +13 -0
- data/lib/mcollective/generators/templates/data_input_snippet.erb +7 -0
- data/lib/mcollective/generators/templates/ddl.erb +8 -0
- data/lib/mcollective/generators/templates/plugin.erb +7 -0
- data/lib/mcollective/log.rb +118 -0
- data/lib/mcollective/logger.rb +5 -0
- data/lib/mcollective/logger/base.rb +77 -0
- data/lib/mcollective/logger/console_logger.rb +61 -0
- data/lib/mcollective/logger/file_logger.rb +53 -0
- data/lib/mcollective/logger/syslog_logger.rb +53 -0
- data/lib/mcollective/matcher.rb +224 -0
- data/lib/mcollective/matcher/parser.rb +128 -0
- data/lib/mcollective/matcher/scanner.rb +241 -0
- data/lib/mcollective/message.rb +248 -0
- data/lib/mcollective/monkey_patches.rb +152 -0
- data/lib/mcollective/optionparser.rb +197 -0
- data/lib/mcollective/pluginmanager.rb +180 -0
- data/lib/mcollective/pluginpackager.rb +98 -0
- data/lib/mcollective/pluginpackager/agent_definition.rb +94 -0
- data/lib/mcollective/pluginpackager/debpackage_packager.rb +237 -0
- data/lib/mcollective/pluginpackager/modulepackage_packager.rb +127 -0
- data/lib/mcollective/pluginpackager/ospackage_packager.rb +59 -0
- data/lib/mcollective/pluginpackager/rpmpackage_packager.rb +180 -0
- data/lib/mcollective/pluginpackager/standard_definition.rb +69 -0
- data/lib/mcollective/pluginpackager/templates/debian/Makefile.erb +7 -0
- data/lib/mcollective/pluginpackager/templates/debian/changelog.erb +5 -0
- data/lib/mcollective/pluginpackager/templates/debian/compat.erb +1 -0
- data/lib/mcollective/pluginpackager/templates/debian/control.erb +15 -0
- data/lib/mcollective/pluginpackager/templates/debian/copyright.erb +8 -0
- data/lib/mcollective/pluginpackager/templates/debian/rules.erb +6 -0
- data/lib/mcollective/pluginpackager/templates/module/Modulefile.erb +5 -0
- data/lib/mcollective/pluginpackager/templates/module/README.md.erb +37 -0
- data/lib/mcollective/pluginpackager/templates/module/_manifest.pp.erb +9 -0
- data/lib/mcollective/pluginpackager/templates/redhat/rpm_spec.erb +63 -0
- data/lib/mcollective/registration/base.rb +91 -0
- data/lib/mcollective/rpc.rb +182 -0
- data/lib/mcollective/rpc/actionrunner.rb +158 -0
- data/lib/mcollective/rpc/agent.rb +374 -0
- data/lib/mcollective/rpc/audit.rb +38 -0
- data/lib/mcollective/rpc/client.rb +1066 -0
- data/lib/mcollective/rpc/helpers.rb +321 -0
- data/lib/mcollective/rpc/progress.rb +63 -0
- data/lib/mcollective/rpc/reply.rb +87 -0
- data/lib/mcollective/rpc/request.rb +86 -0
- data/lib/mcollective/rpc/result.rb +90 -0
- data/lib/mcollective/rpc/stats.rb +294 -0
- data/lib/mcollective/runnerstats.rb +90 -0
- data/lib/mcollective/security.rb +26 -0
- data/lib/mcollective/security/base.rb +244 -0
- data/lib/mcollective/shell.rb +126 -0
- data/lib/mcollective/ssl.rb +285 -0
- data/lib/mcollective/util.rb +579 -0
- data/lib/mcollective/validator.rb +85 -0
- data/lib/mcollective/validator/array_validator.ddl +7 -0
- data/lib/mcollective/validator/array_validator.rb +9 -0
- data/lib/mcollective/validator/ipv4address_validator.ddl +7 -0
- data/lib/mcollective/validator/ipv4address_validator.rb +16 -0
- data/lib/mcollective/validator/ipv6address_validator.ddl +7 -0
- data/lib/mcollective/validator/ipv6address_validator.rb +16 -0
- data/lib/mcollective/validator/length_validator.ddl +7 -0
- data/lib/mcollective/validator/length_validator.rb +11 -0
- data/lib/mcollective/validator/regex_validator.ddl +7 -0
- data/lib/mcollective/validator/regex_validator.rb +9 -0
- data/lib/mcollective/validator/shellsafe_validator.ddl +7 -0
- data/lib/mcollective/validator/shellsafe_validator.rb +13 -0
- data/lib/mcollective/validator/typecheck_validator.ddl +7 -0
- data/lib/mcollective/validator/typecheck_validator.rb +28 -0
- 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 => "[31m",
|
|
283
|
+
:green => "[32m",
|
|
284
|
+
:yellow => "[33m",
|
|
285
|
+
:cyan => "[36m",
|
|
286
|
+
:bold => "[1m",
|
|
287
|
+
:reset => "[0m"}
|
|
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
|