ezmobius-nanite 0.4.0 → 0.4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -0,0 +1,40 @@
1
+ module Nanite
2
+
3
+ # Signature that can be validated against certificates
4
+ class Signature
5
+
6
+ FLAGS = OpenSSL::PKCS7::NOCERTS || OpenSSL::PKCS7::BINARY || OpenSSL::PKCS7::NOATTR || OpenSSL::PKCS7::NOSMIMECAP || OpenSSL::PKCS7::DETACH
7
+
8
+ # Create signature using certificate and key pair.
9
+ #
10
+ # Arguments:
11
+ # - 'data': Data to be signed
12
+ # - 'cert': Certificate used for signature
13
+ # - 'key': RsaKeyPair used for signature
14
+ #
15
+ def initialize(data, cert, key)
16
+ @p7 = OpenSSL::PKCS7.sign(cert.raw_cert, key.raw_key, data, [], FLAGS)
17
+ @store = OpenSSL::X509::Store.new
18
+ end
19
+
20
+ # Load signature previously serialized via 'data'
21
+ def self.from_data(data)
22
+ sig = Signature.allocate
23
+ sig.instance_variable_set(:@p7, OpenSSL::PKCS7::PKCS7.new(data))
24
+ sig.instance_variable_set(:@store, OpenSSL::X509::Store.new)
25
+ sig
26
+ end
27
+
28
+ # 'true' if signature was created using given cert, 'false' otherwise
29
+ def match?(cert)
30
+ @p7.verify([cert.raw_cert], @store, nil, OpenSSL::PKCS7::NOVERIFY)
31
+ end
32
+
33
+ # Signature in PEM format
34
+ def data
35
+ @p7.to_pem
36
+ end
37
+ alias :to_s :data
38
+
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ module Nanite
2
+
3
+ # Simple certificate store, serves a static set of certificates.
4
+ class StaticCertificateStore
5
+
6
+ # Initialize store:
7
+ #
8
+ # - Signer certificates are used when loading data to check the digital
9
+ # signature. The signature associated with the serialized data needs
10
+ # to match with one of the signer certificates for loading to succeed.
11
+ #
12
+ # - Recipient certificates are used when serializing data for encryption.
13
+ # Loading the data can only be done through serializers that have been
14
+ # initialized with a certificate that's in the recipient certificates if
15
+ # encryption is enabled.
16
+ #
17
+ def initialize(signer_certs, recipients_certs)
18
+ signer_certs = [ signer_certs ] unless signer_certs.respond_to?(:each)
19
+ @signer_certs = signer_certs
20
+ recipients_certs = [ recipients_certs ] unless recipients_certs.respond_to?(:each)
21
+ @recipients_certs = recipients_certs
22
+ end
23
+
24
+ # Retrieve signer certificate for given id
25
+ def get_signer(identity)
26
+ @signer_certs
27
+ end
28
+
29
+ # Recipient certificate(s) that will be able to decrypt the serialized data
30
+ def get_recipients(obj)
31
+ @recipients_certs
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ module Nanite
2
+ # This class is used to interface the nanite mapper with an external security
3
+ # module.
4
+ # There are two points of integration:
5
+ # 1. When an agent registers with a mapper
6
+ # 2. When an agent sends a request to another agent
7
+ #
8
+ # In both these cases the security module is called back and can deny the
9
+ # operation.
10
+ # Note: it's the responsability of the module to do any logging or
11
+ # notification that is required.
12
+ class SecurityProvider
13
+
14
+ # Register an external security module
15
+ # This module should expose the 'authorize_registration' and
16
+ # 'authorize_request' methods.
17
+ def self.register(mod)
18
+ @security_module = mod
19
+ end
20
+
21
+ # Used internally by nanite to retrieve the current security module
22
+ def self.get
23
+ @security_module || default_security_module
24
+ end
25
+
26
+ # Default security module, authorizes all operations
27
+ def self.default_security_module
28
+ @default_sec_mod ||= DefaultSecurityModule.new
29
+ end
30
+
31
+ end
32
+
33
+ # Default security module
34
+ class DefaultSecurityModule
35
+
36
+ # Authorize registration of agent (registration is an instance of Register)
37
+ def authorize_registration(registration)
38
+ true
39
+ end
40
+
41
+ # Authorize given inter-agent request (request is an instance of Request)
42
+ def authorize_request(request)
43
+ true
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,52 @@
1
+ module Nanite
2
+ class Serializer
3
+
4
+ class SerializationError < StandardError
5
+ attr_accessor :action, :packet
6
+ def initialize(action, packet, serializers, msg = nil)
7
+ @action, @packet = action, packet
8
+ msg = ":\n#{msg}" if msg && !msg.empty?
9
+ super("Could not #{action} #{packet.inspect} using #{serializers.inspect}#{msg}")
10
+ end
11
+ end # SerializationError
12
+
13
+ # The secure serializer should not be part of the cascading
14
+ def initialize(preferred_format = :marshal)
15
+ preferred_format ||= :marshal
16
+ if preferred_format.to_s == 'secure'
17
+ @serializers = [ SecureSerializer ]
18
+ else
19
+ preferred_serializer = SERIALIZERS[preferred_format.to_sym]
20
+ @serializers = SERIALIZERS.values.clone
21
+ @serializers.unshift(@serializers.delete(preferred_serializer)) if preferred_serializer
22
+ end
23
+ end
24
+
25
+ def dump(packet)
26
+ cascade_serializers(:dump, packet)
27
+ end
28
+
29
+ def load(packet)
30
+ cascade_serializers(:load, packet)
31
+ end
32
+
33
+ private
34
+
35
+ SERIALIZERS = {:json => JSON, :marshal => Marshal, :yaml => YAML}.freeze
36
+
37
+ def cascade_serializers(action, packet)
38
+ errors = []
39
+ @serializers.map do |serializer|
40
+ begin
41
+ o = serializer.send(action, packet)
42
+ rescue Exception => e
43
+ o = nil
44
+ errors << "#{e.message}\n\t#{e.backtrace[0]}"
45
+ end
46
+ return o if o
47
+ end
48
+ raise SerializationError.new(action, packet, @serializers, errors.join("\n"))
49
+ end
50
+
51
+ end # Serializer
52
+ end # Nanite
@@ -0,0 +1,164 @@
1
+ require 'redis'
2
+
3
+ module Nanite
4
+ class State
5
+ include Enumerable
6
+
7
+ # this class encapsulates the state of a nanite system using redis as the
8
+ # data store. here is the schema, for each agent we store a number of items,
9
+ # for a nanite with the identity: nanite-foobar we store the following things:
10
+ #
11
+ # nanite-foobar: 0.72 # load average or 'status'
12
+ # s-nanite-foobar: { /foo/bar, /foo/nik } # a SET of the provided services
13
+ # tg-nanite-foobar: { foo-42, customer-12 } # a SET of the tags for this agent
14
+ # t-nanite-foobar: 123456789 # unix timestamp of the last state update
15
+ #
16
+ # also we do an inverted index for quick lookup of agents providing a certain
17
+ # service, so for each service the agent provides, we add the nanite to a SET
18
+ # of all the nanites that provide said service:
19
+ #
20
+ # foo/bar: { nanite-foobar, nanite-nickelbag, nanite-another } # redis SET
21
+ #
22
+ # we do that same thing for tags:
23
+ # some-tag: { nanite-foobar, nanite-nickelbag, nanite-another } # redis SET
24
+ #
25
+ # This way we can do a lookup of what nanites provide a set of services and tags based
26
+ # on redis SET intersection:
27
+ #
28
+ # nanites_for('/gems/list', 'some-tag')
29
+ # => returns a nested array of nanites and their state that provide the intersection
30
+ # of these two service tags
31
+
32
+ def initialize(redis)
33
+ Nanite::Log.info("initializing redis state: #{redis}")
34
+ host, port = redis.split(':')
35
+ host ||= '127.0.0.1'
36
+ port ||= '6379'
37
+ @redis = Redis.new :host => host, :port => port
38
+ end
39
+
40
+ def log_redis_error(meth,&blk)
41
+ blk.call
42
+ rescue RedisError => e
43
+ Nanite::Log.info("redis error in method: #{meth}")
44
+ raise e
45
+ end
46
+
47
+ def [](nanite)
48
+ log_redis_error("[]") do
49
+ status = @redis[nanite]
50
+ timestamp = @redis["t-#{nanite}"]
51
+ services = @redis.set_members("s-#{nanite}")
52
+ tags = @redis.set_members("tg-#{nanite}")
53
+ return nil unless status && timestamp && services
54
+ {:services => services, :status => status, :timestamp => timestamp.to_i, :tags => tags}
55
+ end
56
+ end
57
+
58
+ def []=(nanite, hsh)
59
+ log_redis_error("[]=") do
60
+ update_state(nanite, hsh[:status], hsh[:services], hsh[:tags])
61
+ end
62
+ end
63
+
64
+ def delete(nanite)
65
+ log_redis_error("delete") do
66
+ (@redis.set_members("s-#{nanite}")||[]).each do |srv|
67
+ @redis.set_delete(srv, nanite)
68
+ if @redis.set_count(srv) == 0
69
+ @redis.delete(srv)
70
+ @redis.set_delete("naniteservices", srv)
71
+ end
72
+ end
73
+ (@redis.set_members("tg-#{nanite}")||[]).each do |tag|
74
+ @redis.set_delete(tag, nanite)
75
+ if @redis.set_count(tag) == 0
76
+ @redis.delete(tag)
77
+ @redis.set_delete("nanitetags", tag)
78
+ end
79
+ end
80
+ @redis.delete nanite
81
+ @redis.delete "s-#{nanite}"
82
+ @redis.delete "t-#{nanite}"
83
+ @redis.delete "tg-#{nanite}"
84
+ end
85
+ end
86
+
87
+ def all_services
88
+ log_redis_error("all_services") do
89
+ @redis.set_members("naniteservices")
90
+ end
91
+ end
92
+
93
+ def all_tags
94
+ log_redis_error("all_tags") do
95
+ @redis.set_members("nanitetags")
96
+ end
97
+ end
98
+
99
+ def update_state(name, status, services, tags)
100
+ old_services = @redis.set_members("s-#{name}")
101
+ if old_services
102
+ (old_services - services).each do |s|
103
+ @redis.set_delete(s, name)
104
+ @redis.set_delete("naniteservices", s)
105
+ end
106
+ end
107
+ old_tags = @redis.set_members("tg-#{name}")
108
+ if old_tags
109
+ (old_tags - tags).each do |t|
110
+ @redis.set_delete(t, name)
111
+ @redis.set_delete("nanitetags", t)
112
+ end
113
+ end
114
+ @redis.delete("s-#{name}")
115
+ services.each do |srv|
116
+ @redis.set_add(srv, name)
117
+ @redis.set_add("s-#{name}", srv)
118
+ @redis.set_add("naniteservices", srv)
119
+ end
120
+ @redis.delete("tg-#{name}")
121
+ tags.each do |tag|
122
+ next if tag.nil?
123
+ @redis.set_add(tag, name)
124
+ @redis.set_add("tg-#{name}", tag)
125
+ @redis.set_add("nanitetags", tag)
126
+ end
127
+ @redis[name] = status
128
+ @redis["t-#{name}"] = Time.now.to_i
129
+ end
130
+
131
+ def list_nanites
132
+ log_redis_error("list_nanites") do
133
+ @redis.keys("nanite-*")
134
+ end
135
+ end
136
+
137
+ def size
138
+ list_nanites.size
139
+ end
140
+
141
+ def clear_state
142
+ log_redis_error("clear_state") do
143
+ @redis.keys("*").each {|k| @redis.delete k}
144
+ end
145
+ end
146
+
147
+ def each
148
+ list_nanites.each do |nan|
149
+ yield nan, self[nan]
150
+ end
151
+ end
152
+
153
+ def nanites_for(service, *tags)
154
+ keys = tags.dup << service
155
+ log_redis_error("nanites_for") do
156
+ res = []
157
+ (@redis.set_intersect(keys)||[]).each do |nan|
158
+ res << [nan, self[nan]]
159
+ end
160
+ res
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,125 @@
1
+ module Nanite
2
+ # Nanite actors can transfer files to each other.
3
+ #
4
+ # ==== Options
5
+ #
6
+ # filename : you guessed it, name of the file!
7
+ # domain : part of the routing key used to locate receiver(s)
8
+ # destination : is a name of the file as it gonna be stored at the destination
9
+ # meta :
10
+ #
11
+ # File streaming is done in chunks. When file streaming starts,
12
+ # Nanite::FileStart packet is sent, followed by one or more (usually more ;))
13
+ # Nanite::FileChunk packets each 16384 (16K) in size. Once file streaming is done,
14
+ # Nanite::FileEnd packet is sent.
15
+ #
16
+ # 16K is a packet size because on certain UNIX-like operating systems, you cannot read/write
17
+ # more than that in one operation via socket.
18
+ #
19
+ # ==== Domains
20
+ #
21
+ # Streaming happens using a topic exchange called 'file broadcast', with keys
22
+ # formatted as "nanite.filepeer.DOMAIN". Domain variable in the key lets senders and
23
+ # receivers find each other in the cluster. Default domain is 'global'.
24
+ #
25
+ # Domains also serve as a way to register a callback Nanite agent executes once file
26
+ # streaming is completed. If a callback with name of domain is registered, it is called.
27
+ #
28
+ # Callbacks are registered by passing a block to subscribe_to_files method.
29
+ module FileStreaming
30
+ def broadcast_file(filename, options = {})
31
+ if File.exist?(filename)
32
+ File.open(filename, 'rb') do |file|
33
+ broadcast_data(filename, file, options)
34
+ end
35
+ else
36
+ return "file not found"
37
+ end
38
+ end
39
+
40
+ def broadcast_data(filename, io, options = {})
41
+ domain = options[:domain] || 'global'
42
+ filename = File.basename(filename)
43
+ dest = options[:destination] || filename
44
+ sent = 0
45
+
46
+ begin
47
+ file_push = Nanite::FileStart.new(filename, dest, Identity.generate)
48
+ amq.topic('file broadcast').publish(serializer.dump(file_push), :key => "nanite.filepeer.#{domain}")
49
+ res = Nanite::FileChunk.new(file_push.token)
50
+ while chunk = io.read(16384)
51
+ res.chunk = chunk
52
+ amq.topic('file broadcast').publish(serializer.dump(res), :key => "nanite.filepeer.#{domain}")
53
+ sent += chunk.length
54
+ end
55
+ fend = Nanite::FileEnd.new(file_push.token, options[:meta])
56
+ amq.topic('file broadcast').publish(serializer.dump(fend), :key => "nanite.filepeer.#{domain}")
57
+ ""
58
+ ensure
59
+ io.close
60
+ end
61
+
62
+ sent
63
+ end
64
+
65
+ # FileState represents a file download in progress.
66
+ # It incapsulates the following information:
67
+ #
68
+ # * unique operation token
69
+ # * domain (namespace for file streaming operations)
70
+ # * file IO chunks are written to on receiver's side
71
+ class FileState
72
+
73
+ def initialize(token, dest, domain, write, blk)
74
+ @token = token
75
+ @cb = blk
76
+ @domain = domain
77
+ @write = write
78
+
79
+ if write
80
+ @filename = File.join(Nanite.agent.file_root, dest)
81
+ @dest = File.open(@filename, 'wb')
82
+ else
83
+ @dest = dest
84
+ end
85
+
86
+ @data = ""
87
+ end
88
+
89
+ def handle_packet(packet)
90
+ case packet
91
+ when Nanite::FileChunk
92
+ Nanite::Log.debug "written chunk to #{@dest.inspect}"
93
+ @data << packet.chunk
94
+
95
+ if @write
96
+ @dest.write(packet.chunk)
97
+ end
98
+ when Nanite::FileEnd
99
+ Nanite::Log.debug "#{@dest.inspect} receiving is completed"
100
+ if @write
101
+ @dest.close
102
+ end
103
+
104
+ @cb.call(@data, @dest, packet.meta)
105
+ end
106
+ end
107
+
108
+ end
109
+
110
+ def subscribe_to_files(domain='global', write=false, &blk)
111
+ Nanite::Log.info "subscribing to file broadcasts for #{domain}"
112
+ @files ||= {}
113
+ amq.queue("files#{domain}").bind(amq.topic('file broadcast'), :key => "nanite.filepeer.#{domain}").subscribe do |packet|
114
+ case msg = serializer.load(packet)
115
+ when FileStart
116
+ @files[msg.token] = FileState.new(msg.token, msg.dest, domain, write, blk)
117
+ when FileChunk, FileEnd
118
+ if file = @files[msg.token]
119
+ file.handle_packet(msg)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end