right_agent 2.0.7-x86-mingw32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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