shift-nanite 0.4.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/LICENSE +201 -0
  2. data/README.rdoc +430 -0
  3. data/Rakefile +76 -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 +264 -0
  13. data/lib/nanite/amqp.rb +58 -0
  14. data/lib/nanite/cluster.rb +250 -0
  15. data/lib/nanite/config.rb +112 -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 +309 -0
  24. data/lib/nanite/mapper_proxy.rb +67 -0
  25. data/lib/nanite/nanite_dispatcher.rb +92 -0
  26. data/lib/nanite/packets.rb +365 -0
  27. data/lib/nanite/pid_file.rb +52 -0
  28. data/lib/nanite/reaper.rb +39 -0
  29. data/lib/nanite/security/cached_certificate_store_proxy.rb +24 -0
  30. data/lib/nanite/security/certificate.rb +55 -0
  31. data/lib/nanite/security/certificate_cache.rb +66 -0
  32. data/lib/nanite/security/distinguished_name.rb +34 -0
  33. data/lib/nanite/security/encrypted_document.rb +46 -0
  34. data/lib/nanite/security/rsa_key_pair.rb +53 -0
  35. data/lib/nanite/security/secure_serializer.rb +68 -0
  36. data/lib/nanite/security/signature.rb +46 -0
  37. data/lib/nanite/security/static_certificate_store.rb +35 -0
  38. data/lib/nanite/security_provider.rb +47 -0
  39. data/lib/nanite/serializer.rb +52 -0
  40. data/lib/nanite/state.rb +168 -0
  41. data/lib/nanite/streaming.rb +125 -0
  42. data/lib/nanite/util.rb +58 -0
  43. data/spec/actor_registry_spec.rb +60 -0
  44. data/spec/actor_spec.rb +77 -0
  45. data/spec/agent_spec.rb +240 -0
  46. data/spec/cached_certificate_store_proxy_spec.rb +34 -0
  47. data/spec/certificate_cache_spec.rb +49 -0
  48. data/spec/certificate_spec.rb +27 -0
  49. data/spec/cluster_spec.rb +622 -0
  50. data/spec/distinguished_name_spec.rb +24 -0
  51. data/spec/encrypted_document_spec.rb +21 -0
  52. data/spec/job_spec.rb +251 -0
  53. data/spec/local_state_spec.rb +130 -0
  54. data/spec/nanite_dispatcher_spec.rb +136 -0
  55. data/spec/packet_spec.rb +220 -0
  56. data/spec/rsa_key_pair_spec.rb +33 -0
  57. data/spec/secure_serializer_spec.rb +41 -0
  58. data/spec/serializer_spec.rb +107 -0
  59. data/spec/signature_spec.rb +30 -0
  60. data/spec/spec_helper.rb +33 -0
  61. data/spec/static_certificate_store_spec.rb +30 -0
  62. data/spec/util_spec.rb +63 -0
  63. metadata +129 -0
@@ -0,0 +1,46 @@
1
+ if defined?(OpenSSL::PKCS7::PKCS7)
2
+ Nanite::PKCS7 = OpenSSL::PKCS7::PKCS7
3
+ else
4
+ Nanite::PKCS7 = OpenSSL::PKCS7
5
+ end
6
+
7
+ module Nanite
8
+
9
+ # Signature that can be validated against certificates
10
+ class Signature
11
+
12
+ FLAGS = OpenSSL::PKCS7::NOCERTS || OpenSSL::PKCS7::BINARY || OpenSSL::PKCS7::NOATTR || OpenSSL::PKCS7::NOSMIMECAP || OpenSSL::PKCS7::DETACH
13
+
14
+ # Create signature using certificate and key pair.
15
+ #
16
+ # Arguments:
17
+ # - 'data': Data to be signed
18
+ # - 'cert': Certificate used for signature
19
+ # - 'key': RsaKeyPair used for signature
20
+ #
21
+ def initialize(data, cert, key)
22
+ @p7 = OpenSSL::PKCS7.sign(cert.raw_cert, key.raw_key, data, [], FLAGS)
23
+ @store = OpenSSL::X509::Store.new
24
+ end
25
+
26
+ # Load signature previously serialized via 'data'
27
+ def self.from_data(data)
28
+ sig = Signature.allocate
29
+ sig.instance_variable_set(:@p7, Nanite::PKCS7.new(data))
30
+ sig.instance_variable_set(:@store, OpenSSL::X509::Store.new)
31
+ sig
32
+ end
33
+
34
+ # 'true' if signature was created using given cert, 'false' otherwise
35
+ def match?(cert)
36
+ @p7.verify([cert.raw_cert], @store, nil, OpenSSL::PKCS7::NOVERIFY)
37
+ end
38
+
39
+ # Signature in PEM format
40
+ def data
41
+ @p7.to_pem
42
+ end
43
+ alias :to_s :data
44
+
45
+ end
46
+ 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,168 @@
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("[setup] 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 Exception => 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, attributes)
59
+ log_redis_error("[]=") do
60
+ update_state(nanite, attributes[:status], attributes[:services], attributes[: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
+ update_status(name, status)
128
+ end
129
+
130
+ def update_status(name, status)
131
+ @redis[name] = status
132
+ @redis["t-#{name}"] = Time.now.utc.to_i
133
+ end
134
+
135
+ def list_nanites
136
+ log_redis_error("list_nanites") do
137
+ @redis.keys("nanite-*")
138
+ end
139
+ end
140
+
141
+ def size
142
+ list_nanites.size
143
+ end
144
+
145
+ def clear_state
146
+ log_redis_error("clear_state") do
147
+ @redis.keys("*").each {|k| @redis.delete k}
148
+ end
149
+ end
150
+
151
+ def each
152
+ list_nanites.each do |nan|
153
+ yield nan, self[nan]
154
+ end
155
+ end
156
+
157
+ def nanites_for(service, *tags)
158
+ keys = tags.dup << service
159
+ log_redis_error("nanites_for") do
160
+ res = []
161
+ (@redis.set_intersect(keys)||[]).each do |nan|
162
+ res << [nan, self[nan]]
163
+ end
164
+ res
165
+ end
166
+ end
167
+ end
168
+ 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