right_agent 0.5.1

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