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.
- data/LICENSE +201 -0
- data/README.rdoc +430 -0
- data/Rakefile +78 -0
- data/TODO +24 -0
- data/bin/nanite-admin +65 -0
- data/bin/nanite-agent +79 -0
- data/bin/nanite-mapper +50 -0
- data/lib/nanite.rb +74 -0
- data/lib/nanite/actor.rb +71 -0
- data/lib/nanite/actor_registry.rb +26 -0
- data/lib/nanite/admin.rb +138 -0
- data/lib/nanite/agent.rb +274 -0
- data/lib/nanite/amqp.rb +58 -0
- data/lib/nanite/cluster.rb +256 -0
- data/lib/nanite/config.rb +111 -0
- data/lib/nanite/console.rb +39 -0
- data/lib/nanite/daemonize.rb +13 -0
- data/lib/nanite/identity.rb +16 -0
- data/lib/nanite/job.rb +104 -0
- data/lib/nanite/local_state.rb +38 -0
- data/lib/nanite/log.rb +66 -0
- data/lib/nanite/log/formatter.rb +39 -0
- data/lib/nanite/mapper.rb +315 -0
- data/lib/nanite/mapper_proxy.rb +75 -0
- data/lib/nanite/nanite_dispatcher.rb +92 -0
- data/lib/nanite/packets.rb +401 -0
- data/lib/nanite/pid_file.rb +52 -0
- data/lib/nanite/reaper.rb +39 -0
- data/lib/nanite/redis_tag_store.rb +141 -0
- data/lib/nanite/security/cached_certificate_store_proxy.rb +24 -0
- data/lib/nanite/security/certificate.rb +55 -0
- data/lib/nanite/security/certificate_cache.rb +66 -0
- data/lib/nanite/security/distinguished_name.rb +34 -0
- data/lib/nanite/security/encrypted_document.rb +46 -0
- data/lib/nanite/security/rsa_key_pair.rb +53 -0
- data/lib/nanite/security/secure_serializer.rb +68 -0
- data/lib/nanite/security/signature.rb +46 -0
- data/lib/nanite/security/static_certificate_store.rb +35 -0
- data/lib/nanite/security_provider.rb +47 -0
- data/lib/nanite/serializer.rb +52 -0
- data/lib/nanite/state.rb +135 -0
- data/lib/nanite/streaming.rb +125 -0
- data/lib/nanite/util.rb +78 -0
- metadata +111 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
module Nanite
|
2
|
+
|
3
|
+
# Allows generating RSA key pairs and extracting public key component
|
4
|
+
# Note: Creating a RSA key pair can take a fair amount of time (seconds)
|
5
|
+
class RsaKeyPair
|
6
|
+
|
7
|
+
DEFAULT_LENGTH = 2048
|
8
|
+
|
9
|
+
# Underlying OpenSSL keys
|
10
|
+
attr_reader :raw_key
|
11
|
+
|
12
|
+
# Create new RSA key pair using 'length' bits
|
13
|
+
def initialize(length = DEFAULT_LENGTH)
|
14
|
+
@raw_key = OpenSSL::PKey::RSA.generate(length)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Does key pair include private key?
|
18
|
+
def has_private?
|
19
|
+
raw_key.private?
|
20
|
+
end
|
21
|
+
|
22
|
+
# New RsaKeyPair instance with identical public key but no private key
|
23
|
+
def to_public
|
24
|
+
RsaKeyPair.from_data(raw_key.public_key.to_pem)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Key material in PEM format
|
28
|
+
def data
|
29
|
+
raw_key.to_pem
|
30
|
+
end
|
31
|
+
alias :to_s :data
|
32
|
+
|
33
|
+
# Load key pair previously serialized via 'data'
|
34
|
+
def self.from_data(data)
|
35
|
+
res = RsaKeyPair.allocate
|
36
|
+
res.instance_variable_set(:@raw_key, OpenSSL::PKey::RSA.new(data))
|
37
|
+
res
|
38
|
+
end
|
39
|
+
|
40
|
+
# Load key from file
|
41
|
+
def self.load(file)
|
42
|
+
from_data(File.read(file))
|
43
|
+
end
|
44
|
+
|
45
|
+
# Save key to file in PEM format
|
46
|
+
def save(file)
|
47
|
+
File.open(file, "w") do |f|
|
48
|
+
f.write(@raw_key.to_pem)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Nanite
|
2
|
+
|
3
|
+
# Serializer implementation which secures messages by using
|
4
|
+
# X.509 certificate sigining.
|
5
|
+
class SecureSerializer
|
6
|
+
|
7
|
+
# Initialize serializer, must be called prior to using it.
|
8
|
+
#
|
9
|
+
# - 'identity': Identity associated with serialized messages
|
10
|
+
# - 'cert': Certificate used to sign and decrypt serialized messages
|
11
|
+
# - 'key': Private key corresponding to 'cert'
|
12
|
+
# - 'store': Certificate store. Exposes certificates used for
|
13
|
+
# encryption and signature validation.
|
14
|
+
# - 'encrypt': Whether data should be signed and encrypted ('true')
|
15
|
+
# or just signed ('false'), 'true' by default.
|
16
|
+
#
|
17
|
+
def self.init(identity, cert, key, store, encrypt = true)
|
18
|
+
@identity = identity
|
19
|
+
@cert = cert
|
20
|
+
@key = key
|
21
|
+
@store = store
|
22
|
+
@encrypt = encrypt
|
23
|
+
end
|
24
|
+
|
25
|
+
# Was serializer initialized?
|
26
|
+
def self.initialized?
|
27
|
+
@identity && @cert && @key && @store
|
28
|
+
end
|
29
|
+
|
30
|
+
# Serialize message and sign it using X.509 certificate
|
31
|
+
def self.dump(obj)
|
32
|
+
raise "Missing certificate identity" unless @identity
|
33
|
+
raise "Missing certificate" unless @cert
|
34
|
+
raise "Missing certificate key" unless @key
|
35
|
+
raise "Missing certificate store" unless @store || !@encrypt
|
36
|
+
json = obj.to_json
|
37
|
+
if @encrypt
|
38
|
+
certs = @store.get_recipients(obj)
|
39
|
+
json = EncryptedDocument.new(json, certs).encrypted_data if certs
|
40
|
+
end
|
41
|
+
sig = Signature.new(json, @cert, @key)
|
42
|
+
{ 'id' => @identity, 'data' => json, 'signature' => sig.data, 'encrypted' => !certs.nil? }.to_json
|
43
|
+
end
|
44
|
+
|
45
|
+
# Unserialize data using certificate store
|
46
|
+
def self.load(json)
|
47
|
+
begin
|
48
|
+
raise "Missing certificate store" unless @store
|
49
|
+
raise "Missing certificate" unless @cert || !@encrypt
|
50
|
+
raise "Missing certificate key" unless @key || !@encrypt
|
51
|
+
data = JSON.load(json)
|
52
|
+
sig = Signature.from_data(data['signature'])
|
53
|
+
certs = @store.get_signer(data['id'])
|
54
|
+
raise "Could not find a cert for signer #{data['id']}" unless certs
|
55
|
+
certs = [ certs ] unless certs.respond_to?(:each)
|
56
|
+
jsn = data['data'] if certs.any? { |c| sig.match?(c) }
|
57
|
+
if jsn && @encrypt && data['encrypted']
|
58
|
+
jsn = EncryptedDocument.from_data(jsn).decrypted_data(@key, @cert)
|
59
|
+
end
|
60
|
+
JSON.load(jsn) if jsn
|
61
|
+
rescue Exception => e
|
62
|
+
Nanite::Log.error("Loading of secure packet failed: #{e.message}\n#{e.backtrace.join("\n")}")
|
63
|
+
raise
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -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
|
data/lib/nanite/state.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'redis_tag_store'
|
3
|
+
|
4
|
+
module Nanite
|
5
|
+
class State
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# This class encapsulates the state of a nanite system using redis as the
|
9
|
+
# data store and a provided tag store. For a nanite with the identity
|
10
|
+
# 'nanite-foobar' we store the following:
|
11
|
+
#
|
12
|
+
# nanite-foobar: 0.72 # load average or 'status'
|
13
|
+
# t-nanite-foobar: 123456789 # unix timestamp of the last state update
|
14
|
+
#
|
15
|
+
# The tag store is used to store the associated services and tags.
|
16
|
+
#
|
17
|
+
# A tag store should provide the following methods:
|
18
|
+
# - initialize(redis): Initialize tag store, may use provided redis handle
|
19
|
+
# - services(nanite): Retrieve services implemented by given agent
|
20
|
+
# - tags(nanite): Retrieve tags implemented by given agent
|
21
|
+
# - all_services: Retrieve all services implemented by all agents
|
22
|
+
# - all_tags: Retrieve all tags exposed by all agents
|
23
|
+
# - store(nanite, services, tags): Store agent's services and tags
|
24
|
+
# - update(name, new_tags,obsolete_tags): Update agent's tags
|
25
|
+
# - delete(nanite): Delete all entries associated with given agent
|
26
|
+
# - nanites_for(service, tags): Retrieve agents implementing given service
|
27
|
+
# and exposing given tags
|
28
|
+
#
|
29
|
+
# The default implementation for the tag store reuses Redis.
|
30
|
+
|
31
|
+
def initialize(redis, tag_store=nil)
|
32
|
+
host, port, tag_store_type = redis.split(':')
|
33
|
+
host ||= '127.0.0.1'
|
34
|
+
port ||= '6379'
|
35
|
+
tag_store||= 'Nanite::RedisTagStore'
|
36
|
+
@redis = Redis.new(:host => host, :port => port)
|
37
|
+
@tag_store = tag_store.to_const.new(@redis)
|
38
|
+
Nanite::Log.info("[setup] Initializing redis state using host '#{host}', port '#{port}' and tag store #{tag_store}")
|
39
|
+
end
|
40
|
+
|
41
|
+
# Retrieve given agent services, tags, status and timestamp
|
42
|
+
def [](nanite)
|
43
|
+
log_redis_error do
|
44
|
+
status = @redis[nanite]
|
45
|
+
timestamp = @redis["t-#{nanite}"]
|
46
|
+
services = @tag_store.services(nanite)
|
47
|
+
tags = @tag_store.tags(nanite)
|
48
|
+
return nil unless status && timestamp && services
|
49
|
+
{:services => services, :status => status, :timestamp => timestamp.to_i, :tags => tags}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set given attributes for given agent
|
54
|
+
# Attributes may include services, tags and status
|
55
|
+
def []=(nanite, attributes)
|
56
|
+
@tag_store.store(nanite, attributes[:services], attributes[:tags])
|
57
|
+
update_status(nanite, attributes[:status])
|
58
|
+
end
|
59
|
+
|
60
|
+
# Delete all information related to given agent
|
61
|
+
def delete(nanite)
|
62
|
+
@tag_store.delete(nanite)
|
63
|
+
log_redis_error do
|
64
|
+
@redis.delete(nanite)
|
65
|
+
@redis.delete("t-#{nanite}")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Return all services exposed by all agents
|
70
|
+
def all_services
|
71
|
+
@tag_store.all_services
|
72
|
+
end
|
73
|
+
|
74
|
+
# Return all tags exposed by all agents
|
75
|
+
def all_tags
|
76
|
+
@tag_store.all_tags
|
77
|
+
end
|
78
|
+
|
79
|
+
# Update status and timestamp for given agent
|
80
|
+
def update_status(name, status)
|
81
|
+
log_redis_error do
|
82
|
+
@redis[name] = status
|
83
|
+
@redis["t-#{name}"] = Time.now.utc.to_i
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Update tags for given agent
|
88
|
+
def update_tags(name, new_tags, obsolete_tags)
|
89
|
+
@tag_store.update(name, new_tags, obsolete_tags)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Return all registered agents
|
93
|
+
def list_nanites
|
94
|
+
log_redis_error do
|
95
|
+
@redis.keys("nanite-*")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Number of registered agents
|
100
|
+
def size
|
101
|
+
list_nanites.size
|
102
|
+
end
|
103
|
+
|
104
|
+
# Iterate through all agents, yielding services, tags
|
105
|
+
# status and timestamp keyed by agent name
|
106
|
+
def each
|
107
|
+
list_nanites.each do |nan|
|
108
|
+
yield nan, self[nan]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Return agents that implement given service and expose
|
113
|
+
# all given tags
|
114
|
+
def nanites_for(from, service, tags)
|
115
|
+
res = []
|
116
|
+
@tag_store.nanites_for(from, service, tags).each do |nanite_id|
|
117
|
+
if nanite = self[nanite_id]
|
118
|
+
res << [nanite_id, nanite]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
res
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
# Helper method, catch and log errors
|
127
|
+
def log_redis_error(&blk)
|
128
|
+
blk.call
|
129
|
+
rescue Exception => e
|
130
|
+
Nanite::Log.warn("redis error in method: #{caller[0]}")
|
131
|
+
raise e
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
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
|