right_agent 2.0.7-x86-mingw32

Sign up to get free protection for your applications and to get access to all the features.
Files changed (176) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +82 -0
  3. data/Rakefile +113 -0
  4. data/lib/right_agent.rb +59 -0
  5. data/lib/right_agent/actor.rb +182 -0
  6. data/lib/right_agent/actor_registry.rb +76 -0
  7. data/lib/right_agent/actors/agent_manager.rb +232 -0
  8. data/lib/right_agent/agent.rb +1149 -0
  9. data/lib/right_agent/agent_config.rb +480 -0
  10. data/lib/right_agent/agent_identity.rb +210 -0
  11. data/lib/right_agent/agent_tag_manager.rb +237 -0
  12. data/lib/right_agent/audit_formatter.rb +107 -0
  13. data/lib/right_agent/clients.rb +31 -0
  14. data/lib/right_agent/clients/api_client.rb +383 -0
  15. data/lib/right_agent/clients/auth_client.rb +247 -0
  16. data/lib/right_agent/clients/balanced_http_client.rb +369 -0
  17. data/lib/right_agent/clients/base_retry_client.rb +495 -0
  18. data/lib/right_agent/clients/right_http_client.rb +279 -0
  19. data/lib/right_agent/clients/router_client.rb +493 -0
  20. data/lib/right_agent/command.rb +30 -0
  21. data/lib/right_agent/command/agent_manager_commands.rb +150 -0
  22. data/lib/right_agent/command/command_client.rb +136 -0
  23. data/lib/right_agent/command/command_constants.rb +33 -0
  24. data/lib/right_agent/command/command_io.rb +126 -0
  25. data/lib/right_agent/command/command_parser.rb +87 -0
  26. data/lib/right_agent/command/command_runner.rb +118 -0
  27. data/lib/right_agent/command/command_serializer.rb +63 -0
  28. data/lib/right_agent/connectivity_checker.rb +179 -0
  29. data/lib/right_agent/console.rb +65 -0
  30. data/lib/right_agent/core_payload_types.rb +44 -0
  31. data/lib/right_agent/core_payload_types/cookbook.rb +61 -0
  32. data/lib/right_agent/core_payload_types/cookbook_position.rb +46 -0
  33. data/lib/right_agent/core_payload_types/cookbook_repository.rb +116 -0
  34. data/lib/right_agent/core_payload_types/cookbook_sequence.rb +70 -0
  35. data/lib/right_agent/core_payload_types/dev_repositories.rb +100 -0
  36. data/lib/right_agent/core_payload_types/dev_repository.rb +76 -0
  37. data/lib/right_agent/core_payload_types/event_categories.rb +38 -0
  38. data/lib/right_agent/core_payload_types/executable_bundle.rb +130 -0
  39. data/lib/right_agent/core_payload_types/login_policy.rb +72 -0
  40. data/lib/right_agent/core_payload_types/login_user.rb +79 -0
  41. data/lib/right_agent/core_payload_types/planned_volume.rb +94 -0
  42. data/lib/right_agent/core_payload_types/recipe_instantiation.rb +73 -0
  43. data/lib/right_agent/core_payload_types/repositories_bundle.rb +50 -0
  44. data/lib/right_agent/core_payload_types/right_script_attachment.rb +95 -0
  45. data/lib/right_agent/core_payload_types/right_script_instantiation.rb +94 -0
  46. data/lib/right_agent/core_payload_types/runlist_policy.rb +44 -0
  47. data/lib/right_agent/core_payload_types/secure_document.rb +66 -0
  48. data/lib/right_agent/core_payload_types/secure_document_location.rb +63 -0
  49. data/lib/right_agent/core_payload_types/software_repository_instantiation.rb +61 -0
  50. data/lib/right_agent/daemonize.rb +35 -0
  51. data/lib/right_agent/dispatched_cache.rb +109 -0
  52. data/lib/right_agent/dispatcher.rb +272 -0
  53. data/lib/right_agent/enrollment_result.rb +221 -0
  54. data/lib/right_agent/exceptions.rb +87 -0
  55. data/lib/right_agent/history.rb +145 -0
  56. data/lib/right_agent/log.rb +460 -0
  57. data/lib/right_agent/minimal.rb +46 -0
  58. data/lib/right_agent/monkey_patches.rb +30 -0
  59. data/lib/right_agent/monkey_patches/ruby_patch.rb +55 -0
  60. data/lib/right_agent/monkey_patches/ruby_patch/array_patch.rb +29 -0
  61. data/lib/right_agent/monkey_patches/ruby_patch/darwin_patch.rb +24 -0
  62. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch.rb +24 -0
  63. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch/file_patch.rb +30 -0
  64. data/lib/right_agent/monkey_patches/ruby_patch/object_patch.rb +49 -0
  65. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch.rb +32 -0
  66. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/file_patch.rb +60 -0
  67. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/process_patch.rb +63 -0
  68. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/stdio_patch.rb +27 -0
  69. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/time_patch.rb +55 -0
  70. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/win32ole_patch.rb +34 -0
  71. data/lib/right_agent/multiplexer.rb +102 -0
  72. data/lib/right_agent/offline_handler.rb +270 -0
  73. data/lib/right_agent/operation_result.rb +300 -0
  74. data/lib/right_agent/packets.rb +673 -0
  75. data/lib/right_agent/payload_formatter.rb +104 -0
  76. data/lib/right_agent/pending_requests.rb +128 -0
  77. data/lib/right_agent/pid_file.rb +159 -0
  78. data/lib/right_agent/platform.rb +770 -0
  79. data/lib/right_agent/platform/unix/darwin/platform.rb +102 -0
  80. data/lib/right_agent/platform/unix/linux/platform.rb +305 -0
  81. data/lib/right_agent/platform/unix/platform.rb +226 -0
  82. data/lib/right_agent/platform/windows/mingw/platform.rb +447 -0
  83. data/lib/right_agent/platform/windows/mswin/platform.rb +236 -0
  84. data/lib/right_agent/platform/windows/platform.rb +1808 -0
  85. data/lib/right_agent/protocol_version_mixin.rb +69 -0
  86. data/lib/right_agent/retryable_request.rb +195 -0
  87. data/lib/right_agent/scripts/agent_controller.rb +543 -0
  88. data/lib/right_agent/scripts/agent_deployer.rb +400 -0
  89. data/lib/right_agent/scripts/common_parser.rb +160 -0
  90. data/lib/right_agent/scripts/log_level_manager.rb +192 -0
  91. data/lib/right_agent/scripts/stats_manager.rb +268 -0
  92. data/lib/right_agent/scripts/usage.rb +58 -0
  93. data/lib/right_agent/secure_identity.rb +92 -0
  94. data/lib/right_agent/security.rb +32 -0
  95. data/lib/right_agent/security/cached_certificate_store_proxy.rb +77 -0
  96. data/lib/right_agent/security/certificate.rb +102 -0
  97. data/lib/right_agent/security/certificate_cache.rb +89 -0
  98. data/lib/right_agent/security/distinguished_name.rb +56 -0
  99. data/lib/right_agent/security/encrypted_document.rb +83 -0
  100. data/lib/right_agent/security/rsa_key_pair.rb +76 -0
  101. data/lib/right_agent/security/signature.rb +86 -0
  102. data/lib/right_agent/security/static_certificate_store.rb +85 -0
  103. data/lib/right_agent/sender.rb +792 -0
  104. data/lib/right_agent/serialize.rb +29 -0
  105. data/lib/right_agent/serialize/message_pack.rb +107 -0
  106. data/lib/right_agent/serialize/secure_serializer.rb +151 -0
  107. data/lib/right_agent/serialize/secure_serializer_initializer.rb +47 -0
  108. data/lib/right_agent/serialize/serializable.rb +151 -0
  109. data/lib/right_agent/serialize/serializer.rb +159 -0
  110. data/lib/right_agent/subprocess.rb +38 -0
  111. data/lib/right_agent/tracer.rb +124 -0
  112. data/right_agent.gemspec +101 -0
  113. data/spec/actor_registry_spec.rb +80 -0
  114. data/spec/actor_spec.rb +162 -0
  115. data/spec/agent_config_spec.rb +235 -0
  116. data/spec/agent_identity_spec.rb +78 -0
  117. data/spec/agent_spec.rb +734 -0
  118. data/spec/agent_tag_manager_spec.rb +319 -0
  119. data/spec/clients/api_client_spec.rb +423 -0
  120. data/spec/clients/auth_client_spec.rb +272 -0
  121. data/spec/clients/balanced_http_client_spec.rb +576 -0
  122. data/spec/clients/base_retry_client_spec.rb +635 -0
  123. data/spec/clients/router_client_spec.rb +594 -0
  124. data/spec/clients/spec_helper.rb +111 -0
  125. data/spec/command/agent_manager_commands_spec.rb +51 -0
  126. data/spec/command/command_io_spec.rb +93 -0
  127. data/spec/command/command_parser_spec.rb +79 -0
  128. data/spec/command/command_runner_spec.rb +107 -0
  129. data/spec/command/command_serializer_spec.rb +51 -0
  130. data/spec/connectivity_checker_spec.rb +83 -0
  131. data/spec/core_payload_types/dev_repositories_spec.rb +64 -0
  132. data/spec/core_payload_types/dev_repository_spec.rb +33 -0
  133. data/spec/core_payload_types/executable_bundle_spec.rb +67 -0
  134. data/spec/core_payload_types/login_user_spec.rb +102 -0
  135. data/spec/core_payload_types/recipe_instantiation_spec.rb +81 -0
  136. data/spec/core_payload_types/right_script_attachment_spec.rb +65 -0
  137. data/spec/core_payload_types/right_script_instantiation_spec.rb +79 -0
  138. data/spec/core_payload_types/spec_helper.rb +23 -0
  139. data/spec/dispatched_cache_spec.rb +136 -0
  140. data/spec/dispatcher_spec.rb +324 -0
  141. data/spec/enrollment_result_spec.rb +53 -0
  142. data/spec/history_spec.rb +246 -0
  143. data/spec/log_spec.rb +192 -0
  144. data/spec/monkey_patches/eventmachine_spec.rb +62 -0
  145. data/spec/multiplexer_spec.rb +48 -0
  146. data/spec/offline_handler_spec.rb +340 -0
  147. data/spec/operation_result_spec.rb +208 -0
  148. data/spec/packets_spec.rb +461 -0
  149. data/spec/pending_requests_spec.rb +136 -0
  150. data/spec/platform/spec_helper.rb +216 -0
  151. data/spec/platform/unix/darwin/platform_spec.rb +181 -0
  152. data/spec/platform/unix/linux/platform_spec.rb +540 -0
  153. data/spec/platform/unix/spec_helper.rb +149 -0
  154. data/spec/platform/windows/mingw/platform_spec.rb +222 -0
  155. data/spec/platform/windows/mswin/platform_spec.rb +259 -0
  156. data/spec/platform/windows/spec_helper.rb +720 -0
  157. data/spec/retryable_request_spec.rb +306 -0
  158. data/spec/secure_identity_spec.rb +50 -0
  159. data/spec/security/cached_certificate_store_proxy_spec.rb +62 -0
  160. data/spec/security/certificate_cache_spec.rb +71 -0
  161. data/spec/security/certificate_spec.rb +49 -0
  162. data/spec/security/distinguished_name_spec.rb +46 -0
  163. data/spec/security/encrypted_document_spec.rb +55 -0
  164. data/spec/security/rsa_key_pair_spec.rb +55 -0
  165. data/spec/security/signature_spec.rb +66 -0
  166. data/spec/security/static_certificate_store_spec.rb +58 -0
  167. data/spec/sender_spec.rb +1045 -0
  168. data/spec/serialize/message_pack_spec.rb +131 -0
  169. data/spec/serialize/secure_serializer_spec.rb +132 -0
  170. data/spec/serialize/serializable_spec.rb +90 -0
  171. data/spec/serialize/serializer_spec.rb +197 -0
  172. data/spec/spec.opts +2 -0
  173. data/spec/spec.win32.opts +1 -0
  174. data/spec/spec_helper.rb +130 -0
  175. data/spec/tracer_spec.rb +114 -0
  176. metadata +447 -0
@@ -0,0 +1,56 @@
1
+ #
2
+ # Copyright (c) 2009-2011 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ module RightScale
24
+
25
+ # Build X.509 compliant distinguished names
26
+ # Distinguished names are used to describe both a certificate issuer and subject
27
+ class DistinguishedName
28
+
29
+ # Initialize distinguished name from hash
30
+ # e.g.:
31
+ # { 'C' => 'US',
32
+ # 'ST' => 'California',
33
+ # 'L' => 'Santa Barbara',
34
+ # 'O' => 'RightScale',
35
+ # 'OU' => 'Certification Services',
36
+ # 'CN' => 'rightscale.com/emailAddress=cert@rightscale.com' }
37
+ #
38
+ def initialize(hash)
39
+ @value = hash
40
+ end
41
+
42
+ # Conversion to OpenSSL X509 DN
43
+ def to_x509
44
+ if @value
45
+ OpenSSL::X509::Name.new(@value.to_a, OpenSSL::X509::Name::OBJECT_TYPE_TEMPLATE)
46
+ end
47
+ end
48
+
49
+ # Human readable form
50
+ def to_s
51
+ '/' + @value.to_a.collect { |p| p.join('=') }.join('/') if @value
52
+ end
53
+
54
+ end # DistinguishedName
55
+
56
+ end # RightScale
@@ -0,0 +1,83 @@
1
+ #
2
+ # Copyright (c) 2009-2011 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ module RightScale
24
+
25
+ # Represents a signed an encrypted document that can be later decrypted using
26
+ # the right private key and whose signature can be verified using the right
27
+ # cert.
28
+ # This class can be used both to encrypt and sign data and to then check the
29
+ # signature and decrypt an encrypted document.
30
+ class EncryptedDocument
31
+
32
+ # Encrypt and sign data using certificate and key pair
33
+ #
34
+ # === Parameters
35
+ # data(String):: Data to be encrypted
36
+ # certs(Array|Certificate):: Target recipient certificates used to encrypt data
37
+ # cipher(Cipher):: Cipher used for encryption, AES 256 CBC by default
38
+ def initialize(data, certs, cipher = 'AES-256-CBC')
39
+ cipher = OpenSSL::Cipher::Cipher.new(cipher)
40
+ certs = [ certs ] unless certs.respond_to?(:collect)
41
+ raw_certs = certs.collect { |c| c.raw_cert }
42
+ @pkcs7 = OpenSSL::PKCS7.encrypt(raw_certs, data, cipher, OpenSSL::PKCS7::BINARY)
43
+ end
44
+
45
+ # Initialize from encrypted data
46
+ #
47
+ # === Parameters
48
+ # encrypted_data(String):: Encrypted data
49
+ #
50
+ # === Return
51
+ # doc(EncryptedDocument):: Encrypted document
52
+ def self.from_data(encrypted_data)
53
+ doc = EncryptedDocument.allocate
54
+ doc.instance_variable_set(:@pkcs7, RightScale::PKCS7.new(encrypted_data))
55
+ doc
56
+ end
57
+
58
+ # Encrypted data in PEM (base64) or DER (binary) format
59
+ #
60
+ # === Parameters
61
+ # format(Symbol):: Encode format: :pem or :der, defaults to :pem
62
+ #
63
+ # === Return
64
+ # (String):: Encrypted data
65
+ def encrypted_data(format = :pem)
66
+ format == :pem ? @pkcs7.to_pem : @pkcs7.to_der
67
+ end
68
+
69
+ # Decrypted data
70
+ #
71
+ # === Parameters
72
+ # key(RsaKeyPair):: Key pair used for decryption
73
+ # cert(Certificate):: Certificate to use for decryption
74
+ #
75
+ # === Return
76
+ # (String):: Decrypted data
77
+ def decrypted_data(key, cert)
78
+ @pkcs7.decrypt(key.raw_key, cert.raw_cert)
79
+ end
80
+
81
+ end # EncryptedDocument
82
+
83
+ end # RightScale
@@ -0,0 +1,76 @@
1
+ #
2
+ # Copyright (c) 2009-2011 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ module RightScale
24
+
25
+ # Allows generating RSA key pairs and extracting public key component
26
+ # Note: Creating a RSA key pair can take a fair amount of time (seconds)
27
+ class RsaKeyPair
28
+
29
+ DEFAULT_LENGTH = 2048
30
+
31
+ # Underlying OpenSSL keys
32
+ attr_reader :raw_key
33
+
34
+ # Create new RSA key pair using 'length' bits
35
+ def initialize(length = DEFAULT_LENGTH)
36
+ @raw_key = OpenSSL::PKey::RSA.generate(length)
37
+ end
38
+
39
+ # Does key pair include private key?
40
+ def has_private?
41
+ raw_key.private?
42
+ end
43
+
44
+ # New RsaKeyPair instance with identical public key but no private key
45
+ def to_public
46
+ RsaKeyPair.from_data(raw_key.public_key.to_pem)
47
+ end
48
+
49
+ # Key material in PEM format
50
+ def data
51
+ raw_key.to_pem
52
+ end
53
+ alias :to_s :data
54
+
55
+ # Load key pair previously serialized via 'data'
56
+ def self.from_data(data)
57
+ res = RsaKeyPair.allocate
58
+ res.instance_variable_set(:@raw_key, OpenSSL::PKey::RSA.new(data))
59
+ res
60
+ end
61
+
62
+ # Load key from file
63
+ def self.load(file)
64
+ from_data(File.read(file))
65
+ end
66
+
67
+ # Save key to file in PEM format
68
+ def save(file)
69
+ File.open(file, "w") do |f|
70
+ f.write(@raw_key.to_pem)
71
+ end
72
+ end
73
+
74
+ end # RsaKeyPair
75
+
76
+ end # RightScale
@@ -0,0 +1,86 @@
1
+ #
2
+ # Copyright (c) 2009-2011 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ if RUBY_VERSION < '1.8.7'
24
+ RightScale::PKCS7 = OpenSSL::PKCS7::PKCS7
25
+ else
26
+ RightScale::PKCS7 = OpenSSL::PKCS7
27
+ end
28
+
29
+ module RightScale
30
+
31
+ # Signature that can be validated against certificates
32
+ class Signature
33
+
34
+ FLAGS = OpenSSL::PKCS7::NOCERTS || OpenSSL::PKCS7::BINARY || OpenSSL::PKCS7::NOATTR || OpenSSL::PKCS7::NOSMIMECAP || OpenSSL::PKCS7::DETACH
35
+
36
+ # Create signature using certificate and key pair.
37
+ #
38
+ # === Parameters
39
+ # data(String):: Data to be signed
40
+ # cert(Certificate):: Certificate used for signature
41
+ # key(RsaKeyPair):: Key pair used for signature
42
+ def initialize(data, cert, key)
43
+ @p7 = OpenSSL::PKCS7.sign(cert.raw_cert, key.raw_key, data, [], FLAGS)
44
+ @store = OpenSSL::X509::Store.new
45
+ end
46
+
47
+ # Load signature from previously serialized data
48
+ #
49
+ # === Parameters
50
+ # data(String):: Serialized data
51
+ #
52
+ # === Return
53
+ # sig(Signature):: Signature for data
54
+ def self.from_data(data)
55
+ sig = Signature.allocate
56
+ sig.instance_variable_set(:@p7, RightScale::PKCS7.new(data))
57
+ sig.instance_variable_set(:@store, OpenSSL::X509::Store.new)
58
+ sig
59
+ end
60
+
61
+ # Check whether signature was created using cert
62
+ #
63
+ # === Parameters
64
+ # cert(Certificate):: Certificate
65
+ #
66
+ # === Return
67
+ # (Boolean):: true if created using given cert, otherwise false
68
+ def match?(cert)
69
+ @p7.verify([cert.raw_cert], @store, nil, OpenSSL::PKCS7::NOVERIFY)
70
+ end
71
+
72
+ # Signature data in PEM or DER format
73
+ #
74
+ # === Parameters
75
+ # format(Symbol):: Encode format: :pem or :der, defaults to :pem
76
+ #
77
+ # === Return
78
+ # (String):: Signature
79
+ def data(format = :pem)
80
+ format == :pem ? @p7.to_pem : @p7.to_der
81
+ end
82
+ alias :to_s :data
83
+
84
+ end # Signature
85
+
86
+ end # RightScale
@@ -0,0 +1,85 @@
1
+ #
2
+ # Copyright (c) 2009-2013 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ module RightScale
24
+
25
+ # Simple certificate store that serves a static set of certificates and one key
26
+ class StaticCertificateStore
27
+
28
+ # Initialize store
29
+ #
30
+ # === Parameters
31
+ # receiver_cert(Certificate):: Certificate for decrypting serialized data being received
32
+ # receiver_key(RsaKeyPair):: Key corresponding to specified cert
33
+ # signer_certs(Array|Certificate):: Signer certificate(s) used when loading data to
34
+ # check the digital signature. The signature associated with the serialized data
35
+ # needs to match with one of the signer certificates for loading to succeed.
36
+ # target_certs(Array|Certificate):: Target certificate(s) used when serializing
37
+ # data for encryption. Loading the data can only be done through serializers that
38
+ # have been initialized with a certificate that's in the target certificates
39
+ # if encryption is enabled.
40
+ def initialize(receiver_cert, receiver_key, signer_certs, target_certs)
41
+ @receiver_cert = receiver_cert
42
+ @receiver_key = receiver_key
43
+ signer_certs = [ signer_certs ] unless signer_certs.respond_to?(:each)
44
+ @signer_certs = signer_certs
45
+ target_certs = [ target_certs ] unless target_certs.respond_to?(:each)
46
+ @target_certs = target_certs
47
+ end
48
+
49
+ # Retrieve signer certificates for use in verifying a signature
50
+ #
51
+ # === Parameters
52
+ # id(String):: Serialized identity of signer, ignored
53
+ #
54
+ # === Return
55
+ # (Array|Certificate):: Signer certificates
56
+ def get_signer(id)
57
+ @signer_certs
58
+ end
59
+
60
+ # Retrieve certificates of target for encryption
61
+ #
62
+ # === Parameters
63
+ # packet(RightScale::Packet):: Packet containing target identity, ignored
64
+ #
65
+ # === Return
66
+ # (Array|Certificate):: Target certificates
67
+ def get_target(packet)
68
+ @target_certs
69
+ end
70
+
71
+ # Retrieve receiver's certificate and key for decryption
72
+ #
73
+ # === Parameters
74
+ # id(String|nil):: Optional identifier of source of data for use
75
+ # in determining who is the receiver, ignored
76
+ #
77
+ # === Return
78
+ # (Array):: Certificate and key
79
+ def get_receiver(id)
80
+ [@receiver_cert, @receiver_key]
81
+ end
82
+
83
+ end # StaticCertificateStore
84
+
85
+ end # RightScale
@@ -0,0 +1,792 @@
1
+ #
2
+ # Copyright (c) 2009-2013 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ module RightScale
24
+
25
+ # TODO require target to be hash or nil only once cleanup string values in RightLink usage
26
+ # TODO require payload to be hash or nil only once cleanup RightApi and RightLink usage
27
+
28
+ # This class allows sending requests to agents via RightNet
29
+ # It is used by Actor.request which is used by actors that need to send requests to remote agents
30
+ # If requested, it will queue requests when there are is no RightNet connection
31
+ class Sender
32
+
33
+ include OperationResultHelper
34
+
35
+ class SendFailure < RuntimeError; end
36
+ class TemporarilyOffline < RuntimeError; end
37
+
38
+ # Factor used on each retry iteration to achieve exponential backoff
39
+ RETRY_BACKOFF_FACTOR = 3
40
+
41
+ # (PendingRequests) Requests waiting for a response
42
+ attr_reader :pending_requests
43
+
44
+ # (OfflineHandler) Handler for requests when client disconnected
45
+ attr_reader :offline_handler
46
+
47
+ # (ConnectivityChecker) AMQP broker connection checker
48
+ attr_reader :connectivity_checker
49
+
50
+ # (String) Identity of the associated agent
51
+ attr_reader :identity
52
+
53
+ # (Agent) Associated agent
54
+ attr_reader :agent
55
+
56
+ # (Symbol) RightNet communication mode: :http or :amqp
57
+ attr_reader :mode
58
+
59
+ # For direct access to current sender
60
+ #
61
+ # === Return
62
+ # (Sender):: This sender instance if defined, otherwise nil
63
+ def self.instance
64
+ @@instance if defined?(@@instance)
65
+ end
66
+
67
+ # Initialize sender
68
+ #
69
+ # === Parameters
70
+ # agent(Agent):: Agent using this sender; uses its identity, client, and following options:
71
+ # :exception_callback(Proc):: Callback with following parameters that is activated on exception events:
72
+ # exception(Exception):: Exception
73
+ # message(Packet):: Message being processed
74
+ # agent(Agent):: Reference to agent
75
+ # :offline_queueing(Boolean):: Whether to queue request if client currently disconnected,
76
+ # also requires agent invocation of initialize_offline_queue and start_offline_queue methods below,
77
+ # as well as enable_offline_mode and disable_offline_mode as client connection status changes
78
+ # :ping_interval(Integer):: Minimum number of seconds since last message receipt to ping RightNet
79
+ # to check connectivity, defaults to 0 meaning do not ping
80
+ # :restart_callback(Proc):: Callback that is activated on each restart vote with votes being initiated
81
+ # by offline queue exceeding MAX_QUEUED_REQUESTS or by repeated failures to access RightNet when online
82
+ # :retry_timeout(Numeric):: Maximum number of seconds to retry request before give up
83
+ # :retry_interval(Numeric):: Number of seconds before initial request retry, increases exponentially
84
+ # :time_to_live(Integer):: Number of seconds before a request expires and is to be ignored
85
+ # by the receiver, 0 means never expire
86
+ # :async_response(Boolean):: Whether to handle responses asynchronously or to handle them immediately
87
+ # upon arrival (for use by applications that were written expecting asynchronous AMQP responses)
88
+ # :secure(Boolean):: true indicates to use Security features of rabbitmq to restrict agents to themselves
89
+ def initialize(agent)
90
+ @agent = agent
91
+ @identity = @agent.identity
92
+ @options = @agent.options || {}
93
+ @mode = @agent.mode
94
+ @request_queue = @agent.request_queue
95
+ @secure = @options[:secure]
96
+ @retry_timeout = RightSupport::Stats.nil_if_zero(@options[:retry_timeout])
97
+ @retry_interval = RightSupport::Stats.nil_if_zero(@options[:retry_interval])
98
+ @pending_requests = PendingRequests.new
99
+ @terminating = nil
100
+ reset_stats
101
+ @offline_handler = OfflineHandler.new(@options[:restart_callback], @offline_stats)
102
+ @connectivity_checker = if @mode == :amqp
103
+ # Only need connectivity checker for AMQP broker since RightHttpClient does its own checking
104
+ # via periodic session renewal
105
+ ConnectivityChecker.new(self, @options[:ping_interval] || 0, @ping_stats, @exception_stats)
106
+ end
107
+ @@instance = self
108
+ end
109
+
110
+ # Initialize the offline queue
111
+ # All requests sent prior to running this initialization are queued if offline
112
+ # queueing is enabled and then are sent once this initialization has run
113
+ # All requests following this call and prior to calling start_offline_queue
114
+ # are prepended to the request queue
115
+ #
116
+ # === Return
117
+ # true:: Always return true
118
+ def initialize_offline_queue
119
+ @offline_handler.init if @options[:offline_queueing]
120
+ end
121
+
122
+ # Switch offline queueing to online mode and flush all buffered messages
123
+ #
124
+ # === Return
125
+ # true:: Always return true
126
+ def start_offline_queue
127
+ @offline_handler.start if @options[:offline_queueing]
128
+ end
129
+
130
+ # Switch to offline mode
131
+ # In this mode requests are queued in memory rather than sent
132
+ # Idempotent
133
+ #
134
+ # === Return
135
+ # true:: Always return true
136
+ def enable_offline_mode
137
+ @offline_handler.enable if @options[:offline_queueing]
138
+ end
139
+
140
+ # Switch back to sending requests after in memory queue gets flushed
141
+ # Idempotent
142
+ #
143
+ # === Return
144
+ # true:: Always return true
145
+ def disable_offline_mode
146
+ @offline_handler.disable if @options[:offline_queueing]
147
+ end
148
+
149
+ # Determine whether currently connected to RightNet via client
150
+ #
151
+ # === Return
152
+ # (Boolean):: true if offline or if client disconnected, otherwise false
153
+ def connected?
154
+ @mode == :http ? @agent.client.connected? : @agent.client.connected.size == 0
155
+ end
156
+
157
+ # Update the time this agent last received a request or response message
158
+ # Also forward this message receipt notification to any callbacks that have registered
159
+ #
160
+ # === Block
161
+ # Optional block without parameters that is activated when a message is received
162
+ #
163
+ # === Return
164
+ # true:: Always return true
165
+ def message_received(&callback)
166
+ @connectivity_checker.message_received(&callback) if @connectivity_checker
167
+ end
168
+
169
+ # Send a request to a single target or multiple targets with no response expected other
170
+ # than routing failures
171
+ # Persist the request en route to reduce the chance of it being lost at the expense of some
172
+ # additional network overhead
173
+ # Enqueue the request if the target is not currently available
174
+ # Never automatically retry the request if there is the possibility of it being duplicated
175
+ # Set time-to-live to be forever
176
+ #
177
+ # === Parameters
178
+ # type(String):: Dispatch route for the request; typically identifies actor and action
179
+ # payload(Object):: Data to be sent with marshalling en route
180
+ # target(Hash|NilClass) Target for request, which may be a specific agent (using :agent_id),
181
+ # potentially multiple targets (using :tags, :scope, :selector), or nil to route solely
182
+ # using type:
183
+ # :agent_id(String):: serialized identity of specific target
184
+ # :tags(Array):: Tags that must all be associated with a target for it to be selected
185
+ # :scope(Hash):: Scoping to be used to restrict routing
186
+ # :account(Integer):: Restrict to agents with this account id
187
+ # :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
188
+ # ones with no shard id
189
+ # :selector(Symbol):: Which of the matched targets to be selected, either :any or :all,
190
+ # defaults to :any
191
+ # token(String|NilClass):: Token uniquely identifying request; defaults to random generated
192
+ #
193
+ # === Block
194
+ # Optional block used to process routing responses asynchronously with the following parameter:
195
+ # result(Result):: Response with an OperationResult of SUCCESS, RETRY, NON_DELIVERY, or ERROR,
196
+ # with an initial SUCCESS response containing the targets to which the request was sent
197
+ # and any additional responses indicating any failures to actually route the request
198
+ # to those targets, use RightScale::OperationResult.from_results to decode
199
+ #
200
+ # === Return
201
+ # true:: Always return true
202
+ #
203
+ # === Raise
204
+ # ArgumentError:: If target invalid
205
+ # SendFailure:: If sending of request failed unexpectedly
206
+ # TemporarilyOffline:: If cannot send request because RightNet client currently disconnected
207
+ # and offline queueing is disabled
208
+ def send_push(type, payload = nil, target = nil, token = nil, &callback)
209
+ build_and_send_packet(:send_push, type, payload, target, token, callback)
210
+ end
211
+
212
+ # Send a request to a single target with a response expected
213
+ # Automatically retry the request if a response is not received in a reasonable amount of time
214
+ # or if there is a non-delivery response indicating the target is not currently available
215
+ # Timeout the request if a response is not received in time, typically configured to 2 minutes
216
+ # Because of retries there is the possibility of duplicated requests, and these are detected and
217
+ # discarded automatically for non-idempotent actions
218
+ # Allow the request to expire per the agent's configured time-to-live, typically 1 minute
219
+ # Note that receiving a response does not guarantee that the request activity has actually
220
+ # completed since the request processing may involve other asynchronous requests
221
+ #
222
+ # === Parameters
223
+ # type(String):: Dispatch route for the request; typically identifies actor and action
224
+ # payload(Object):: Data to be sent with marshalling en route
225
+ # target(Hash|NilClass) Target for request, which may be a specific agent (using :agent_id),
226
+ # one chosen randomly from potentially multiple targets (using :tags, :scope), or nil to
227
+ # route solely using type:
228
+ # :agent_id(String):: serialized identity of specific target
229
+ # :tags(Array):: Tags that must all be associated with a target for it to be selected
230
+ # :scope(Hash):: Scoping to be used to restrict routing
231
+ # :account(Integer):: Restrict to agents with this account id
232
+ # :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
233
+ # ones with no shard id
234
+ # token(String|NilClass):: Token uniquely identifying request; defaults to random generated
235
+ #
236
+ # === Block
237
+ # Required block used to process response asynchronously with the following parameter:
238
+ # result(Result):: Response with an OperationResult of SUCCESS, RETRY, NON_DELIVERY, or ERROR,
239
+ # use RightScale::OperationResult.from_results to decode
240
+ #
241
+ # === Return
242
+ # true:: Always return true
243
+ #
244
+ # === Raise
245
+ # ArgumentError:: If target invalid or block missing
246
+ def send_request(type, payload = nil, target = nil, token = nil, &callback)
247
+ raise ArgumentError, "Missing block for response callback" unless callback
248
+ build_and_send_packet(:send_request, type, payload, target, token, callback)
249
+ end
250
+
251
+ # Build and send packet
252
+ #
253
+ # === Parameters
254
+ # kind(Symbol):: Kind of request: :send_push or :send_request
255
+ # type(String):: Dispatch route for the request; typically identifies actor and action
256
+ # payload(Object):: Data to be sent with marshalling en route
257
+ # target(Hash|NilClass):: Identity of specific target as string, or hash for selecting targets
258
+ # :agent_id(String):: Identity of specific target
259
+ # :tags(Array):: Tags that must all be associated with a target for it to be selected
260
+ # :scope(Hash):: Scoping to be used to restrict routing
261
+ # :account(Integer):: Restrict to agents with this account id
262
+ # :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
263
+ # ones with no shard id
264
+ # :selector(Symbol):: Which of the matched targets to be selected: :any or :all
265
+ # token(String|NilClass):: Token uniquely identifying request; defaults to random generated
266
+ # callback(Proc|nil):: Block used to process routing response
267
+ #
268
+ # === Return
269
+ # true:: Always return true
270
+ #
271
+ # === Raise
272
+ # ArgumentError:: If target invalid
273
+ def build_and_send_packet(kind, type, payload, target, token, callback)
274
+ if (packet = build_packet(kind, type, payload, target, token, callback))
275
+ action = type.split('/').last
276
+ received_at = @request_stats.update(action, packet.token)
277
+ @request_kind_stats.update((packet.selector == :all ? "fanout" : kind.to_s)[5..-1])
278
+ send("#{@mode}_send", kind, target, packet, received_at, callback)
279
+ end
280
+ true
281
+ end
282
+
283
+ # Build packet or queue it if offline
284
+ #
285
+ # === Parameters
286
+ # kind(Symbol):: Kind of request: :send_push or :send_request
287
+ # type(String):: Dispatch route for the request; typically identifies actor and action
288
+ # payload(Object):: Data to be sent with marshalling en route
289
+ # target(Hash|NilClass):: Identity of specific target as string, or hash for selecting targets
290
+ # :agent_id(String):: Identity of specific target
291
+ # :tags(Array):: Tags that must all be associated with a target for it to be selected
292
+ # :scope(Hash):: Scoping to be used to restrict routing
293
+ # :account(Integer):: Restrict to agents with this account id
294
+ # :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
295
+ # ones with no shard id
296
+ # :selector(Symbol):: Which of the matched targets to be selected: :any or :all
297
+ # token(String|NilClass):: Token uniquely identifying request; defaults to random generated
298
+ # callback(Boolean):: Whether this request has an associated response callback
299
+ #
300
+ # === Return
301
+ # (Push|Request|NilClass):: Packet created, or nil if queued instead
302
+ #
303
+ # === Raise
304
+ # ArgumentError:: If target is invalid
305
+ def build_packet(kind, type, payload, target, token, callback = false)
306
+ validate_target(target, kind == :send_push)
307
+ if queueing?
308
+ @offline_handler.queue_request(kind, type, payload, target, callback)
309
+ nil
310
+ else
311
+ if kind == :send_push
312
+ packet = Push.new(type, payload)
313
+ packet.selector = target[:selector] || :any if target.is_a?(Hash)
314
+ packet.persistent = true
315
+ packet.confirm = true if callback
316
+ else
317
+ packet = Request.new(type, payload)
318
+ ttl = @options[:time_to_live]
319
+ packet.expires_at = Time.now.to_i + ttl if ttl && ttl != 0
320
+ packet.selector = :any
321
+ end
322
+ packet.from = @identity
323
+ packet.token = token || RightSupport::Data::UUID.generate
324
+ if target.is_a?(Hash)
325
+ if (agent_id = target[:agent_id])
326
+ packet.target = agent_id
327
+ else
328
+ packet.tags = target[:tags] || []
329
+ packet.scope = target[:scope]
330
+ end
331
+ else
332
+ packet.target = target
333
+ end
334
+ packet
335
+ end
336
+ end
337
+
338
+ # Handle response to a request
339
+ #
340
+ # === Parameters
341
+ # response(Result):: Packet received as result of request
342
+ #
343
+ # === Return
344
+ # true:: Always return true
345
+ def handle_response(response)
346
+ if response.is_a?(Result)
347
+ token = response.token
348
+ if (result = OperationResult.from_results(response))
349
+ if result.non_delivery?
350
+ @non_delivery_stats.update(result.content.nil? ? "nil" : result.content.inspect)
351
+ elsif result.error?
352
+ @result_error_stats.update(result.content.nil? ? "nil" : result.content.inspect)
353
+ end
354
+ @result_stats.update(result.status)
355
+ else
356
+ @result_stats.update(response.results.nil? ? "nil" : response.results)
357
+ end
358
+
359
+ if (pending_request = @pending_requests[token])
360
+ if result && result.non_delivery? && pending_request.kind == :send_request
361
+ if result.content == OperationResult::TARGET_NOT_CONNECTED
362
+ # Log and temporarily ignore so that timeout retry mechanism continues, but save reason for use below if timeout
363
+ # Leave purging of associated request until final response, i.e., success response or retry timeout
364
+ if (parent_token = pending_request.retry_parent_token)
365
+ @pending_requests[parent_token].non_delivery = result.content
366
+ else
367
+ pending_request.non_delivery = result.content
368
+ end
369
+ Log.info("Non-delivery of <#{token}> because #{result.content}")
370
+ elsif result.content == OperationResult::RETRY_TIMEOUT && pending_request.non_delivery
371
+ # Request timed out but due to another non-delivery reason, so use that reason since more germane
372
+ response.results = OperationResult.non_delivery(pending_request.non_delivery)
373
+ deliver_response(response, pending_request)
374
+ else
375
+ deliver_response(response, pending_request)
376
+ end
377
+ else
378
+ deliver_response(response, pending_request)
379
+ end
380
+ elsif result && result.non_delivery?
381
+ Log.info("Non-delivery of <#{token}> because #{result.content}")
382
+ else
383
+ Log.debug("No pending request for response #{response.to_s([])}")
384
+ end
385
+ end
386
+ true
387
+ end
388
+
389
+ # Take any actions necessary to quiesce client interaction in preparation
390
+ # for agent termination but allow message receipt to continue
391
+ #
392
+ # === Return
393
+ # (Array):: Number of pending non-push requests and age of youngest request
394
+ def terminate
395
+ if @offline_handler
396
+ @offline_handler.terminate
397
+ @connectivity_checker.terminate if @connectivity_checker
398
+ pending = @pending_requests.kind(:send_request)
399
+ [pending.size, pending.youngest_age]
400
+ else
401
+ [0, nil]
402
+ end
403
+ end
404
+
405
+ # Create displayable dump of unfinished non-push request information
406
+ # Truncate list if there are more than 50 requests
407
+ #
408
+ # === Return
409
+ # info(Array(String)):: Receive time and token for each request in descending time order
410
+ def dump_requests
411
+ info = []
412
+ if @pending_requests
413
+ @pending_requests.kind(:send_request).each do |token, request|
414
+ info << "#{request.receive_time.localtime} <#{token}>"
415
+ end
416
+ info.sort!.reverse!
417
+ info = info[0..49] + ["..."] if info.size > 50
418
+ end
419
+ info
420
+ end
421
+
422
+ # Get sender statistics
423
+ #
424
+ # === Parameters
425
+ # reset(Boolean):: Whether to reset the statistics after getting the current ones
426
+ #
427
+ # === Return
428
+ # stats(Hash):: Current statistics:
429
+ # "exceptions"(Hash|nil):: Exceptions raised per category, or nil if none
430
+ # "total"(Integer):: Total exceptions for this category
431
+ # "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
432
+ # "non-deliveries"(Hash|nil):: Non-delivery activity stats with keys "total", "percent", "last",
433
+ # and 'rate' with percentage breakdown per reason, or nil if none
434
+ # "offlines"(Hash|nil):: Offline activity stats with keys "total", "last", and "duration",
435
+ # or nil if none
436
+ # "pings"(Hash|nil):: Request activity stats with keys "total", "percent", "last", and "rate"
437
+ # with percentage breakdown for "success" vs. "timeout", or nil if none
438
+ # "request kinds"(Hash|nil):: Request kind activity stats with keys "total", "percent", and "last"
439
+ # with percentage breakdown per kind, or nil if none
440
+ # "requests"(Hash|nil):: Request activity stats with keys "total", "percent", "last", and "rate"
441
+ # with percentage breakdown per request type, or nil if none
442
+ # "requests pending"(Hash|nil):: Number of requests waiting for response and age of oldest,
443
+ # or nil if none
444
+ # "response time"(Float):: Average number of seconds to respond to a request recently
445
+ # "result errors"(Hash|nil):: Error result activity stats with keys "total", "percent", "last",
446
+ # and 'rate' with percentage breakdown per error, or nil if none
447
+ # "results"(Hash|nil):: Results activity stats with keys "total", "percent", "last", and "rate"
448
+ # with percentage breakdown per operation result type, or nil if none
449
+ # "retries"(Hash|nil):: Retry activity stats with keys "total", "percent", "last", and "rate"
450
+ # with percentage breakdown per request type, or nil if none
451
+ # "send failure"(Hash|nil):: Send failure activity stats with keys "total", "percent", "last", and "rate"
452
+ # with percentage breakdown per failure type, or nil if none
453
+ def stats(reset = false)
454
+ stats = {}
455
+ if @agent
456
+ offlines = @offline_stats.all
457
+ offlines.merge!("duration" => @offline_stats.avg_duration) if offlines
458
+ if @pending_requests.size > 0
459
+ pending = {}
460
+ pending["pushes"] = @pending_requests.kind(:send_push).size
461
+ requests = @pending_requests.kind(:send_request)
462
+ if (pending["requests"] = requests.size) > 0
463
+ pending["oldest age"] = requests.oldest_age
464
+ end
465
+ end
466
+ stats = {
467
+ "exceptions" => @exception_stats.stats,
468
+ "non-deliveries" => @non_delivery_stats.all,
469
+ "offlines" => offlines,
470
+ "pings" => @ping_stats.all,
471
+ "request kinds" => @request_kind_stats.all,
472
+ "requests" => @request_stats.all,
473
+ "requests pending" => pending,
474
+ "response time" => @request_stats.avg_duration,
475
+ "result errors" => @result_error_stats.all,
476
+ "results" => @result_stats.all,
477
+ "retries" => @retry_stats.all,
478
+ "send failures" => @send_failure_stats.all
479
+ }
480
+ reset_stats if reset
481
+ end
482
+ stats
483
+ end
484
+
485
+ protected
486
+
487
+ # Reset dispatch statistics
488
+ #
489
+ # === Return
490
+ # true:: Always return true
491
+ def reset_stats
492
+ @ping_stats = RightSupport::Stats::Activity.new
493
+ @retry_stats = RightSupport::Stats::Activity.new
494
+ @request_stats = RightSupport::Stats::Activity.new
495
+ @result_stats = RightSupport::Stats::Activity.new
496
+ @result_error_stats = RightSupport::Stats::Activity.new
497
+ @non_delivery_stats = RightSupport::Stats::Activity.new
498
+ @offline_stats = RightSupport::Stats::Activity.new(measure_rate = false)
499
+ @request_kind_stats = RightSupport::Stats::Activity.new(measure_rate = false)
500
+ @send_failure_stats = RightSupport::Stats::Activity.new
501
+ @exception_stats = RightSupport::Stats::Exceptions.new(@agent, @options[:exception_callback])
502
+ true
503
+ end
504
+
505
+ # Validate target argument of send per the semantics of each kind of send:
506
+ # - The target is either a specific target name, a non-empty hash, or nil
507
+ # - A specific target name must be a string (or use :agent_id key)
508
+ # - A non-empty hash target
509
+ # - may have keys in symbol or string format
510
+ # - may contain an :agent_id to select a specific target, but then cannot
511
+ # have any other keys
512
+ # - may be allowed to contain a :selector key with value :any or :all,
513
+ # depending on the kind of send
514
+ # - may contain a :scope key with a hash value with keys :account and/or :shard
515
+ # - may contain a :tags key with an array value
516
+ #
517
+ # === Parameters
518
+ # target(String|Hash):: Identity of specific target, or hash for selecting targets;
519
+ # returned with all hash keys converted to symbols
520
+ # allow_selector(Boolean):: Whether to allow :selector
521
+ #
522
+ # === Return
523
+ # true:: Always return true
524
+ #
525
+ # === Raise
526
+ # ArgumentError:: If target is invalid
527
+ def validate_target(target, allow_selector)
528
+ choices = ":agent_id OR " + (allow_selector ? ":selector, " : "") + ":tags and/or :scope"
529
+ if target.is_a?(Hash)
530
+ t = SerializationHelper.symbolize_keys(target)
531
+
532
+ if (agent_id = t[:agent_id])
533
+ raise ArgumentError, "Invalid target: #{target.inspect}" if t.size > 1
534
+ end
535
+
536
+ if (selector = t[:selector])
537
+ if allow_selector
538
+ selector = selector.to_sym
539
+ unless [:any, :all].include?(selector)
540
+ raise ArgumentError, "Invalid target selector (#{t[:selector].inspect}), choices are :any and :all"
541
+ end
542
+ t[:selector] = selector
543
+ else
544
+ raise ArgumentError, "Invalid target hash (#{target.inspect}), choices are #{choices}"
545
+ end
546
+ end
547
+
548
+ if (scope = t[:scope])
549
+ if scope.is_a?(Hash)
550
+ scope = SerializationHelper.symbolize_keys(scope)
551
+ unless (scope[:account] || scope[:shard]) && (scope.keys - [:account, :shard]).empty?
552
+ raise ArgumentError, "Invalid target scope (#{t[:scope].inspect}), choices are :account and :shard"
553
+ end
554
+ t[:scope] = scope
555
+ else
556
+ raise ArgumentError, "Invalid target scope (#{t[:scope].inspect}), must be a hash of :account and/or :shard"
557
+ end
558
+ end
559
+
560
+ if (tags = t[:tags]) && !tags.is_a?(Array)
561
+ raise ArgumentError, "Invalid target tags (#{t[:tags].inspect}), must be an array"
562
+ end
563
+
564
+ unless (agent_id || selector || scope || tags) && (t.keys - [:agent_id, :selector, :scope, :tags]).empty?
565
+ raise ArgumentError, "Invalid target hash (#{target.inspect}), choices are #{choices}"
566
+ end
567
+ target = t
568
+ elsif !target.nil? && !target.is_a?(String)
569
+ raise ArgumentError, "Invalid target (#{target.inspect}), choices are specific target name or a hash of #{choices}"
570
+ end
571
+ true
572
+ end
573
+
574
+ # Send request via HTTP and then immediately handle response
575
+ #
576
+ # === Parameters
577
+ # kind(Symbol):: Kind of request: :send_push or :send_request
578
+ # target(Hash|String|nil):: Target for request
579
+ # packet(Push|Request):: Request packet to send
580
+ # received_at(Time):: Time when request received
581
+ # callback(Proc|nil):: Block used to process response
582
+ #
583
+ # === Return
584
+ # true:: Always return true
585
+ def http_send(kind, target, packet, received_at, callback)
586
+ begin
587
+ method = packet.class.name.split("::").last.downcase
588
+ result = success_result(@agent.client.send(method, packet.type, packet.payload, target, packet.token))
589
+ rescue Exceptions::Unauthorized => e
590
+ result = error_result(e.message)
591
+ rescue Exceptions::ConnectivityFailure => e
592
+ if queueing?
593
+ @offline_handler.queue_request(kind, packet.type, packet.payload, target, callback)
594
+ result = nil
595
+ else
596
+ result = retry_result(e.message)
597
+ end
598
+ rescue Exceptions::RetryableError => e
599
+ result = retry_result(e.message)
600
+ rescue Exceptions::InternalServerError => e
601
+ result = error_result("#{e.server} internal error")
602
+ rescue Exceptions::Terminating => e
603
+ result = nil
604
+ rescue StandardError => e
605
+ # These errors are either unexpected errors or RestClient errors with an http_body
606
+ # giving details about the error that are conveyed in the error_result
607
+ if e.respond_to?(:http_body)
608
+ # No need to log here since any HTTP request errors have already been logged
609
+ result = error_result(e.inspect)
610
+ else
611
+ agent_type = AgentIdentity.parse(@identity).agent_type
612
+ Log.error("Failed to send #{packet.trace} #{packet.type}", e, :trace)
613
+ @exception_stats.track("request", e)
614
+ result = error_result("#{agent_type.capitalize} agent internal error")
615
+ end
616
+ end
617
+
618
+ if result && packet.is_a?(Request)
619
+ result = Result.new(packet.token, @identity, result, from = packet.target)
620
+ result.received_at = received_at.to_f
621
+ @pending_requests[packet.token] = PendingRequest.new(kind, received_at, callback) if callback
622
+ if @options[:async_response]
623
+ EM.next_tick { handle_response(result) }
624
+ else
625
+ handle_response(result)
626
+ end
627
+ end
628
+ true
629
+ end
630
+
631
+ # Send request via AMQP
632
+ # If lack connectivity and queueing enabled, queue request
633
+ #
634
+ # === Parameters
635
+ # kind(Symbol):: Kind of request: :send_push or :send_request
636
+ # target(Hash|String|nil):: Target for request
637
+ # received_at(Time):: Time when request received
638
+ # packet(Push|Request):: Request packet to send
639
+ # callback(Proc|nil):: Block used to process response
640
+ #
641
+ # === Return
642
+ # true:: Always return true
643
+ def amqp_send(kind, target, packet, received_at, callback)
644
+ begin
645
+ @pending_requests[packet.token] = PendingRequest.new(kind, received_at, callback) if callback
646
+ if packet.class == Request
647
+ amqp_send_retry(packet, packet.token)
648
+ else
649
+ amqp_send_once(packet)
650
+ end
651
+ rescue TemporarilyOffline => e
652
+ if queueing?
653
+ # Queue request until come back online
654
+ @offline_handler.queue_request(kind, packet.type, packet.payload, target, callback)
655
+ @pending_requests.delete(packet.token) if callback
656
+ else
657
+ # Send retry response so that requester, e.g., RetryableRequest, can retry
658
+ result = OperationResult.retry("lost RightNet connectivity")
659
+ handle_response(Result.new(packet.token, @identity, result, @identity))
660
+ end
661
+ rescue SendFailure => e
662
+ # Send non-delivery response so that requester, e.g., RetryableRequest, can retry
663
+ result = OperationResult.non_delivery("send failed unexpectedly")
664
+ handle_response(Result.new(packet.token, @identity, result, @identity))
665
+ end
666
+ true
667
+ end
668
+
669
+ # Send request via AMQP without retrying
670
+ # Use mandatory flag to request return of message if it cannot be delivered
671
+ #
672
+ # === Parameters
673
+ # packet(Push|Request):: Request packet to send
674
+ # ids(Array|nil):: Identity of specific brokers to choose from, or nil if any okay
675
+ #
676
+ # === Return
677
+ # (Array):: Identity of brokers to which request was published
678
+ #
679
+ # === Raise
680
+ # SendFailure:: If sending of request failed unexpectedly
681
+ # TemporarilyOffline:: If cannot send request because RightNet client currently disconnected
682
+ # and offline queueing is disabled
683
+ def amqp_send_once(packet, ids = nil)
684
+ name =
685
+ exchange = {:type => :fanout, :name => @request_queue, :options => {:durable => true, :no_declare => @secure}}
686
+ @agent.client.publish(exchange, packet, :persistent => packet.persistent, :mandatory => true,
687
+ :log_filter => [:tags, :target, :tries, :persistent], :brokers => ids)
688
+ rescue RightAMQP::HABrokerClient::NoConnectedBrokers => e
689
+ msg = "Failed to publish request #{packet.trace} #{packet.type}"
690
+ Log.error(msg, e)
691
+ @send_failure_stats.update("NoConnectedBrokers")
692
+ raise TemporarilyOffline.new(msg + " (#{e.class}: #{e.message})")
693
+ rescue Exception => e
694
+ msg = "Failed to publish request #{packet.trace} #{packet.type}"
695
+ Log.error(msg, e, :trace)
696
+ @send_failure_stats.update(e.class.name)
697
+ @exception_stats.track("publish", e, packet)
698
+ raise SendFailure.new(msg + " (#{e.class}: #{e.message})")
699
+ end
700
+
701
+ # Send request via AMQP with one or more retries if do not receive a response in time
702
+ # Send timeout result if reach configured retry timeout limit
703
+ # Use exponential backoff with RETRY_BACKOFF_FACTOR for retry spacing
704
+ # Adjust retry interval by average response time to avoid adding to system load
705
+ # when system gets slow
706
+ # Rotate through brokers on retries
707
+ # Check connectivity after first retry timeout
708
+ #
709
+ # === Parameters
710
+ # packet(Request):: Request packet to send
711
+ # parent_token(String):: Token for original request
712
+ # count(Integer):: Number of retries so far
713
+ # multiplier(Integer):: Multiplier for retry interval for exponential backoff
714
+ # elapsed(Integer):: Elapsed time in seconds since this request was first attempted
715
+ # broker_ids(Array):: Identity of brokers to be used in priority order
716
+ #
717
+ # === Return
718
+ # true:: Always return true
719
+ def amqp_send_retry(packet, parent_token, count = 0, multiplier = 1, elapsed = 0, broker_ids = nil)
720
+ check_broker_ids = amqp_send_once(packet, broker_ids)
721
+
722
+ if @retry_interval && @retry_timeout && parent_token
723
+ interval = [(@retry_interval * multiplier) + (@request_stats.avg_duration || 0), @retry_timeout - elapsed].min
724
+ EM.add_timer(interval) do
725
+ begin
726
+ if @pending_requests[parent_token]
727
+ count += 1
728
+ elapsed += interval
729
+ if elapsed < @retry_timeout
730
+ packet.tries << packet.token
731
+ packet.token = RightSupport::Data::UUID.generate
732
+ @pending_requests[parent_token].retry_parent_token = parent_token if count == 1
733
+ @pending_requests[packet.token] = @pending_requests[parent_token]
734
+ broker_ids ||= @agent.client.all
735
+ amqp_send_retry(packet, parent_token, count, multiplier * RETRY_BACKOFF_FACTOR, elapsed,
736
+ broker_ids.push(broker_ids.shift))
737
+ @retry_stats.update(packet.type.split('/').last)
738
+ else
739
+ Log.warning("RE-SEND TIMEOUT after #{elapsed.to_i} seconds for #{packet.trace} #{packet.type}")
740
+ result = OperationResult.non_delivery(OperationResult::RETRY_TIMEOUT)
741
+ @non_delivery_stats.update(result.content)
742
+ handle_response(Result.new(packet.token, @identity, result, @identity))
743
+ end
744
+ @connectivity_checker.check(check_broker_ids.first) if check_broker_ids.any? && count == 1
745
+ end
746
+ rescue TemporarilyOffline => e
747
+ Log.error("Failed retry for #{packet.trace} #{packet.type} because temporarily offline")
748
+ rescue SendFailure => e
749
+ Log.error("Failed retry for #{packet.trace} #{packet.type} because of send failure")
750
+ rescue Exception => e
751
+ # Not sending a response here because something more basic is broken in the retry
752
+ # mechanism and don't want an error response to preempt a delayed actual response
753
+ Log.error("Failed retry for #{packet.trace} #{packet.type} without responding", e, :trace)
754
+ @exception_stats.track("retry", e, packet)
755
+ end
756
+ end
757
+ end
758
+ true
759
+ end
760
+
761
+ # Deliver the response and remove associated non-push requests from pending
762
+ # including all associated retry requests
763
+ #
764
+ # === Parameters
765
+ # response(Result):: Packet received as result of request
766
+ # pending_request(Hash):: Associated pending request
767
+ #
768
+ # === Return
769
+ # true:: Always return true
770
+ def deliver_response(response, pending_request)
771
+ @request_stats.finish(pending_request.receive_time, response.token)
772
+
773
+ @pending_requests.delete(response.token) if pending_request.kind == :send_request
774
+ if (parent_token = pending_request.retry_parent_token)
775
+ @pending_requests.reject! { |k, v| k == parent_token || v.retry_parent_token == parent_token }
776
+ end
777
+
778
+ pending_request.response_handler.call(response) if pending_request.response_handler
779
+ true
780
+ end
781
+
782
+ # Determine whether currently queueing requests because offline
783
+ #
784
+ # === Return
785
+ # (Boolean):: true if queueing, otherwise false
786
+ def queueing?
787
+ @options[:offline_queueing] && @offline_handler.queueing?
788
+ end
789
+
790
+ end # Sender
791
+
792
+ end # RightScale