rightscale-nanite-dev 0.4.1.10

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 (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