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,52 @@
1
+ module Nanite
2
+ class PidFile
3
+ def initialize(identity, options)
4
+ @pid_dir = File.expand_path(options[:pid_dir] || options[:root] || Dir.pwd)
5
+ @pid_file = File.join(@pid_dir, "nanite.#{identity}.pid")
6
+ end
7
+
8
+ def check
9
+ if pid = read_pid
10
+ if process_running? pid
11
+ raise "#{@pid_file} already exists (pid: #{pid})"
12
+ else
13
+ Log.info "removing stale pid file: #{@pid_file}"
14
+ remove
15
+ end
16
+ end
17
+ end
18
+
19
+ def ensure_dir
20
+ FileUtils.mkdir_p @pid_dir
21
+ end
22
+
23
+ def write
24
+ ensure_dir
25
+ open(@pid_file,'w') {|f| f.write(Process.pid) }
26
+ File.chmod(0644, @pid_file)
27
+ end
28
+
29
+ def remove
30
+ File.delete(@pid_file) if exists?
31
+ end
32
+
33
+ def read_pid
34
+ open(@pid_file,'r') {|f| f.read.to_i } if exists?
35
+ end
36
+
37
+ def exists?
38
+ File.exists? @pid_file
39
+ end
40
+
41
+ def to_s
42
+ @pid_file
43
+ end
44
+
45
+ private
46
+ def process_running?(pid)
47
+ Process.getpgid(pid) != -1
48
+ rescue Errno::ESRCH
49
+ false
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,39 @@
1
+ module Nanite
2
+ class Reaper
3
+ attr_reader :timeouts
4
+ def initialize(frequency=2)
5
+ @timeouts = {}
6
+ EM.add_periodic_timer(frequency) { EM.next_tick { reap } }
7
+ end
8
+
9
+ # Add the specified token to the internal timeout hash.
10
+ # The reaper will then check this instance on every reap.
11
+ def register(token, seconds, &blk)
12
+ @timeouts[token] = {:timestamp => Time.now + seconds, :seconds => seconds, :callback => blk}
13
+ end
14
+
15
+ def unregister(token)
16
+ @timeouts.delete(token)
17
+ end
18
+
19
+ # Updates the timeout timestamp for the given token. If the token is
20
+ # unknown to this reaper instance it will be auto-registered, usually
21
+ # happening when you have several mappers and not all of them know
22
+ # this agent yet, but received a ping from it.
23
+ def update(token, seconds, &blk)
24
+ unless @timeouts[token]
25
+ register(token, seconds, &blk)
26
+ end
27
+ @timeouts[token][:timestamp] = Time.now + @timeouts[token][:seconds]
28
+ end
29
+
30
+ private
31
+
32
+ def reap
33
+ time = Time.now
34
+ @timeouts.reject! do |token, data|
35
+ time > data[:timestamp] and data[:callback].call
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,141 @@
1
+ require 'redis'
2
+
3
+ module Nanite
4
+
5
+ # Implementation of a tag store on top of Redis
6
+ # For a nanite with the identity 'nanite-foobar', we store the following:
7
+ #
8
+ # s-nanite-foobar: { /foo/bar, /foo/nik } # a SET of the provided services
9
+ # tg-nanite-foobar: { foo-42, customer-12 } # a SET of the tags for this agent
10
+ #
11
+ # Also we do an inverted index for quick lookup of agents providing a certain
12
+ # service, so for each service the agent provides, we add the nanite to a SET
13
+ # of all the nanites that provide said service:
14
+ #
15
+ # foo/bar: { nanite-foobar, nanite-nickelbag, nanite-another } # redis SET
16
+ #
17
+ # We do that same thing for tags:
18
+ #
19
+ # some-tag: { nanite-foobar, nanite-nickelbag, nanite-another } # redis SET
20
+ #
21
+ # This way we can do a lookup of what nanites provide a set of services and tags based
22
+ # on redis SET intersection:
23
+ #
24
+ # nanites_for('/gems/list', 'some-tag')
25
+ # => returns an array of nanites that provide the intersection of these two service tags
26
+
27
+ class RedisTagStore
28
+
29
+ # Initialize tag store with given redis handle
30
+ def initialize(redis)
31
+ @redis = redis
32
+ end
33
+
34
+ # Store services and tags for given agent
35
+ def store(nanite, services, tags)
36
+ services = nil if services.compact.empty?
37
+ tags = nil if tags.compact.empty?
38
+ log_redis_error do
39
+ if services
40
+ obsolete_services = @redis.set_members("s-#{nanite}") - services
41
+ update_elems(nanite, services, obsolete_services, "s-#{nanite}", 'naniteservices')
42
+ end
43
+ if tags
44
+ obsolete_tags = @redis.set_members("tg-#{nanite}") - tags
45
+ update_elems(nanite, tags, obsolete_tags, "tg-#{nanite}", 'nanitestags')
46
+ end
47
+ end
48
+ end
49
+
50
+ # Update tags for given agent
51
+ def update(nanite, new_tags, obsolete_tags)
52
+ update_elems(nanite, new_tags, obsolete_tags, "tg-#{nanite}", 'nanitestags')
53
+ end
54
+
55
+ # Delete services and tags for given agent
56
+ def delete(nanite)
57
+ delete_elems(nanite, "s-#{nanite}", 'naniteservices')
58
+ delete_elems(nanite, "tg-#{nanite}", 'nanitestags')
59
+ end
60
+
61
+ # Services implemented by given agent
62
+ def services(nanite)
63
+ @redis.set_members("s-#{nanite}")
64
+ end
65
+
66
+ # Tags exposed by given agent
67
+ def tags(nanite)
68
+ @redis.set_members("tg-#{nanite}")
69
+ end
70
+
71
+ # Retrieve all agents services
72
+ def all_services
73
+ log_redis_error do
74
+ @redis.set_members('naniteservices')
75
+ end
76
+ end
77
+
78
+ # Retrieve all agents tags
79
+ def all_tags
80
+ log_redis_error do
81
+ @redis.set_members('nanitetags')
82
+ end
83
+ end
84
+
85
+ # Retrieve nanites implementing given service and exposing given tags
86
+ def nanites_for(from, service, tags)
87
+ keys = tags && tags.dup || []
88
+ keys << service
89
+ log_redis_error do
90
+ @redis.set_intersect(keys.compact)
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ # Update values stored for given agent
97
+ # Also store reverse lookup information using both a unique and
98
+ # a global key (so it's possible to retrieve that agent value or
99
+ # all related values)
100
+ def update_elems(nanite, new_tags, obsolete_tags, elem_key, global_key)
101
+ new_tags = nil if new_tags.compact.empty?
102
+ obsolete_tags = nil if obsolete_tags.compact.empty?
103
+ log_redis_error do
104
+ obsolete_tags.each do |val|
105
+ @redis.set_delete(val, nanite)
106
+ @redis.set_delete(elem_key, val)
107
+ @redis.set_delete(global_key, val)
108
+ end if obsolete_tags
109
+ new_tags.each do |val|
110
+ @redis.set_add(val, nanite)
111
+ @redis.set_add(elem_key, val)
112
+ @redis.set_add(global_key, val)
113
+ end if new_tags
114
+ end
115
+ end
116
+
117
+ # Delete all values for given nanite agent
118
+ # Also delete reverse lookup information
119
+ def delete_elems(nanite, elem_key, global_key)
120
+ log_redis_error do
121
+ (@redis.set_members(elem_key)||[]).each do |val|
122
+ @redis.set_delete(val, nanite)
123
+ if @redis.set_count(val) == 0
124
+ @redis.delete(val)
125
+ @redis.set_delete(global_key, val)
126
+ end
127
+ end
128
+ @redis.delete(elem_key)
129
+ end
130
+ end
131
+
132
+ # Helper method, catch and log errors
133
+ def log_redis_error(&blk)
134
+ blk.call
135
+ rescue Exception => e
136
+ Nanite::Log.warn("redis error in method: #{caller[0]}")
137
+ raise e
138
+ end
139
+
140
+ end
141
+ end
@@ -0,0 +1,24 @@
1
+ module Nanite
2
+
3
+ # Proxy to actual certificate store which caches results in an LRU
4
+ # cache.
5
+ class CachedCertificateStoreProxy
6
+
7
+ # Initialize cache proxy with given certificate store.
8
+ def initialize(store)
9
+ @signer_cache = CertificateCache.new
10
+ @store = store
11
+ end
12
+
13
+ # Results from 'get_recipients' are not cached
14
+ def get_recipients(obj)
15
+ @store.get_recipients(obj)
16
+ end
17
+
18
+ # Check cache for signer certificate
19
+ def get_signer(id)
20
+ @signer_cache.get(id) { @store.get_signer(id) }
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,55 @@
1
+ module Nanite
2
+
3
+ # X.509 Certificate management
4
+ class Certificate
5
+
6
+ # Underlying OpenSSL cert
7
+ attr_accessor :raw_cert
8
+
9
+ # Generate a signed X.509 certificate
10
+ #
11
+ # Arguments:
12
+ # - key: RsaKeyPair, key pair used to sign certificate
13
+ # - issuer: DistinguishedName, certificate issuer
14
+ # - subject: DistinguishedName, certificate subject
15
+ # - valid_for: Time in seconds before certificate expires (10 years by default)
16
+ def initialize(key, issuer, subject, valid_for = 3600*24*365*10)
17
+ @raw_cert = OpenSSL::X509::Certificate.new
18
+ @raw_cert.version = 2
19
+ @raw_cert.serial = 1
20
+ @raw_cert.subject = subject.to_x509
21
+ @raw_cert.issuer = issuer.to_x509
22
+ @raw_cert.public_key = key.to_public.raw_key
23
+ @raw_cert.not_before = Time.now
24
+ @raw_cert.not_after = Time.now + valid_for
25
+ @raw_cert.sign(key.raw_key, OpenSSL::Digest::SHA1.new)
26
+ end
27
+
28
+ # Load certificate from file
29
+ def self.load(file)
30
+ from_data(File.new(file))
31
+ end
32
+
33
+ # Initialize with raw certificate
34
+ def self.from_data(data)
35
+ cert = OpenSSL::X509::Certificate.new(data)
36
+ res = Certificate.allocate
37
+ res.instance_variable_set(:@raw_cert, cert)
38
+ res
39
+ end
40
+
41
+ # Save certificate to file in PEM format
42
+ def save(file)
43
+ File.open(file, "w") do |f|
44
+ f.write(@raw_cert.to_pem)
45
+ end
46
+ end
47
+
48
+ # Certificate data in PEM format
49
+ def data
50
+ @raw_cert.to_pem
51
+ end
52
+ alias :to_s :data
53
+
54
+ end
55
+ end
@@ -0,0 +1,66 @@
1
+ module Nanite
2
+
3
+ # Implements a simple LRU cache: items that are the least accessed are
4
+ # deleted first.
5
+ class CertificateCache
6
+
7
+ # Max number of items to keep in memory
8
+ DEFAULT_CACHE_MAX_COUNT = 100
9
+
10
+ # Initialize cache
11
+ def initialize(max_count = DEFAULT_CACHE_MAX_COUNT)
12
+ @items = {}
13
+ @list = []
14
+ @max_count = max_count
15
+ end
16
+
17
+ # Add item to cache
18
+ def put(key, item)
19
+ if @items.include?(key)
20
+ delete(key)
21
+ end
22
+ if @list.size == @max_count
23
+ delete(@list.first)
24
+ end
25
+ @items[key] = item
26
+ @list.push(key)
27
+ item
28
+ end
29
+ alias :[]= :put
30
+
31
+ # Retrieve item from cache
32
+ # Store item returned by given block if any
33
+ def get(key)
34
+ if @items.include?(key)
35
+ @list.each_index do |i|
36
+ if @list[i] == key
37
+ @list.delete_at(i)
38
+ break
39
+ end
40
+ end
41
+ @list.push(key)
42
+ @items[key]
43
+ else
44
+ return nil unless block_given?
45
+ self[key] = yield
46
+ end
47
+ end
48
+ alias :[] :get
49
+
50
+ # Delete item from cache
51
+ def delete(key)
52
+ c = @items[key]
53
+ if c
54
+ @items.delete(key)
55
+ @list.each_index do |i|
56
+ if @list[i] == key
57
+ @list.delete_at(i)
58
+ break
59
+ end
60
+ end
61
+ c
62
+ end
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,34 @@
1
+ module Nanite
2
+
3
+ # Build X.509 compliant distinguished names
4
+ # Distinghuished names are used to desccribe both a certificate issuer and
5
+ # subject.
6
+ class DistinguishedName
7
+
8
+ # Initialize distinguished name from hash
9
+ # e.g.:
10
+ # { 'C' => 'US',
11
+ # 'ST' => 'California',
12
+ # 'L' => 'Santa Barbara',
13
+ # 'O' => 'RightScale',
14
+ # 'OU' => 'Certification Services',
15
+ # 'CN' => 'rightscale.com/emailAddress=cert@rightscale.com' }
16
+ #
17
+ def initialize(hash)
18
+ @value = hash
19
+ end
20
+
21
+ # Conversion to OpenSSL X509 DN
22
+ def to_x509
23
+ if @value
24
+ OpenSSL::X509::Name.new(@value.to_a, OpenSSL::X509::Name::OBJECT_TYPE_TEMPLATE)
25
+ end
26
+ end
27
+
28
+ # Human readable form
29
+ def to_s
30
+ '/' + @value.to_a.collect { |p| p.join('=') }.join('/') if @value
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,46 @@
1
+ module Nanite
2
+
3
+ # Represents a signed an encrypted document that can be later decrypted using
4
+ # the right private key and whose signature can be verified using the right
5
+ # cert.
6
+ # This class can be used both to encrypt and sign data and to then check the
7
+ # signature and decrypt an encrypted document.
8
+ class EncryptedDocument
9
+
10
+ # Encrypt and sign data using certificate and key pair.
11
+ #
12
+ # Arguments:
13
+ # - 'data': Data to be encrypted
14
+ # - 'certs': Recipient certificates (certificates corresponding to private
15
+ # keys that may be used to decrypt data)
16
+ # - 'cipher': Cipher used for encryption, AES 256 CBC by default
17
+ #
18
+ def initialize(data, certs, cipher = 'AES-256-CBC')
19
+ cipher = OpenSSL::Cipher::Cipher.new(cipher)
20
+ certs = [ certs ] unless certs.respond_to?(:collect)
21
+ raw_certs = certs.collect { |c| c.raw_cert }
22
+ @pkcs7 = OpenSSL::PKCS7.encrypt(raw_certs, data, cipher, OpenSSL::PKCS7::BINARY)
23
+ end
24
+
25
+ # Initialize from encrypted data.
26
+ def self.from_data(encrypted_data)
27
+ doc = EncryptedDocument.allocate
28
+ doc.instance_variable_set(:@pkcs7, Nanite::PKCS7.new(encrypted_data))
29
+ doc
30
+ end
31
+
32
+ # Encrypted data using DER format
33
+ def encrypted_data
34
+ @pkcs7.to_pem
35
+ end
36
+
37
+ # Decrypted data
38
+ #
39
+ # Arguments:
40
+ # - 'key': Key used for decryption
41
+ # - 'cert': Certificate to use for decryption
42
+ def decrypted_data(key, cert)
43
+ @pkcs7.decrypt(key.raw_key, cert.raw_cert)
44
+ end
45
+ end
46
+ end