rightscale-nanite-dev 0.4.1.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/LICENSE +201 -0
  2. data/README.rdoc +430 -0
  3. data/Rakefile +78 -0
  4. data/TODO +24 -0
  5. data/bin/nanite-admin +65 -0
  6. data/bin/nanite-agent +79 -0
  7. data/bin/nanite-mapper +50 -0
  8. data/lib/nanite.rb +74 -0
  9. data/lib/nanite/actor.rb +71 -0
  10. data/lib/nanite/actor_registry.rb +26 -0
  11. data/lib/nanite/admin.rb +138 -0
  12. data/lib/nanite/agent.rb +274 -0
  13. data/lib/nanite/amqp.rb +58 -0
  14. data/lib/nanite/cluster.rb +256 -0
  15. data/lib/nanite/config.rb +111 -0
  16. data/lib/nanite/console.rb +39 -0
  17. data/lib/nanite/daemonize.rb +13 -0
  18. data/lib/nanite/identity.rb +16 -0
  19. data/lib/nanite/job.rb +104 -0
  20. data/lib/nanite/local_state.rb +38 -0
  21. data/lib/nanite/log.rb +66 -0
  22. data/lib/nanite/log/formatter.rb +39 -0
  23. data/lib/nanite/mapper.rb +315 -0
  24. data/lib/nanite/mapper_proxy.rb +75 -0
  25. data/lib/nanite/nanite_dispatcher.rb +92 -0
  26. data/lib/nanite/packets.rb +401 -0
  27. data/lib/nanite/pid_file.rb +52 -0
  28. data/lib/nanite/reaper.rb +39 -0
  29. data/lib/nanite/redis_tag_store.rb +141 -0
  30. data/lib/nanite/security/cached_certificate_store_proxy.rb +24 -0
  31. data/lib/nanite/security/certificate.rb +55 -0
  32. data/lib/nanite/security/certificate_cache.rb +66 -0
  33. data/lib/nanite/security/distinguished_name.rb +34 -0
  34. data/lib/nanite/security/encrypted_document.rb +46 -0
  35. data/lib/nanite/security/rsa_key_pair.rb +53 -0
  36. data/lib/nanite/security/secure_serializer.rb +68 -0
  37. data/lib/nanite/security/signature.rb +46 -0
  38. data/lib/nanite/security/static_certificate_store.rb +35 -0
  39. data/lib/nanite/security_provider.rb +47 -0
  40. data/lib/nanite/serializer.rb +52 -0
  41. data/lib/nanite/state.rb +135 -0
  42. data/lib/nanite/streaming.rb +125 -0
  43. data/lib/nanite/util.rb +78 -0
  44. metadata +111 -0
@@ -0,0 +1,26 @@
1
+ module Nanite
2
+ class ActorRegistry
3
+ attr_reader :actors
4
+
5
+ def initialize
6
+ @actors = {}
7
+ end
8
+
9
+ def register(actor, prefix)
10
+ raise ArgumentError, "#{actor.inspect} is not a Nanite::Actor subclass instance" unless Nanite::Actor === actor
11
+ log_msg = "[actor] #{actor.class.to_s}"
12
+ log_msg += ", prefix #{prefix}" if prefix && !prefix.empty?
13
+ Nanite::Log.info(log_msg)
14
+ prefix ||= actor.class.default_prefix
15
+ actors[prefix.to_s] = actor
16
+ end
17
+
18
+ def services
19
+ actors.map {|prefix, actor| actor.class.provides_for(prefix) }.flatten.uniq
20
+ end
21
+
22
+ def actor_for(prefix)
23
+ actor = actors[prefix]
24
+ end
25
+ end # ActorRegistry
26
+ end # Nanite
@@ -0,0 +1,138 @@
1
+ require 'rack'
2
+
3
+ module Nanite
4
+ # This is a Rack app for nanite-admin. You need to have an async capable
5
+ # version of Thin installed for this to work. See bin/nanite-admin for install
6
+ # instructions.
7
+ class Admin
8
+ def initialize(mapper)
9
+ @mapper = mapper
10
+ end
11
+
12
+ AsyncResponse = [-1, {}, []].freeze
13
+
14
+ def call(env)
15
+ req = Rack::Request.new(env)
16
+ if cmd = req.params['command']
17
+ @command = cmd
18
+ @selection = req.params['type'] if req.params['type']
19
+
20
+ options = {}
21
+ case @selection
22
+ when 'least_loaded', 'random', 'all', 'rr'
23
+ options[:selector] = @selection
24
+ else
25
+ options[:target] = @selection
26
+ end
27
+
28
+ @mapper.request(cmd, req.params['payload'], options) do |response, responsejob|
29
+ env['async.callback'].call [200, {'Content-Type' => 'text/html'}, [layout(ul(response, responsejob))]]
30
+ end
31
+ AsyncResponse
32
+ else
33
+ [200, {'Content-Type' => 'text/html'}, layout]
34
+ end
35
+ end
36
+
37
+ def services
38
+ buf = "<select name='command'>"
39
+ @mapper.cluster.nanites.all_services.each do |srv|
40
+ buf << "<option value='#{srv}' #{@command == srv ? 'selected="true"' : ''}>#{srv}</option>"
41
+ end
42
+ buf << "</select>"
43
+ buf
44
+ end
45
+
46
+ def ul(hash, job)
47
+ buf = "<ul>"
48
+ hash.each do |k,v|
49
+ buf << "<li><div class=\"nanite\">#{k}:</div><div class=\"response\">#{v.inspect}</div>"
50
+ if job.intermediate_state && job.intermediate_state[k]
51
+ buf << "<div class=\"intermediatestates\"><span class=\"statenote\">intermediate state:</span> #{job.intermediate_state[k].inspect}</div>"
52
+ end
53
+ buf << "</li>"
54
+ end
55
+ buf << "</ul>"
56
+ buf
57
+ end
58
+
59
+ def layout(content=nil)
60
+ %Q{
61
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
62
+ <html xmlns='http://www.w3.org/1999/xhtml'>
63
+ <head>
64
+ <meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
65
+ <meta content='en' http-equiv='Content-Language' />
66
+ <meta content='Engineyard' name='author' />
67
+ <title>Nanite Control Tower</title>
68
+
69
+ <style>
70
+ body {margin: 0; font-family: verdana; background-color: #fcfcfc;}
71
+ ul {margin: 0; padding: 0; margin-left: 10px}
72
+ li {list-style-type: none; margin-bottom: 6px}
73
+ li .nanite {font-weight: bold; font-size: 12px}
74
+ li .response {padding: 8px}
75
+ li .intermediatestates {padding: 8px; font-size: 10px;}
76
+ li .intermediatestates span.statenote {font-style: italic;}
77
+ h1, h2, h3 {margin-top: none; padding: none; margin-left: 40px;}
78
+ h1 {font-size: 22px; margin-top: 40px; margin-bottom: 30px; border-bottom: 1px solid #ddd; padding-bottom: 6px;
79
+ margin-right: 40px}
80
+ h2 {font-size: 16px;}
81
+ h3 {margin-left: 0; font-size: 14px}
82
+ .section {border: 1px solid #ccc; background-color: #fefefe; padding: 10px; margin: 20px 40px; padding: 20px;
83
+ font-size: 14px}
84
+ #footer {text-align: center; color: #AAA; font-size: 12px}
85
+ </style>
86
+
87
+ </head>
88
+
89
+ <body>
90
+ <div id="header">
91
+ <h1>Nanite Control Tower</h1>
92
+ </div>
93
+
94
+ <h2>#{@mapper.options[:vhost]}</h2>
95
+ <div class="section">
96
+ <form method="post" action="/">
97
+ <input type="hidden" value="POST" name="_method"/>
98
+
99
+ <label>Send</label>
100
+ <select name="type">
101
+ <option #{@selection == 'least_loaded' ? 'selected="true"' : ''} value="least_loaded">the least loaded nanite</option>
102
+ <option #{@selection == 'random' ? 'selected="true"' : ''} value="random">a random nanite</option>
103
+ <option #{@selection == 'all' ? 'selected="true"' : ''} value="all">all nanites</option>
104
+ <option #{@selection == 'rr' ? 'selected="true"' : ''} value="rr">a nanite chosen by round robin</option>
105
+ #{@mapper.cluster.nanites.map {|k,v| "<option #{@selection == k ? 'selected="true"' : ''} value='#{k}'>#{k}</option>" }.join}
106
+ </select>
107
+
108
+ <label>providing service</label>
109
+ #{services}
110
+
111
+ <label>the payload</label>
112
+ <input type="text" class="text" name="payload" id="payload"/>
113
+
114
+ <input type="submit" class="submit" value="Go!" name="submit"/>
115
+ </form>
116
+
117
+ #{"<h3>Responses</h3>" if content}
118
+ #{content}
119
+ </div>
120
+
121
+ <h2>Running nanites</h2>
122
+ <div class="section">
123
+ #{"No nanites online." if @mapper.cluster.nanites.size == 0}
124
+ <ul>
125
+ #{@mapper.cluster.nanites.map {|k,v| "<li>identity : #{k}<br />load : #{v[:status]}<br />services : #{v[:services].to_a.inspect}<br />tags: #{v[:tags].to_a.inspect}</li>" }.join}
126
+ </ul>
127
+ </div>
128
+ <div id="footer">
129
+ Nanite #{Nanite::VERSION}
130
+ <br />
131
+ &copy; 2009 a bunch of random geeks
132
+ </div>
133
+ </body>
134
+ </html>
135
+ }
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,274 @@
1
+ module Nanite
2
+ class Agent
3
+ include AMQPHelper
4
+ include FileStreaming
5
+ include ConsoleHelper
6
+ include DaemonizeHelper
7
+
8
+ attr_reader :identity, :options, :serializer, :dispatcher, :registry, :amq, :tags
9
+ attr_accessor :status_proc
10
+
11
+ DEFAULT_OPTIONS = COMMON_DEFAULT_OPTIONS.merge({
12
+ :user => 'nanite',
13
+ :ping_time => 15,
14
+ :default_services => []
15
+ }) unless defined?(DEFAULT_OPTIONS)
16
+
17
+ # Initializes a new agent and establishes AMQP connection.
18
+ # This must be used inside EM.run block or if EventMachine reactor
19
+ # is already started, for instance, by a Thin server that your Merb/Rails
20
+ # application runs on.
21
+ #
22
+ # Agent options:
23
+ #
24
+ # identity : identity of this agent, may be any string
25
+ #
26
+ # status_proc : a callable object that returns agent load as a string,
27
+ # defaults to load averages string extracted from `uptime`
28
+ # format : format to use for packets serialization. One of the three:
29
+ # :marshall, :json, or :yaml. Defaults to
30
+ # Ruby's Marshall format. For interoperability with
31
+ # AMQP clients implemented in other languages, use JSON.
32
+ #
33
+ # Note that Nanite uses JSON gem,
34
+ # and ActiveSupport's JSON encoder may cause clashes
35
+ # if ActiveSupport is loaded after JSON gem.
36
+ #
37
+ # root : application root for this agent, defaults to Dir.pwd
38
+ #
39
+ # log_dir : path to directory where agent stores it's log file
40
+ # if not given, app_root is used.
41
+ #
42
+ # file_root : path to directory to files this agent provides
43
+ # defaults to app_root/files
44
+ #
45
+ # ping_time : time interval in seconds between two subsequent heartbeat messages
46
+ # this agent broadcasts. Default value is 15.
47
+ #
48
+ # console : true tells Nanite to start interactive console
49
+ #
50
+ # daemonize : true tells Nanite to daemonize
51
+ #
52
+ # pid_dir : path to the directory where the agent stores its pid file (only if daemonized)
53
+ # defaults to the root or the current working directory.
54
+ #
55
+ # services : list of services provided by this agent, by default
56
+ # all methods exposed by actors are listed
57
+ #
58
+ # single_threaded: Run all operations in one thread
59
+ #
60
+ # threadpool_size: Number of threads to run operations in
61
+ #
62
+ # Connection options:
63
+ #
64
+ # vhost : AMQP broker vhost that should be used
65
+ #
66
+ # user : AMQP broker user
67
+ #
68
+ # pass : AMQP broker password
69
+ #
70
+ # host : host AMQP broker (or node of interest) runs on,
71
+ # defaults to 0.0.0.0
72
+ #
73
+ # port : port AMQP broker (or node of interest) runs on,
74
+ # this defaults to 5672, port used by some widely
75
+ # used AMQP brokers (RabbitMQ and ZeroMQ)
76
+ #
77
+ # On start Nanite reads config.yml, so it is common to specify
78
+ # options in the YAML file. However, when both Ruby code options
79
+ # and YAML file specify option, Ruby code options take precedence.
80
+ def self.start(options = {})
81
+ agent = new(options)
82
+ agent.run
83
+ agent
84
+ end
85
+
86
+ def initialize(opts)
87
+ set_configuration(opts)
88
+ @tags = []
89
+ @tags << opts[:tag] if opts[:tag]
90
+ @tags.flatten!
91
+ @options.freeze
92
+ end
93
+
94
+ def run
95
+ Log.init(@identity, @options[:log_path])
96
+ Log.level = @options[:log_level] if @options[:log_level]
97
+ @serializer = Serializer.new(@options[:format])
98
+ @status_proc = lambda { parse_uptime(`uptime 2> /dev/null`) rescue 'no status' }
99
+ pid_file = PidFile.new(@identity, @options)
100
+ pid_file.check
101
+ if @options[:daemonize]
102
+ daemonize(@identity, @options)
103
+ pid_file.write
104
+ at_exit { pid_file.remove }
105
+ end
106
+ @amq = start_amqp(@options)
107
+ @registry = ActorRegistry.new
108
+ @dispatcher = Dispatcher.new(@amq, @registry, @serializer, @identity, @options)
109
+ setup_mapper_proxy
110
+ load_actors
111
+ setup_traps
112
+ setup_queue
113
+ advertise_services
114
+ setup_heartbeat
115
+ at_exit { un_register } unless $TESTING
116
+ start_console if @options[:console] && !@options[:daemonize]
117
+ end
118
+
119
+ def register(actor, prefix = nil)
120
+ registry.register(actor, prefix)
121
+ end
122
+
123
+ # Can be used in agent's initialization file to register a security module
124
+ # This security module 'authorize' method will be called back whenever the
125
+ # agent receives a request and will be given the corresponding deliverable.
126
+ # It should return 'true' for the request to proceed.
127
+ # Requests will return 'deny_token' or the string "Denied" by default when
128
+ # 'authorize' does not return 'true'.
129
+ def register_security(security, deny_token = "Denied")
130
+ @security = security
131
+ @deny_token = deny_token
132
+ end
133
+
134
+ # Update set of tags published by agent and notify mapper
135
+ # Add tags in 'new_tags' and remove tags in 'old_tags'
136
+ def update_tags(new_tags, old_tags)
137
+ @tags += (new_tags || [])
138
+ @tags -= (old_tags || [])
139
+ @tags.uniq!
140
+ tag_update = TagUpdate.new(identity, new_tags, old_tags)
141
+ amq.fanout('registration', :no_declare => options[:secure]).publish(serializer.dump(tag_update))
142
+ end
143
+
144
+ protected
145
+
146
+ def set_configuration(opts)
147
+ @options = DEFAULT_OPTIONS.clone
148
+ root = opts[:root] || @options[:root]
149
+ custom_config = if root
150
+ file = File.expand_path(File.join(root, 'config.yml'))
151
+ File.exists?(file) ? (YAML.load(IO.read(file)) || {}) : {}
152
+ else
153
+ {}
154
+ end
155
+ opts.delete(:identity) unless opts[:identity]
156
+ @options.update(custom_config.merge(opts))
157
+ @options[:file_root] ||= File.join(@options[:root], 'files')
158
+ @options[:log_path] = false
159
+ if @options[:daemonize]
160
+ @options[:log_path] = (@options[:log_dir] || @options[:root] || Dir.pwd)
161
+ end
162
+
163
+ return @identity = "nanite-#{@options[:identity]}" if @options[:identity]
164
+ token = Identity.generate
165
+ @identity = "nanite-#{token}"
166
+ File.open(File.expand_path(File.join(@options[:root], 'config.yml')), 'w') do |fd|
167
+ fd.write(YAML.dump(custom_config.merge(:identity => token)))
168
+ end
169
+ end
170
+
171
+ def load_actors
172
+ return unless options[:root]
173
+ actors_dir = @options[:actors_dir] || "#{@options[:root]}/actors"
174
+ Nanite::Log.warn("Actors dir #{actors_dir} does not exist or is not reachable") unless File.directory?(actors_dir)
175
+ actors = @options[:actors]
176
+ Dir["#{actors_dir}/*.rb"].each do |actor|
177
+ next if actors && !actors.include?(File.basename(actor, ".rb"))
178
+ Nanite::Log.info("[setup] loading #{actor}")
179
+ require actor
180
+ end
181
+ init_path = @options[:initrb] || File.join(options[:root], 'init.rb')
182
+ if File.exist?(init_path)
183
+ instance_eval(File.read(init_path), init_path)
184
+ else
185
+ Nanite::Log.warn("init.rb #{init_path} does not exist or is not reachable") unless File.exists?(init_path)
186
+ end
187
+ end
188
+
189
+ def receive(packet)
190
+ Nanite::Log.debug("RECV #{packet.to_s}")
191
+ case packet
192
+ when Advertise
193
+ Nanite::Log.info("RECV #{packet.to_s}") unless Nanite::Log.level == :debug
194
+ advertise_services
195
+ when Request, Push
196
+ if @security && !@security.authorize(packet)
197
+ Nanite::Log.warn("RECV NOT AUTHORIZED #{packet.to_s}")
198
+ if packet.kind_of?(Request)
199
+ r = Result.new(packet.token, packet.reply_to, @deny_token, identity)
200
+ amq.queue(packet.reply_to, :no_declare => options[:secure]).publish(serializer.dump(r))
201
+ end
202
+ else
203
+ Nanite::Log.info("RECV #{packet.to_s([:from, :tags])}") unless Nanite::Log.level == :debug
204
+ dispatcher.dispatch(packet)
205
+ end
206
+ when Result
207
+ Nanite::Log.info("RECV #{packet.to_s([])}") unless Nanite::Log.level == :debug
208
+ @mapper_proxy.handle_result(packet)
209
+ when IntermediateMessage
210
+ Nanite::Log.info("RECV #{packet.to_s([])}") unless Nanite::Log.level == :debug
211
+ @mapper_proxy.handle_intermediate_result(packet)
212
+ end
213
+ end
214
+
215
+ def tag(*tags)
216
+ tags.each {|t| @tags << t}
217
+ @tags.uniq!
218
+ end
219
+
220
+ def setup_queue
221
+ amq.queue(identity, :durable => true).subscribe(:ack => true) do |info, msg|
222
+ begin
223
+ info.ack
224
+ receive(serializer.load(msg))
225
+ rescue Exception => e
226
+ Nanite::Log.error("RECV #{e.message}")
227
+ end
228
+ end
229
+ end
230
+
231
+ def setup_heartbeat
232
+ EM.add_periodic_timer(options[:ping_time]) do
233
+ amq.fanout('heartbeat', :no_declare => options[:secure]).publish(serializer.dump(Ping.new(identity, status_proc.call)))
234
+ end
235
+ end
236
+
237
+ def setup_mapper_proxy
238
+ @mapper_proxy = MapperProxy.new(identity, options)
239
+ end
240
+
241
+ def setup_traps
242
+ ['INT', 'TERM'].each do |sig|
243
+ old = trap(sig) do
244
+ un_register
245
+ amq.instance_variable_get('@connection').close do
246
+ EM.stop
247
+ old.call if old.is_a? Proc
248
+ end
249
+ end
250
+ end
251
+ end
252
+
253
+ def un_register
254
+ unless @unregistered
255
+ @unregistered = true
256
+ Nanite::Log.info("SEND [un_register]")
257
+ amq.fanout('registration', :no_declare => options[:secure]).publish(serializer.dump(UnRegister.new(identity)))
258
+ end
259
+ end
260
+
261
+ def advertise_services
262
+ reg = Register.new(identity, registry.services, status_proc.call, self.tags)
263
+ Nanite::Log.info("SEND #{reg.to_s}")
264
+ amq.fanout('registration', :no_declare => options[:secure]).publish(serializer.dump(reg))
265
+ end
266
+
267
+ def parse_uptime(up)
268
+ if up =~ /load averages?: (.*)/
269
+ a,b,c = $1.split(/\s+|,\s+/)
270
+ (a.to_f + b.to_f + c.to_f) / 3
271
+ end
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,58 @@
1
+ class MQ
2
+ class Queue
3
+ # Asks the broker to redeliver all unacknowledged messages on a
4
+ # specifieid channel. Zero or more messages may be redelivered.
5
+ #
6
+ # * requeue (default false)
7
+ # If this parameter is false, the message will be redelivered to the original recipient.
8
+ # If this flag is true, the server will attempt to requeue the message, potentially then
9
+ # delivering it to an alternative subscriber.
10
+ #
11
+ def recover requeue = false
12
+ @mq.callback{
13
+ @mq.send Protocol::Basic::Recover.new({ :requeue => requeue })
14
+ }
15
+ self
16
+ end
17
+ end
18
+ end
19
+
20
+ # monkey patch to the amqp gem that adds :no_declare => true option for new
21
+ # Exchange objects. This allows us to send messeages to exchanges that are
22
+ # declared by the mappers and that we have no configuration priviledges on.
23
+ # temporary until we get this into amqp proper
24
+ MQ::Exchange.class_eval do
25
+ def initialize mq, type, name, opts = {}
26
+ @mq = mq
27
+ @type, @name, @opts = type, name, opts
28
+ @mq.exchanges[@name = name] ||= self
29
+ @key = opts[:key]
30
+
31
+ @mq.callback{
32
+ @mq.send AMQP::Protocol::Exchange::Declare.new({ :exchange => name,
33
+ :type => type,
34
+ :nowait => true }.merge(opts))
35
+ } unless name == "amq.#{type}" or name == '' or opts[:no_declare]
36
+ end
37
+ end
38
+
39
+ module Nanite
40
+ module AMQPHelper
41
+ def start_amqp(options)
42
+ connection = AMQP.connect({
43
+ :user => options[:user],
44
+ :pass => options[:pass],
45
+ :vhost => options[:vhost],
46
+ :host => options[:host],
47
+ :port => (options[:port] || ::AMQP::PORT).to_i,
48
+ :insist => options[:insist] || false,
49
+ :retry => options[:retry] || 5,
50
+ :connection_status => options[:connection_callback] || proc {|event|
51
+ Nanite::Log.debug("CONNECTED to MQ") if event == :connected
52
+ Nanite::Log.debug("DISCONNECTED from MQ") if event == :disconnected
53
+ }
54
+ })
55
+ MQ.new(connection)
56
+ end
57
+ end
58
+ end