ezmobius-nanite 0.4.0 → 0.4.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/README.rdoc +70 -20
  2. data/Rakefile +1 -1
  3. data/bin/nanite-agent +34 -8
  4. data/bin/nanite-mapper +18 -8
  5. data/lib/nanite.rb +71 -0
  6. data/lib/nanite/actor.rb +60 -0
  7. data/lib/nanite/actor_registry.rb +24 -0
  8. data/lib/nanite/admin.rb +138 -0
  9. data/lib/nanite/agent.rb +250 -0
  10. data/lib/nanite/amqp.rb +47 -0
  11. data/lib/nanite/cluster.rb +203 -0
  12. data/lib/nanite/config.rb +102 -0
  13. data/lib/nanite/console.rb +39 -0
  14. data/lib/nanite/daemonize.rb +13 -0
  15. data/lib/nanite/dispatcher.rb +90 -0
  16. data/lib/nanite/identity.rb +16 -0
  17. data/lib/nanite/job.rb +104 -0
  18. data/lib/nanite/local_state.rb +34 -0
  19. data/lib/nanite/log.rb +64 -0
  20. data/lib/nanite/log/formatter.rb +39 -0
  21. data/lib/nanite/mapper.rb +277 -0
  22. data/lib/nanite/mapper_proxy.rb +56 -0
  23. data/lib/nanite/packets.rb +231 -0
  24. data/lib/nanite/pid_file.rb +52 -0
  25. data/lib/nanite/reaper.rb +38 -0
  26. data/lib/nanite/security/cached_certificate_store_proxy.rb +24 -0
  27. data/lib/nanite/security/certificate.rb +55 -0
  28. data/lib/nanite/security/certificate_cache.rb +66 -0
  29. data/lib/nanite/security/distinguished_name.rb +34 -0
  30. data/lib/nanite/security/encrypted_document.rb +46 -0
  31. data/lib/nanite/security/rsa_key_pair.rb +53 -0
  32. data/lib/nanite/security/secure_serializer.rb +67 -0
  33. data/lib/nanite/security/signature.rb +40 -0
  34. data/lib/nanite/security/static_certificate_store.rb +35 -0
  35. data/lib/nanite/security_provider.rb +47 -0
  36. data/lib/nanite/serializer.rb +52 -0
  37. data/lib/nanite/state.rb +164 -0
  38. data/lib/nanite/streaming.rb +125 -0
  39. data/lib/nanite/util.rb +51 -0
  40. data/spec/actor_registry_spec.rb +62 -0
  41. data/spec/actor_spec.rb +59 -0
  42. data/spec/agent_spec.rb +235 -0
  43. data/spec/cached_certificate_store_proxy_spec.rb +34 -0
  44. data/spec/certificate_cache_spec.rb +49 -0
  45. data/spec/certificate_spec.rb +27 -0
  46. data/spec/cluster_spec.rb +300 -0
  47. data/spec/dispatcher_spec.rb +136 -0
  48. data/spec/distinguished_name_spec.rb +24 -0
  49. data/spec/encrypted_document_spec.rb +21 -0
  50. data/spec/job_spec.rb +219 -0
  51. data/spec/local_state_spec.rb +112 -0
  52. data/spec/packet_spec.rb +218 -0
  53. data/spec/rsa_key_pair_spec.rb +33 -0
  54. data/spec/secure_serializer_spec.rb +41 -0
  55. data/spec/serializer_spec.rb +107 -0
  56. data/spec/signature_spec.rb +30 -0
  57. data/spec/spec_helper.rb +23 -0
  58. data/spec/static_certificate_store_spec.rb +30 -0
  59. data/spec/util_spec.rb +63 -0
  60. metadata +63 -2
data/README.rdoc CHANGED
@@ -228,7 +228,7 @@ Second shell
228
228
 
229
229
  Now run a mapper. Mappers can be run from within your Merb or Rails app, from an interactive irb shell, or from the command line. For this example we'll run it from the command line so open a third shell window and run the following:
230
230
 
231
- cd examples
231
+ cd examples/simpleagent
232
232
  ./cli.rb
233
233
 
234
234
  Which should soon return something like the following.
@@ -296,32 +296,82 @@ Mongrel on the other hand does not use EventMachine and therefore requires to wr
296
296
 
297
297
  Using nanite with Passenger:
298
298
 
299
- current = Thread.current
300
- Thread.new do
301
- AMQP.start(:host => AMQP_HOST) do
302
- current.wakeup
303
- end
304
- end
305
- Thread.stop
306
-
307
- # catch these, stop AMQP, stop eventmachine, and re-throw to
308
- Mongrel/Passenger's signal traps
309
- EM.run do
310
- ['INT', 'TERM'].each do |sig|
311
- old = trap(sig) do
312
- AMQP.stop do
313
- EM.stop
314
- old.call
315
- end
316
- end
317
- end
299
+ if defined?(PhusionPassenger)
300
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
301
+ if forked
302
+ if EM.reactor_running?
303
+ EM.stop_event_loop
304
+ EM.release_machine
305
+ EM.instance_variable_set( '@reactor_running', false )
318
306
  end
307
+ Thread.current[:mq] = nil
308
+ AMQP.instance_variable_set('@conn', nil)
309
+ end
310
+
311
+ th = Thread.current
312
+ Thread.new{
313
+ Nanite.start_mapper(:host => 'localhost', :user => 'mapper', :pass => 'testing', :vhost => '/nanite', :log_level => 'info')
314
+ }
315
+ Thread.stop
316
+ end
317
+ end
319
318
 
320
319
  =======
321
320
  Where to put the mapper initialization code depends on the framework and our preference.
322
321
  For Rails the canonical place to start our mapper is within nanite.rb (or any other filename you prefer) in config/initalizers.
323
322
  In Merb we can use init.rb in config.
324
323
 
324
+ == Security
325
+
326
+ Nanite implements a secure serializer which can be used in place of the other serializers to encrypt the
327
+ AMQP messages exchanged between the mappers and the agents.
328
+
329
+ The secure serializer uses X.509 certificates and cryptographic keys to sign and encrypt the messages.
330
+
331
+ It is important to understand that:
332
+ 1. A certificate only includes the public key component of a cryptographic key
333
+ pair.
334
+ 2. Signing requires the use of a certificate and its private key, checking the
335
+ signature then only requires the certificate (the idea is that only the
336
+ signer has the secret private key and thus can sign but anyone can check the
337
+ signature).
338
+ 3. Encrypting only requires the certificate but decrypting also requires the
339
+ private key (anyone can encrypt the data but only the intended recipient can
340
+ decrypt it).
341
+
342
+ A signing serializer thus needs access to the signer certificate and private
343
+ key. An encrypting serializer *also* needs access to the intended recipients
344
+ certificates. There needs to be a way to dynamically retrieve the corresponding
345
+ certificates. This is done using certificate stores.
346
+
347
+ Certificate stores associate identities with certificates. The identity is
348
+ associated when the data is serialized and can be keyed off to retrieve the
349
+ right certificate upon deserialization.
350
+
351
+ Nanite provides a static store implementation which can be used when the
352
+ certificates used for serialization are always the same and can be kept in
353
+ memory. Nanite also provides a certificate store proxy cache which can be
354
+ associated with any store implementation and will cache the most used
355
+ certificates.
356
+
357
+ The serializer should be initialized prior to being used by calling the 'init'
358
+ method:
359
+
360
+ # Initialize serializer, must be called prior to using it.
361
+ #
362
+ # - 'identity': Identity associated with serialized messages
363
+ # - 'cert': Certificate used to sign serialized messages and
364
+ # decrypt encrypted messages
365
+ # - 'key': Private key corresponding to 'cert'
366
+ # - 'store': Certificate store. Exposes certificates used for
367
+ # encryption and signature validation.
368
+ # - 'encrypt': Whether data should be signed and encrypted ('true')
369
+ # or just signed ('false'), 'true' by default.
370
+ #
371
+ def SecureSerializer.init(identity, cert, key, store, encrypt = true)
372
+
373
+ The 'secure' agent example (examples\secure) shows how the mappers and agents should be
374
+ configured to use the secure serializer.
325
375
 
326
376
  == Troubleshooting
327
377
 
data/Rakefile CHANGED
@@ -10,7 +10,7 @@ end
10
10
  require 'rake/clean'
11
11
 
12
12
  GEM = "nanite"
13
- VER = "0.4.0"
13
+ VER = "0.4.1"
14
14
  AUTHOR = "Ezra Zygmuntowicz"
15
15
  EMAIL = "ezra@engineyard.com"
16
16
  HOMEPAGE = "http://github.com/ezmobius/nanite"
data/bin/nanite-agent CHANGED
@@ -21,25 +21,51 @@ opts = OptionParser.new do |opts|
21
21
  opts.on("--ping-time PINGTIME", "Specify how often the agents contacts the mapper") do |ping|
22
22
  options[:ping_time] = ping
23
23
  end
24
+
25
+ opts.on("--actors-dir DIR", "Path to directory containing actors (NANITE_ROOT/actors by default)") do |dir|
26
+ options[:actors_dir] = dir
27
+ end
28
+
29
+ opts.on("--actors ACTORS", "Comma separated list of actors to load (all ruby files in actors directory by default)") do |a|
30
+ options[:actors] = a.split(',')
31
+ end
32
+
33
+ opts.on("--initrb FILE", "Path to agent initialization file (NANITE_ROOT/init.rb by default)") do |initrb|
34
+ options[:initrb] = initrb
35
+ end
36
+
37
+ opts.on("--single-threaded", "Run all operations in one thread") do
38
+ options[:single_threaded] = true
39
+ end
24
40
  end
25
41
 
26
42
  opts.parse!
27
43
 
28
- if ARGV[0] == 'stop'
44
+ if ARGV[0] == 'stop' || ARGV[0] == 'status'
29
45
  agent = Nanite::Agent.new(options)
30
46
  pid_file = Nanite::PidFile.new(agent.identity, agent.options)
31
47
  unless pid = pid_file.read_pid
32
48
  puts "#{pid_file} not found"
33
49
  exit
34
50
  end
35
- puts "Stopping nanite agent #{agent.identity} (pid #{pid})"
36
- begin
37
- Process.kill('TERM', pid)
38
- rescue Errno::ESRCH
39
- puts "Process does not exist (pid #{pid})"
40
- exit
51
+ if ARGV[0] == 'stop'
52
+ puts "Stopping nanite agent #{agent.identity} (pid #{pid})"
53
+ begin
54
+ Process.kill('TERM', pid)
55
+ rescue Errno::ESRCH
56
+ puts "Process does not exist (pid #{pid})"
57
+ exit
58
+ end
59
+ puts 'Done.'
60
+ else
61
+ if Process.getpgid(pid) != -1
62
+ psdata = `ps up #{pid}`.split("\n").last.split
63
+ memory = (psdata[5].to_i / 1024)
64
+ puts "The agent is alive, using #{memory}MB of memory"
65
+ else
66
+ puts "The agent is not running but has a stale pid file at #{pid_file}"
67
+ end
41
68
  end
42
- puts 'Done.'
43
69
  exit
44
70
  end
45
71
 
data/bin/nanite-mapper CHANGED
@@ -17,21 +17,31 @@ end
17
17
 
18
18
  opts.parse!
19
19
 
20
- if ARGV[0] == 'stop'
20
+ if ARGV[0] == 'stop' || ARGV[0] == 'status'
21
21
  mapper = Nanite::Mapper.new(options)
22
22
  pid_file = Nanite::PidFile.new(mapper.identity, mapper.options)
23
23
  unless pid = pid_file.read_pid
24
24
  puts "#{pid_file} not found"
25
25
  exit
26
26
  end
27
- puts "Stopping nanite mapper #{mapper.identity} (pid #{pid})"
28
- begin
29
- Process.kill('TERM', pid)
30
- rescue Errno::ESRCH
31
- puts "Process does not exist (pid #{pid})"
32
- exit
27
+ if ARGV[0] == 'stop'
28
+ puts "Stopping nanite mapper #{mapper.identity} (pid #{pid})"
29
+ begin
30
+ Process.kill('TERM', pid)
31
+ rescue Errno::ESRCH
32
+ puts "Process does not exist (pid #{pid})"
33
+ exit
34
+ end
35
+ puts 'Done.'
36
+ else
37
+ if Process.getpgid(pid) != -1
38
+ psdata = `ps up #{pid}`.split("\n").last.split
39
+ memory = (psdata[5].to_i / 1024)
40
+ puts "The mapper is alive, using #{memory}MB of memory"
41
+ else
42
+ puts "The mapper is not running but has a stale pid file at #{pid_file}"
43
+ end
33
44
  end
34
- puts 'Done.'
35
45
  exit
36
46
  end
37
47
 
data/lib/nanite.rb ADDED
@@ -0,0 +1,71 @@
1
+ require 'rubygems'
2
+ require 'amqp'
3
+ require 'mq'
4
+ require 'json'
5
+ require 'logger'
6
+ require 'yaml'
7
+ require 'openssl'
8
+
9
+ $:.unshift File.dirname(__FILE__)
10
+ require 'nanite/amqp'
11
+ require 'nanite/util'
12
+ require 'nanite/config'
13
+ require 'nanite/packets'
14
+ require 'nanite/identity'
15
+ require 'nanite/console'
16
+ require 'nanite/daemonize'
17
+ require 'nanite/pid_file'
18
+ require 'nanite/job'
19
+ require 'nanite/mapper'
20
+ require 'nanite/actor'
21
+ require 'nanite/actor_registry'
22
+ require 'nanite/streaming'
23
+ require 'nanite/dispatcher'
24
+ require 'nanite/agent'
25
+ require 'nanite/cluster'
26
+ require 'nanite/reaper'
27
+ require 'nanite/log'
28
+ require 'nanite/mapper_proxy'
29
+ require 'nanite/security_provider'
30
+ require 'nanite/security/cached_certificate_store_proxy'
31
+ require 'nanite/security/certificate'
32
+ require 'nanite/security/certificate_cache'
33
+ require 'nanite/security/distinguished_name'
34
+ require 'nanite/security/encrypted_document'
35
+ require 'nanite/security/rsa_key_pair'
36
+ require 'nanite/security/secure_serializer'
37
+ require 'nanite/security/signature'
38
+ require 'nanite/security/static_certificate_store'
39
+ require 'nanite/serializer'
40
+
41
+ module Nanite
42
+ VERSION = '0.4.0' unless defined?(Nanite::VERSION)
43
+
44
+ class MapperNotRunning < StandardError; end
45
+
46
+ class << self
47
+ attr_reader :mapper, :agent
48
+
49
+ def start_agent(options = {})
50
+ @agent = Nanite::Agent.start(options)
51
+ end
52
+
53
+ def start_mapper(options = {})
54
+ @mapper = Nanite::Mapper.start(options)
55
+ end
56
+
57
+ def request(*args, &blk)
58
+ ensure_mapper
59
+ @mapper.request(*args, &blk)
60
+ end
61
+
62
+ def push(*args)
63
+ ensure_mapper
64
+ @mapper.push(*args)
65
+ end
66
+
67
+ def ensure_mapper
68
+ raise MapperNotRunning.new('A mapper needs to be started via Nanite.start_mapper') unless @mapper
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,60 @@
1
+ module Nanite
2
+ # This mixin provides Nanite actor functionality.
3
+ #
4
+ # To use it simply include it your class containing the functionality to be exposed:
5
+ #
6
+ # class Foo
7
+ # include Nanite::Actor
8
+ # expose :bar
9
+ #
10
+ # def bar(payload)
11
+ # # ...
12
+ # end
13
+ #
14
+ # end
15
+ module Actor
16
+
17
+ def self.included(base)
18
+ base.class_eval do
19
+ include Nanite::Actor::InstanceMethods
20
+ extend Nanite::Actor::ClassMethods
21
+ end # base.class_eval
22
+ end # self.included
23
+
24
+ module ClassMethods
25
+ def default_prefix
26
+ to_s.to_const_path
27
+ end
28
+
29
+ def expose(*meths)
30
+ @exposed ||= []
31
+ meths.each do |meth|
32
+ @exposed << meth unless @exposed.include?(meth)
33
+ end
34
+ end
35
+
36
+ def provides_for(prefix)
37
+ return [] unless @exposed
38
+ @exposed.map {|meth| "/#{prefix}/#{meth}".squeeze('/')}
39
+ end
40
+
41
+ def on_exception(proc = nil, &blk)
42
+ raise 'No callback provided for on_exception' unless proc || blk
43
+ @exception_callback = proc || blk
44
+ end
45
+
46
+ def exception_callback
47
+ @exception_callback
48
+ end
49
+
50
+ end # ClassMethods
51
+
52
+ module InstanceMethods
53
+ # send nanite request to another agent (through the mapper)
54
+ def request(*args, &blk)
55
+ MapperProxy.instance.request(*args, &blk)
56
+ end
57
+ end # InstanceMethods
58
+
59
+ end # Actor
60
+ end # Nanite
@@ -0,0 +1,24 @@
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
+ Nanite::Log.info("Registering #{actor.inspect} with prefix #{prefix.inspect}")
12
+ prefix ||= actor.class.default_prefix
13
+ actors[prefix.to_s] = actor
14
+ end
15
+
16
+ def services
17
+ actors.map {|prefix, actor| actor.class.provides_for(prefix) }.flatten.uniq
18
+ end
19
+
20
+ def actor_for(prefix)
21
+ actor = actors[prefix]
22
+ end
23
+ end # ActorRegistry
24
+ 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