right_agent 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +78 -0
  3. data/Rakefile +86 -0
  4. data/lib/right_agent.rb +66 -0
  5. data/lib/right_agent/actor.rb +163 -0
  6. data/lib/right_agent/actor_registry.rb +76 -0
  7. data/lib/right_agent/actors/agent_manager.rb +189 -0
  8. data/lib/right_agent/agent.rb +735 -0
  9. data/lib/right_agent/agent_config.rb +403 -0
  10. data/lib/right_agent/agent_identity.rb +209 -0
  11. data/lib/right_agent/agent_tags_manager.rb +213 -0
  12. data/lib/right_agent/audit_formatter.rb +107 -0
  13. data/lib/right_agent/broker_client.rb +683 -0
  14. data/lib/right_agent/command.rb +30 -0
  15. data/lib/right_agent/command/agent_manager_commands.rb +134 -0
  16. data/lib/right_agent/command/command_client.rb +136 -0
  17. data/lib/right_agent/command/command_constants.rb +42 -0
  18. data/lib/right_agent/command/command_io.rb +128 -0
  19. data/lib/right_agent/command/command_parser.rb +87 -0
  20. data/lib/right_agent/command/command_runner.rb +105 -0
  21. data/lib/right_agent/command/command_serializer.rb +63 -0
  22. data/lib/right_agent/console.rb +65 -0
  23. data/lib/right_agent/core_payload_types.rb +42 -0
  24. data/lib/right_agent/core_payload_types/cookbook.rb +61 -0
  25. data/lib/right_agent/core_payload_types/cookbook_position.rb +46 -0
  26. data/lib/right_agent/core_payload_types/cookbook_repository.rb +116 -0
  27. data/lib/right_agent/core_payload_types/cookbook_sequence.rb +70 -0
  28. data/lib/right_agent/core_payload_types/dev_repositories.rb +90 -0
  29. data/lib/right_agent/core_payload_types/event_categories.rb +38 -0
  30. data/lib/right_agent/core_payload_types/executable_bundle.rb +138 -0
  31. data/lib/right_agent/core_payload_types/login_policy.rb +72 -0
  32. data/lib/right_agent/core_payload_types/login_user.rb +62 -0
  33. data/lib/right_agent/core_payload_types/planned_volume.rb +94 -0
  34. data/lib/right_agent/core_payload_types/recipe_instantiation.rb +60 -0
  35. data/lib/right_agent/core_payload_types/repositories_bundle.rb +50 -0
  36. data/lib/right_agent/core_payload_types/right_script_attachment.rb +95 -0
  37. data/lib/right_agent/core_payload_types/right_script_instantiation.rb +73 -0
  38. data/lib/right_agent/core_payload_types/secure_document.rb +66 -0
  39. data/lib/right_agent/core_payload_types/secure_document_location.rb +63 -0
  40. data/lib/right_agent/core_payload_types/software_repository_instantiation.rb +61 -0
  41. data/lib/right_agent/daemonize.rb +35 -0
  42. data/lib/right_agent/dispatcher.rb +348 -0
  43. data/lib/right_agent/enrollment_result.rb +217 -0
  44. data/lib/right_agent/exceptions.rb +30 -0
  45. data/lib/right_agent/ha_broker_client.rb +1278 -0
  46. data/lib/right_agent/idempotent_request.rb +140 -0
  47. data/lib/right_agent/log.rb +418 -0
  48. data/lib/right_agent/monkey_patches.rb +29 -0
  49. data/lib/right_agent/monkey_patches/amqp_patch.rb +274 -0
  50. data/lib/right_agent/monkey_patches/ruby_patch.rb +49 -0
  51. data/lib/right_agent/monkey_patches/ruby_patch/array_patch.rb +29 -0
  52. data/lib/right_agent/monkey_patches/ruby_patch/darwin_patch.rb +24 -0
  53. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch.rb +24 -0
  54. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch/file_patch.rb +30 -0
  55. data/lib/right_agent/monkey_patches/ruby_patch/object_patch.rb +49 -0
  56. data/lib/right_agent/monkey_patches/ruby_patch/singleton_patch.rb +46 -0
  57. data/lib/right_agent/monkey_patches/ruby_patch/string_patch.rb +107 -0
  58. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch.rb +32 -0
  59. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/file_patch.rb +90 -0
  60. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/process_patch.rb +63 -0
  61. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/stdio_patch.rb +27 -0
  62. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/time_patch.rb +55 -0
  63. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/win32ole_patch.rb +34 -0
  64. data/lib/right_agent/multiplexer.rb +91 -0
  65. data/lib/right_agent/operation_result.rb +270 -0
  66. data/lib/right_agent/packets.rb +637 -0
  67. data/lib/right_agent/payload_formatter.rb +104 -0
  68. data/lib/right_agent/pid_file.rb +159 -0
  69. data/lib/right_agent/platform.rb +319 -0
  70. data/lib/right_agent/platform/darwin.rb +227 -0
  71. data/lib/right_agent/platform/linux.rb +268 -0
  72. data/lib/right_agent/platform/windows.rb +1204 -0
  73. data/lib/right_agent/scripts/agent_controller.rb +522 -0
  74. data/lib/right_agent/scripts/agent_deployer.rb +379 -0
  75. data/lib/right_agent/scripts/common_parser.rb +153 -0
  76. data/lib/right_agent/scripts/log_level_manager.rb +193 -0
  77. data/lib/right_agent/scripts/stats_manager.rb +256 -0
  78. data/lib/right_agent/scripts/usage.rb +58 -0
  79. data/lib/right_agent/secure_identity.rb +92 -0
  80. data/lib/right_agent/security.rb +32 -0
  81. data/lib/right_agent/security/cached_certificate_store_proxy.rb +63 -0
  82. data/lib/right_agent/security/certificate.rb +102 -0
  83. data/lib/right_agent/security/certificate_cache.rb +89 -0
  84. data/lib/right_agent/security/distinguished_name.rb +56 -0
  85. data/lib/right_agent/security/encrypted_document.rb +84 -0
  86. data/lib/right_agent/security/rsa_key_pair.rb +76 -0
  87. data/lib/right_agent/security/signature.rb +86 -0
  88. data/lib/right_agent/security/static_certificate_store.rb +69 -0
  89. data/lib/right_agent/sender.rb +937 -0
  90. data/lib/right_agent/serialize.rb +29 -0
  91. data/lib/right_agent/serialize/message_pack.rb +102 -0
  92. data/lib/right_agent/serialize/secure_serializer.rb +131 -0
  93. data/lib/right_agent/serialize/secure_serializer_initializer.rb +47 -0
  94. data/lib/right_agent/serialize/serializable.rb +135 -0
  95. data/lib/right_agent/serialize/serializer.rb +149 -0
  96. data/lib/right_agent/stats_helper.rb +731 -0
  97. data/lib/right_agent/subprocess.rb +38 -0
  98. data/lib/right_agent/tracer.rb +124 -0
  99. data/right_agent.gemspec +60 -0
  100. data/spec/actor_registry_spec.rb +81 -0
  101. data/spec/actor_spec.rb +99 -0
  102. data/spec/agent_config_spec.rb +226 -0
  103. data/spec/agent_identity_spec.rb +75 -0
  104. data/spec/agent_spec.rb +571 -0
  105. data/spec/broker_client_spec.rb +961 -0
  106. data/spec/command/agent_manager_commands_spec.rb +51 -0
  107. data/spec/command/command_io_spec.rb +93 -0
  108. data/spec/command/command_parser_spec.rb +79 -0
  109. data/spec/command/command_runner_spec.rb +72 -0
  110. data/spec/command/command_serializer_spec.rb +51 -0
  111. data/spec/core_payload_types/dev_repositories_spec.rb +64 -0
  112. data/spec/core_payload_types/executable_bundle_spec.rb +59 -0
  113. data/spec/core_payload_types/login_user_spec.rb +98 -0
  114. data/spec/core_payload_types/right_script_attachment_spec.rb +65 -0
  115. data/spec/core_payload_types/spec_helper.rb +23 -0
  116. data/spec/dispatcher_spec.rb +372 -0
  117. data/spec/enrollment_result_spec.rb +53 -0
  118. data/spec/ha_broker_client_spec.rb +1673 -0
  119. data/spec/idempotent_request_spec.rb +136 -0
  120. data/spec/log_spec.rb +177 -0
  121. data/spec/monkey_patches/amqp_patch_spec.rb +100 -0
  122. data/spec/monkey_patches/eventmachine_spec.rb +62 -0
  123. data/spec/monkey_patches/string_patch_spec.rb +99 -0
  124. data/spec/multiplexer_spec.rb +48 -0
  125. data/spec/operation_result_spec.rb +171 -0
  126. data/spec/packets_spec.rb +418 -0
  127. data/spec/platform/platform_spec.rb +60 -0
  128. data/spec/results_mock.rb +45 -0
  129. data/spec/secure_identity_spec.rb +50 -0
  130. data/spec/security/cached_certificate_store_proxy_spec.rb +56 -0
  131. data/spec/security/certificate_cache_spec.rb +71 -0
  132. data/spec/security/certificate_spec.rb +49 -0
  133. data/spec/security/distinguished_name_spec.rb +46 -0
  134. data/spec/security/encrypted_document_spec.rb +55 -0
  135. data/spec/security/rsa_key_pair_spec.rb +55 -0
  136. data/spec/security/signature_spec.rb +66 -0
  137. data/spec/security/static_certificate_store_spec.rb +52 -0
  138. data/spec/sender_spec.rb +887 -0
  139. data/spec/serialize/message_pack_spec.rb +131 -0
  140. data/spec/serialize/secure_serializer_spec.rb +102 -0
  141. data/spec/serialize/serializable_spec.rb +90 -0
  142. data/spec/serialize/serializer_spec.rb +174 -0
  143. data/spec/spec.opts +2 -0
  144. data/spec/spec_helper.rb +77 -0
  145. data/spec/stats_helper_spec.rb +681 -0
  146. data/spec/tracer_spec.rb +114 -0
  147. metadata +320 -0
@@ -0,0 +1,189 @@
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
+ require 'socket'
24
+
25
+ # Generic actor for all agents to provide basic agent management services
26
+ class AgentManager
27
+
28
+ include RightScale::Actor
29
+ include RightScale::OperationResultHelper
30
+
31
+ on_exception { |meth, deliverable, e| RightScale::ExceptionMailer.deliver_notification(meth, deliverable, e) }
32
+
33
+ expose :ping, :stats, :set_log_level, :execute, :connect, :disconnect, :terminate
34
+
35
+ # Valid log levels
36
+ LEVELS = [:debug, :info, :warn, :error, :fatal]
37
+
38
+ # Initialize broker
39
+ #
40
+ # === Parameters
41
+ # agent(RightScale::Agent):: This agent
42
+ def initialize(agent)
43
+ @agent = agent
44
+ end
45
+
46
+ # Always return success along with identity, protocol version, and broker information
47
+ # Used for troubleshooting
48
+ #
49
+ # === Return
50
+ # (RightScale::OperationResult):: Always returns success
51
+ def ping(_)
52
+ success_result(:identity => @agent.options[:identity],
53
+ :hostname => Socket.gethostname,
54
+ :version => RightScale::AgentConfig.protocol_version,
55
+ :brokers => @agent.broker.status,
56
+ :time => Time.now.to_i)
57
+ end
58
+
59
+ # Retrieve statistics about agent operation
60
+ #
61
+ # === Parameters:
62
+ # options(Hash):: Request options:
63
+ # :reset(Boolean):: Whether to reset the statistics after getting the current ones
64
+ #
65
+ # === Return
66
+ # (RightScale::OperationResult):: Always returns success
67
+ def stats(options)
68
+ @agent.stats(RightScale::SerializationHelper.symbolize_keys(options))
69
+ end
70
+
71
+ # Change log level of agent
72
+ #
73
+ # === Parameter
74
+ # level(Symbol|String):: One of :debug, :info, :warn, :error, :fatal
75
+ #
76
+ # === Return
77
+ # (RightScale::OperationResult):: Success if level was changed, error otherwise
78
+ def set_log_level(level)
79
+ level = level.to_sym if level.is_a?(String)
80
+ if LEVELS.include?(level)
81
+ RightScale::Log.level = level
82
+ success_result
83
+ else
84
+ error_result("Invalid log level '#{level.to_s}'")
85
+ end
86
+ end
87
+
88
+ # Eval given code in context of agent
89
+ #
90
+ # === Parameter
91
+ # code(String):: Code to be eval'd
92
+ #
93
+ # === Return
94
+ # (RightScale::OperationResult):: Success with result if code didn't raise an exception,
95
+ # otherwise failure with exception message
96
+ def execute(code)
97
+ begin
98
+ success_result(self.instance_eval(code))
99
+ rescue Exception => e
100
+ error_result(e.message + " at\n " + e.backtrace.join("\n "))
101
+ end
102
+ end
103
+
104
+ # Connect agent to an additional broker or reconnect it if connection has failed
105
+ # Assumes agent already has credentials on this broker and identity queue exists
106
+ #
107
+ # === Parameters
108
+ # options(Hash):: Connect options:
109
+ # :host(String):: Host name of broker
110
+ # :port(Integer):: Port number of broker
111
+ # :id(Integer):: Small unique id associated with this broker for use in forming alias
112
+ # :priority(Integer|nil):: Priority position of this broker in list for use
113
+ # by this agent with nil meaning add to end of list
114
+ # :force(Boolean):: Reconnect even if already connected
115
+ #
116
+ # === Return
117
+ # res(RightScale::OperationResult):: Success unless exception is raised
118
+ def connect(options)
119
+ options = RightScale::SerializationHelper.symbolize_keys(options)
120
+ res = success_result
121
+ begin
122
+ if error = @agent.connect(options[:host], options[:port], options[:id], options[:priority], options[:force])
123
+ res = error_result(error)
124
+ end
125
+ rescue Exception => e
126
+ res = error_result("Failed to connect to broker: #{e.message}")
127
+ end
128
+ res
129
+ end
130
+
131
+ # Disconnect agent from a broker
132
+ #
133
+ # === Parameters
134
+ # options(Hash):: Connect options:
135
+ # :host(String):: Host name of broker
136
+ # :port(Integer):: Port number of broker
137
+ # :remove(Boolean):: Remove broker from configuration in addition to disconnecting it
138
+ #
139
+ # === Return
140
+ # res(RightScale::OperationResult):: Success unless exception is raised
141
+ def disconnect(options)
142
+ options = RightScale::SerializationHelper.symbolize_keys(options)
143
+ res = success_result
144
+ begin
145
+ if error = @agent.disconnect(options[:host], options[:port], options[:remove])
146
+ res = error_result(error)
147
+ end
148
+ rescue Exception => e
149
+ res = error_result("Failed to disconnect from broker: #{e.message}")
150
+ end
151
+ res
152
+ end
153
+
154
+ # Declare one or more broker connections unusable because connection setup has failed
155
+ #
156
+ # === Parameters
157
+ # options(Hash):: Failure options:
158
+ # :brokers(Array):: Identity of brokers
159
+ #
160
+ # === Return
161
+ # res(RightScale::OperationResult):: Success unless exception is raised
162
+ def connect_failed(options)
163
+ options = RightScale::SerializationHelper.symbolize_keys(options)
164
+ res = success_result
165
+ begin
166
+ if error = @agent.connect_failed(options[:brokers])
167
+ res = error_result(error)
168
+ end
169
+ rescue Exception => e
170
+ res = error_result("Failed to notify agent that brokers #{options[:brokers]} are unusable: #{e.message}")
171
+ end
172
+ res
173
+ end
174
+
175
+ # Terminate self
176
+ #
177
+ # === Parameters
178
+ # options(Hash):: Terminate options
179
+ #
180
+ # === Return
181
+ # true
182
+ def terminate(options = nil)
183
+ RightScale::CommandRunner.stop
184
+ # Delay terminate a bit to give reply a chance to be sent
185
+ EM.next_tick { @agent.terminate }
186
+ true
187
+ end
188
+
189
+ end
@@ -0,0 +1,735 @@
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
+ require 'socket'
24
+
25
+ module RightScale
26
+
27
+ # Agent for receiving messages from the mapper and acting upon them
28
+ # by dispatching to a registered actor to perform
29
+ # See load_actors for details on how the agent specific environment is loaded
30
+ class Agent
31
+
32
+ include ConsoleHelper
33
+ include DaemonizeHelper
34
+ include StatsHelper
35
+
36
+ # (String) Identity of this agent
37
+ attr_reader :identity
38
+
39
+ # (Hash) Configuration options applied to the agent
40
+ attr_reader :options
41
+
42
+ # (Dispatcher) Dispatcher for messages received
43
+ attr_reader :dispatcher
44
+
45
+ # (ActorRegistry) Registry for this agents actors
46
+ attr_reader :registry
47
+
48
+ # (HABrokerClient) High availability AMQP broker client
49
+ attr_reader :broker
50
+
51
+ # (Array) Tag strings published by agent
52
+ attr_accessor :tags
53
+
54
+ # (Proc) Callback procedure for exceptions
55
+ attr_reader :exception_callback
56
+
57
+ # Default option settings for the agent
58
+ DEFAULT_OPTIONS = {
59
+ :user => 'agent',
60
+ :pass => 'testing',
61
+ :vhost => '/right_net',
62
+ :secure => true,
63
+ :log_level => :info,
64
+ :daemonize => false,
65
+ :console => false,
66
+ :root_dir => Dir.pwd,
67
+ :time_to_live => 0,
68
+ :retry_interval => nil,
69
+ :retry_timeout => nil,
70
+ :connect_timeout => 60,
71
+ :reconnect_interval => 60,
72
+ :ping_interval => 0,
73
+ :check_interval => 5 * 60,
74
+ :grace_timeout => 30,
75
+ :prefetch => 1,
76
+ }
77
+
78
+ # Initializes a new agent and establishes an AMQP connection.
79
+ # This must be used inside EM.run block or if EventMachine reactor
80
+ # is already started, for instance, by a Thin server that your Merb/Rails
81
+ # application runs on.
82
+ #
83
+ # === Parameters
84
+ # opts(Hash):: Configuration options:
85
+ # :identity(String):: Identity of this agent, no default
86
+ # :agent_name(String):: Local name for this agent
87
+ # :root_dir(String):: Application root for this agent containing subdirectories actors, certs, and init,
88
+ # defaults to current working directory
89
+ # :pid_dir(String):: Path to the directory where the agent stores its process id file (only if daemonized),
90
+ # defaults to the current working directory
91
+ # :log_dir(String):: Log directory path, defaults to the platform specific log directory
92
+ # :log_level(Symbol):: The verbosity of logging -- :debug, :info, :warn, :error or :fatal
93
+ # :actors(Array):: List of actors to load
94
+ # :console(Boolean):: true indicates to start interactive console
95
+ # :daemonize(Boolean):: true indicates to daemonize
96
+ # :retry_interval(Numeric):: Number of seconds between request retries
97
+ # :retry_timeout(Numeric):: Maximum number of seconds to retry request before give up
98
+ # :time_to_live(Integer):: Number of seconds before a request expires and is to be ignored
99
+ # by the receiver, 0 means never expire, defaults to 0
100
+ # :connect_timeout:: Number of seconds to wait for a broker connection to be established
101
+ # :reconnect_interval(Integer):: Number of seconds between broker reconnect attempts
102
+ # :ping_interval(Integer):: Minimum number of seconds since last message receipt to ping the mapper
103
+ # to check connectivity, defaults to 0 meaning do not ping
104
+ # :check_interval:: Number of seconds between publishing stats and checking for broker connections
105
+ # that failed during agent launch and then attempting to reconnect via the mapper
106
+ # :grace_timeout(Integer):: Maximum number of seconds to wait after last request received before
107
+ # terminating regardless of whether there are still unfinished requests
108
+ # :dup_check(Boolean):: Whether to check for and reject duplicate requests, e.g., due to retries
109
+ # :prefetch(Integer):: Maximum number of messages the AMQP broker is to prefetch for this agent
110
+ # before it receives an ack. Value 1 ensures that only last unacknowledged gets redelivered
111
+ # if the agent crashes. Value 0 means unlimited prefetch.
112
+ # :exception_callback(Proc):: Callback with following parameters that is activated on exception events:
113
+ # exception(Exception):: Exception
114
+ # message(Packet):: Message being processed
115
+ # agent(Agent):: Reference to agent
116
+ # :ready_callback(Proc):: Called once agent is connected ready to service (no argument)
117
+ # :restart_callback(Proc):: Callback that is activated on each restart vote with votes being initiated
118
+ # by offline queue exceeding MAX_QUEUED_REQUESTS or by repeated failures to access mapper when online
119
+ # :services(Symbol):: List of services provided by this agent. Defaults to all methods exposed by actors.
120
+ # :secure(Boolean):: true indicates to use security features of RabbitMQ to restrict agents to themselves
121
+ # :single_threaded(Boolean):: true indicates to run all operations in one thread; false indicates
122
+ # to do requested work on EM defer thread and all else on main thread
123
+ # :threadpool_size(Integer):: Number of threads in EM thread pool
124
+ # :vhost(String):: AMQP broker virtual host
125
+ # :user(String):: AMQP broker user
126
+ # :pass(String):: AMQP broker password
127
+ # :host(String):: Comma-separated list of AMQP broker hosts; if only one, it is reapplied
128
+ # to successive ports; if none, defaults to 'localhost'
129
+ # :port(Integer):: Comma-separated list of AMQP broker ports corresponding to hosts; if only one,
130
+ # it is incremented and applied to successive hosts; if none, defaults to AMQP::HOST
131
+ #
132
+ # On start config.yml is read, so it is common to specify options in the YAML file. However, when both
133
+ # Ruby code options and YAML file specify options, Ruby code options take precedence.
134
+ #
135
+ # === Return
136
+ # agent(Agent):: New agent
137
+ def self.start(opts = {})
138
+ agent = new(opts)
139
+ agent.run
140
+ agent
141
+ end
142
+
143
+ # Initialize the new agent
144
+ #
145
+ # === Parameters
146
+ # opts(Hash):: Configuration options per #start above
147
+ #
148
+ # === Return
149
+ # true:: Always return true
150
+ def initialize(opts)
151
+ set_configuration(opts)
152
+ @tags = []
153
+ @tags << opts[:tag] if opts[:tag]
154
+ @tags.flatten!
155
+ @options.freeze
156
+ @terminating = false
157
+ @last_stat_reset_time = @service_start_time = Time.now
158
+ reset_agent_stats
159
+ true
160
+ end
161
+
162
+ # Put the agent in service
163
+ #
164
+ # === Return
165
+ # true:: Always return true
166
+ def run
167
+ Log.init(@identity, @options[:log_path], :print => true)
168
+ Log.level = @options[:log_level] if @options[:log_level]
169
+ Log.debug("Start options:")
170
+ log_opts = @options.inject([]){ |t, (k, v)| t << "- #{k}: #{v}" }
171
+ log_opts.each { |l| Log.debug(l) }
172
+
173
+ begin
174
+ # Capture process id in file after optional daemonize
175
+ pid_file = PidFile.new(@identity)
176
+ pid_file.check
177
+ daemonize(@identity, @options) if @options[:daemonize]
178
+ pid_file.write
179
+ at_exit { pid_file.remove }
180
+
181
+ # Initiate AMQP broker connection, wait for connection before proceeding
182
+ # otherwise messages published on failed connection will be lost
183
+ @broker = HABrokerClient.new(Serializer.new(:secure), @options)
184
+ @all_setup.each { |s| @remaining_setup[s] = @broker.all }
185
+ @broker.connection_status(:one_off => @options[:connect_timeout]) do |status|
186
+ if status == :connected
187
+ # Need to give EM (on Windows) a chance to respond to the AMQP handshake
188
+ # before doing anything interesting to prevent AMQP handshake from
189
+ # timing-out; delay post-connected activity a second.
190
+ EM.add_timer(1) do
191
+ begin
192
+ @registry = ActorRegistry.new
193
+ @dispatcher = Dispatcher.new(self)
194
+ @sender = Sender.new(self)
195
+ load_actors
196
+ setup_traps
197
+ setup_queues
198
+ start_console if @options[:console] && !@options[:daemonize]
199
+
200
+ # Need to keep reconnect interval at least :connect_timeout in size,
201
+ # otherwise connection_status callback will not timeout prior to next
202
+ # reconnect attempt, which can result in repeated attempts to setup
203
+ # queues when finally do connect
204
+ interval = [@options[:check_interval], @options[:connect_timeout]].max
205
+ @check_status_count = 0
206
+ @check_status_brokers = @broker.all
207
+ EM.next_tick { @options[:ready_callback].call } if @options[:ready_callback]
208
+ @check_status_timer = EM::PeriodicTimer.new(interval) { check_status }
209
+ rescue Exception => e
210
+ Log.error("Agent failed startup", e, :trace) unless e.message == "exit"
211
+ EM.stop
212
+ end
213
+ end
214
+ elsif status == :failed
215
+ Log.error("Agent failed to connect to any brokers")
216
+ EM.stop
217
+ end
218
+ end
219
+ rescue SystemExit => e
220
+ raise e
221
+ rescue PidFile::AlreadyRunning
222
+ raise
223
+ rescue Exception => e
224
+ Log.error("Agent failed startup", e, :trace) unless e.message == "exit"
225
+ raise e
226
+ end
227
+ true
228
+ end
229
+
230
+ # Register an actor for this agent
231
+ #
232
+ # === Parameters
233
+ # actor(Actor):: Actor to be registered
234
+ # prefix(String):: Prefix to be used in place of actor's default_prefix
235
+ #
236
+ # === Return
237
+ # (Actor):: Actor registered
238
+ def register(actor, prefix = nil)
239
+ @registry.register(actor, prefix)
240
+ end
241
+
242
+ # Connect to an additional broker or reconnect it if connection has failed
243
+ # Subscribe to identity queue on this broker
244
+ # Update config file if this is a new broker
245
+ # Assumes already has credentials on this broker and identity queue exists
246
+ #
247
+ # === Parameters
248
+ # host(String):: Host name of broker
249
+ # port(Integer):: Port number of broker
250
+ # index(Integer):: Small unique id associated with this broker for use in forming alias
251
+ # priority(Integer|nil):: Priority position of this broker in list for use
252
+ # by this agent with nil meaning add to end of list
253
+ # force(Boolean):: Reconnect even if already connected
254
+ #
255
+ # === Return
256
+ # res(String|nil):: Error message if failed, otherwise nil
257
+ def connect(host, port, index, priority = nil, force = false)
258
+ @connect_requests.update("connect b#{index}")
259
+ even_if = " even if already connected" if force
260
+ Log.info("Connecting to broker at host #{host.inspect} port #{port.inspect} " +
261
+ "index #{index.inspect} priority #{priority.inspect}#{even_if}")
262
+ Log.info("Current broker configuration: #{@broker.status.inspect}")
263
+ res = nil
264
+ begin
265
+ @broker.connect(host, port, index, priority, nil, force) do |id|
266
+ @broker.connection_status(:one_off => @options[:connect_timeout], :brokers => [id]) do |status|
267
+ begin
268
+ if status == :connected
269
+ setup_queues([id])
270
+ remaining = 0
271
+ @remaining_setup.each_value { |ids| remaining += ids.size }
272
+ Log.info("[setup] Finished subscribing to queues after reconnecting to broker #{id}") if remaining == 0
273
+ unless update_configuration(:host => @broker.hosts, :port => @broker.ports)
274
+ Log.warning("Successfully connected to broker #{id} but failed to update config file")
275
+ end
276
+ else
277
+ Log.error("Failed to connect to broker #{id}, status #{status.inspect}")
278
+ end
279
+ rescue Exception => e
280
+ Log.error("Failed to connect to broker #{id}, status #{status.inspect}", e)
281
+ @exceptions.track("connect", e)
282
+ end
283
+ end
284
+ end
285
+ rescue Exception => e
286
+ res = Log.format("Failed to connect to broker #{HABrokerClient.identity(host, port)}", e)
287
+ @exceptions.track("connect", e)
288
+ end
289
+ Log.error(res) if res
290
+ res
291
+ end
292
+
293
+ # Disconnect from a broker and optionally remove it from the configuration
294
+ # Refuse to do so if it is the last connected broker
295
+ #
296
+ # === Parameters
297
+ # host(String):: Host name of broker
298
+ # port(Integer):: Port number of broker
299
+ # remove(Boolean):: Whether to remove broker from configuration rather than just closing it,
300
+ # defaults to false
301
+ #
302
+ # === Return
303
+ # res(String|nil):: Error message if failed, otherwise nil
304
+ def disconnect(host, port, remove = false)
305
+ and_remove = " and removing" if remove
306
+ Log.info("Disconnecting#{and_remove} broker at host #{host.inspect} port #{port.inspect}")
307
+ Log.info("Current broker configuration: #{@broker.status.inspect}")
308
+ id = HABrokerClient.identity(host, port)
309
+ @connect_requests.update("disconnect #{@broker.alias_(id)}")
310
+ connected = @broker.connected
311
+ res = nil
312
+ if connected.include?(id) && connected.size == 1
313
+ res = "Not disconnecting from #{id} because it is the last connected broker for this agent"
314
+ elsif @broker.get(id)
315
+ begin
316
+ if remove
317
+ @broker.remove(host, port) do |id|
318
+ unless update_configuration(:host => @broker.hosts, :port => @broker.ports)
319
+ res = "Successfully disconnected from broker #{id} but failed to update config file"
320
+ end
321
+ end
322
+ else
323
+ @broker.close_one(id)
324
+ end
325
+ rescue Exception => e
326
+ res = Log.format("Failed to disconnect from broker #{id}", e)
327
+ @exceptions.track("disconnect", e)
328
+ end
329
+ else
330
+ res = "Cannot disconnect from broker #{id} because not configured for this agent"
331
+ end
332
+ Log.error(res) if res
333
+ res
334
+ end
335
+
336
+ # There were problems while setting up service for this agent on the given brokers,
337
+ # so mark these brokers as failed if not currently connected and later, during the
338
+ # periodic status check, attempt to reconnect
339
+ #
340
+ # === Parameters
341
+ # ids(Array):: Identity of brokers
342
+ #
343
+ # === Return
344
+ # res(String|nil):: Error message if failed, otherwise nil
345
+ def connect_failed(ids)
346
+ aliases = @broker.aliases(ids).join(", ")
347
+ @connect_requests.update("enroll failed #{aliases}")
348
+ res = nil
349
+ begin
350
+ Log.info("Received indication that service initialization for this agent for brokers #{ids.inspect} has failed")
351
+ connected = @broker.connected
352
+ ignored = connected & ids
353
+ Log.info("Not marking brokers #{ignored.inspect} as unusable because currently connected") if ignored
354
+ Log.info("Current broker configuration: #{@broker.status.inspect}")
355
+ @broker.declare_unusable(ids - ignored)
356
+ rescue Exception => e
357
+ res = Log.format("Failed handling broker connection failure indication for #{ids.inspect}", e)
358
+ Log.error(res)
359
+ @exceptions.track("connect failed", e)
360
+ end
361
+ res
362
+ end
363
+
364
+ # Handle packet received
365
+ #
366
+ # === Parameters
367
+ # packet(Request|Push|Result):: Packet received
368
+ #
369
+ # === Return
370
+ # true:: Always return true
371
+ def receive(packet)
372
+ begin
373
+ case packet
374
+ when Push, Request then @dispatcher.dispatch(packet) unless @terminating
375
+ when Result then @sender.handle_response(packet)
376
+ end
377
+ @sender.message_received
378
+ rescue HABrokerClient::NoConnectedBrokers => e
379
+ Log.error("Identity queue processing error", e)
380
+ rescue Exception => e
381
+ Log.error("Identity queue processing error", e, :trace)
382
+ @exceptions.track("identity queue", e, packet)
383
+ end
384
+ true
385
+ end
386
+
387
+ # Gracefully terminate execution by allowing unfinished tasks to complete
388
+ # Immediately terminate if called a second time
389
+ #
390
+ # === Block
391
+ # Optional block to be executed after termination is complete
392
+ #
393
+ # === Return
394
+ # true:: Always return true
395
+ def terminate(&blk)
396
+ begin
397
+ if @terminating
398
+ Log.info("[stop] Terminating immediately")
399
+ @termination_timer.cancel if @termination_timer
400
+ @termination_timer = nil
401
+ if blk then blk.call else EM.stop end
402
+ else
403
+ @terminating = true
404
+ @check_status_timer.cancel if @check_status_timer
405
+ @check_status_timer = nil
406
+ timeout = @options[:grace_timeout]
407
+ Log.info("[stop] Agent #{@identity} terminating")
408
+
409
+ stop_gracefully(timeout) do
410
+ if @sender
411
+ dispatch_age = @dispatcher.dispatch_age
412
+ request_count, request_age = @sender.terminate
413
+
414
+ finish = lambda do
415
+ request_count, request_age = @sender.terminate
416
+ Log.info("[stop] The following #{request_count} requests initiated as recently as #{request_age} " +
417
+ "seconds ago are being dropped:\n " + @sender.dump_requests.join("\n ")) if request_age
418
+ @broker.close(&blk)
419
+ EM.stop unless blk
420
+ end
421
+
422
+ wait_time = [timeout - (request_age || timeout), timeout - (dispatch_age || timeout), 0].max
423
+ if wait_time == 0
424
+ finish.call
425
+ else
426
+ reason = ""
427
+ reason = "completion of #{request_count} requests initiated as recently as #{request_age} seconds ago" if request_age
428
+ reason += " and " if request_age && dispatch_age
429
+ reason += "requests received as recently as #{dispatch_age} seconds ago" if dispatch_age
430
+ Log.info("[stop] Termination waiting #{wait_time} seconds for #{reason}")
431
+ @termination_timer = EM::Timer.new(wait_time) do
432
+ begin
433
+ Log.info("[stop] Continuing with termination")
434
+ finish.call
435
+ rescue Exception => e
436
+ Log.error("Failed while finishing termination", e, :trace)
437
+ EM.stop
438
+ end
439
+ end
440
+ end
441
+ end
442
+ end
443
+ end
444
+ rescue Exception => e
445
+ Log.error("Failed to terminate gracefully", e, :trace)
446
+ EM.stop
447
+ end
448
+ true
449
+ end
450
+
451
+ # Retrieve statistics about agent operation
452
+ #
453
+ # === Parameters:
454
+ # options(Hash):: Request options:
455
+ # :reset(Boolean):: Whether to reset the statistics after getting the current ones
456
+ #
457
+ # === Return
458
+ # result(OperationResult):: Always returns success
459
+ def stats(options = {})
460
+ now = Time.now
461
+ reset = options[:reset]
462
+ result = OperationResult.success("name" => @agent_name,
463
+ "identity" => @identity,
464
+ "hostname" => Socket.gethostname,
465
+ "version" => AgentConfig.protocol_version,
466
+ "brokers" => @broker.stats(reset),
467
+ "agent stats" => agent_stats(reset),
468
+ "receive stats" => @dispatcher.stats(reset),
469
+ "send stats" => @sender.stats(reset),
470
+ "last reset time" => @last_stat_reset_time.to_i,
471
+ "stat time" => now.to_i,
472
+ "service uptime" => (now - @service_start_time).to_i,
473
+ "machine uptime" => Platform.shell.uptime)
474
+ @last_stat_reset_time = now if reset
475
+ result
476
+ end
477
+
478
+ protected
479
+
480
+ # Get request statistics
481
+ #
482
+ # === Parameters
483
+ # reset(Boolean):: Whether to reset the statistics after getting the current ones
484
+ #
485
+ # === Return
486
+ # stats(Hash):: Current statistics:
487
+ # "connect requests"(Hash|nil):: Stats about requests to update connections with keys "total", "percent",
488
+ # and "last" with percentage breakdown by "connects: <alias>", "disconnects: <alias>", "enroll setup failed:
489
+ # <aliases>", or nil if none
490
+ # "exceptions"(Hash|nil):: Exceptions raised per category, or nil if none
491
+ # "total"(Integer):: Total exceptions for this category
492
+ # "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
493
+ # "non-deliveries"(Hash):: Message non-delivery activity stats with keys "total", "percent", "last", and "rate"
494
+ # with percentage breakdown by request type, or nil if none
495
+ def agent_stats(reset = false)
496
+ stats = {
497
+ "connect requests" => @connect_requests.all,
498
+ "exceptions" => @exceptions.stats,
499
+ "non-deliveries" => @non_deliveries.all
500
+ }
501
+ reset_agent_stats if reset
502
+ stats
503
+ end
504
+
505
+ # Reset cache statistics
506
+ #
507
+ # === Return
508
+ # true:: Always return true
509
+ def reset_agent_stats
510
+ @connect_requests = ActivityStats.new(measure_rate = false)
511
+ @non_deliveries = ActivityStats.new
512
+ @exceptions = ExceptionStats.new(self, @options[:exception_callback])
513
+ true
514
+ end
515
+
516
+ # Set the agent's configuration using the supplied options
517
+ #
518
+ # === Parameters
519
+ # opts(Hash):: Configuration options
520
+ #
521
+ # === Return
522
+ # (String):: Serialized agent identity
523
+ def set_configuration(opts)
524
+ @options = DEFAULT_OPTIONS.clone
525
+ @options.update(opts)
526
+
527
+ AgentConfig.root_dir = @options[:root_dir]
528
+ AgentConfig.pid_dir = @options[:pid_dir]
529
+
530
+ @options[:log_path] = false
531
+ if @options[:daemonize] || @options[:log_dir]
532
+ @options[:log_path] = (@options[:log_dir] || Platform.filesystem.log_dir)
533
+ FileUtils.mkdir_p(@options[:log_path]) unless File.directory?(@options[:log_path])
534
+ end
535
+
536
+ @identity = @options[:identity]
537
+ parsed_identity = AgentIdentity.parse(@identity)
538
+ @agent_type = parsed_identity.agent_type
539
+ @agent_name = @options[:agent_name]
540
+ @stats_routing_key = "stats.#{@agent_type}.#{parsed_identity.base_id}"
541
+
542
+ @remaining_setup = {}
543
+ @all_setup = [:setup_identity_queue]
544
+ @identity
545
+ end
546
+
547
+ # Update agent's persisted configuration
548
+ # Note that @options are frozen and therefore not updated
549
+ #
550
+ # === Parameters
551
+ # opts(Hash):: Options being updated
552
+ #
553
+ # === Return
554
+ # (Boolean):: true if successful, otherwise false
555
+ def update_configuration(opts)
556
+ if cfg = AgentConfig.load_cfg(@agent_name)
557
+ opts.each { |k, v| cfg[k] = v if cfg.has_key?(k) }
558
+ AgentConfig.store_cfg(@agent_name, cfg)
559
+ true
560
+ else
561
+ Log.error("Could not access configuration file #{AgentConfig.cfg_file(@agent_name).inspect} for update")
562
+ false
563
+ end
564
+ rescue Exception => e
565
+ Log.error("Failed updating configuration file #{AgentConfig.cfg_file(@agent_name).inspect}", e, :trace)
566
+ false
567
+ end
568
+
569
+ # Load the ruby code for the actors
570
+ #
571
+ # === Return
572
+ # true:: Always return true
573
+ def load_actors
574
+ # Load agent's configured actors
575
+ actors = (@options[:actors] || []).clone
576
+ Log.info("[setup] Agent #{@identity} with actors #{actors.inspect}")
577
+ actors_dirs = AgentConfig.actors_dirs
578
+ actors_dirs.each do |dir|
579
+ Dir["#{dir}/*.rb"].each do |file|
580
+ actor = File.basename(file, ".rb")
581
+ next if actors && !actors.include?(actor)
582
+ Log.info("[setup] loading actor #{file}")
583
+ require file
584
+ actors.delete(actor)
585
+ end
586
+ end
587
+ Log.error("Actors #{actors.inspect} not found in #{actors_dirs.inspect}") unless actors.empty?
588
+
589
+ # Perform agent-specific initialization including actor creation and registration
590
+ if init_file = AgentConfig.init_file
591
+ Log.info("[setup] initializing agent from #{init_file}")
592
+ instance_eval(File.read(init_file), init_file)
593
+ else
594
+ Log.error("No agent init.rb file found in init directory of #{AgentConfig.root_dir.inspect}")
595
+ end
596
+ true
597
+ end
598
+
599
+ # Setup the queues on the specified brokers for this agent
600
+ # Also configure message non-delivery handling
601
+ #
602
+ # === Parameters
603
+ # ids(Array):: Identity of brokers for which to subscribe, defaults to all usable
604
+ #
605
+ # === Return
606
+ # true:: Always return true
607
+ def setup_queues(ids = nil)
608
+ @broker.non_delivery do |reason, type, token, from, to|
609
+ begin
610
+ @non_deliveries.update(type)
611
+ reason = case reason
612
+ when "NO_ROUTE" then OperationResult::NO_ROUTE_TO_TARGET
613
+ when "NO_CONSUMERS" then OperationResult::TARGET_NOT_CONNECTED
614
+ else reason.to_s
615
+ end
616
+ result = Result.new(token, from, OperationResult.non_delivery(reason), to)
617
+ @sender.handle_response(result)
618
+ rescue Exception => e
619
+ Log.error("Failed handling non-delivery for <#{token}>", e, :trace)
620
+ @exceptions.track("message return", e)
621
+ end
622
+ end
623
+ # Do the setup regardless of whether remaining setup is empty since may be reconnecting
624
+ @all_setup.each { |setup| @remaining_setup[setup] -= self.__send__(setup, ids) }
625
+ true
626
+ end
627
+
628
+ # Setup identity queue for this agent
629
+ #
630
+ # === Parameters
631
+ # ids(Array):: Identity of brokers for which to subscribe, defaults to all usable
632
+ #
633
+ # === Return
634
+ # ids(Array):: Identity of brokers to which subscribe submitted (although may still fail)
635
+ def setup_identity_queue(ids = nil)
636
+ queue = {:name => @identity, :options => {:durable => true, :no_declare => @options[:secure]}}
637
+ filter = [:from, :tags, :tries, :persistent]
638
+ options = {:ack => true, Request => filter, Push => filter, Result => [:from], :brokers => ids}
639
+ ids = @broker.subscribe(queue, nil, options) { |_, packet| receive(packet) }
640
+ end
641
+
642
+ # Setup signal traps
643
+ #
644
+ # === Return
645
+ # true:: Always return true
646
+ def setup_traps
647
+ ['INT', 'TERM'].each do |sig|
648
+ old = trap(sig) do
649
+ EM.next_tick do
650
+ begin
651
+ terminate do
652
+ EM.stop
653
+ old.call if old.is_a? Proc
654
+ end
655
+ rescue Exception => e
656
+ Log.error("Failed in termination", e, :trace)
657
+ end
658
+ end
659
+ end
660
+ end
661
+ true
662
+ end
663
+
664
+ # Finish any remaining agent setup
665
+ #
666
+ # === Return
667
+ # true:: Always return true
668
+ def finish_setup
669
+ @broker.failed.each do |id|
670
+ p = {:agent_identity => @identity}
671
+ p[:host], p[:port], p[:id], p[:priority], _ = @broker.identity_parts(id)
672
+ @sender.send_push("/registrar/connect", p)
673
+ end
674
+ true
675
+ end
676
+
677
+ # Check status of agent by gathering current operation statistics and publishing them and
678
+ # by completing any queue setup that can be completed now based on broker status
679
+ #
680
+ # === Return
681
+ # true:: Always return true
682
+ def check_status
683
+ begin
684
+ finish_setup
685
+ rescue Exception => e
686
+ Log.error("Failed finishing setup", e)
687
+ @exceptions.track("check status", e)
688
+ end
689
+
690
+ begin
691
+ if @stats_routing_key
692
+ exchange = {:type => :topic, :name => "stats", :options => {:no_declare => true}}
693
+ @broker.publish(exchange, Stats.new(stats.content, @identity), :no_log => true,
694
+ :routing_key => @stats_routing_key, :brokers => @check_status_brokers.rotate!)
695
+ end
696
+ rescue Exception => e
697
+ Log.error("Failed publishing stats", e)
698
+ @exceptions.track("check status", e)
699
+ end
700
+
701
+ @check_status_count += 1
702
+ true
703
+ end
704
+
705
+ # Store unique tags
706
+ #
707
+ # === Parameters
708
+ # tags(Array):: Tags to be added
709
+ #
710
+ # === Return
711
+ # @tags(Array):: Current tags
712
+ def tag(*tags)
713
+ tags.each {|t| @tags << t}
714
+ @tags.uniq!
715
+ end
716
+
717
+ # Gracefully stop processing
718
+ #
719
+ # === Parameters
720
+ # timeout(Integer):: Maximum number of seconds to wait after last request received before
721
+ # terminating regardless of whether there are still unfinished requests
722
+ #
723
+ # === Block
724
+ # Required block to be executed after stopping message receipt wherever possible
725
+ #
726
+ # === Return
727
+ # true:: Always return true
728
+ def stop_gracefully(timeout)
729
+ @broker.unusable.each { |id| @broker.close_one(id, propagate = false) }
730
+ yield
731
+ end
732
+
733
+ end # Agent
734
+
735
+ end # RightScale