omf_common 6.0.0 → 6.0.2.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/Gemfile +4 -0
  2. data/bin/file_broadcaster.rb +56 -0
  3. data/bin/file_receiver.rb +62 -0
  4. data/bin/omf_keygen +21 -0
  5. data/bin/{monitor_topic.rb → omf_monitor_topic} +21 -8
  6. data/bin/omf_send_create +118 -0
  7. data/bin/{send_request.rb → omf_send_request} +12 -7
  8. data/example/engine_alt.rb +23 -24
  9. data/example/ls_app.yaml +21 -0
  10. data/lib/omf_common.rb +73 -12
  11. data/lib/omf_common/auth.rb +15 -0
  12. data/lib/omf_common/auth/certificate.rb +174 -0
  13. data/lib/omf_common/auth/certificate_store.rb +72 -0
  14. data/lib/omf_common/auth/ssh_pub_key_convert.rb +80 -0
  15. data/lib/omf_common/comm.rb +66 -9
  16. data/lib/omf_common/comm/amqp/amqp_communicator.rb +40 -13
  17. data/lib/omf_common/comm/amqp/amqp_file_transfer.rb +259 -0
  18. data/lib/omf_common/comm/amqp/amqp_topic.rb +14 -21
  19. data/lib/omf_common/comm/local/local_communicator.rb +31 -2
  20. data/lib/omf_common/comm/local/local_topic.rb +19 -3
  21. data/lib/omf_common/comm/topic.rb +48 -34
  22. data/lib/omf_common/comm/xmpp/communicator.rb +19 -10
  23. data/lib/omf_common/comm/xmpp/topic.rb +22 -81
  24. data/lib/omf_common/default_logging.rb +11 -0
  25. data/lib/omf_common/eventloop.rb +14 -0
  26. data/lib/omf_common/eventloop/em.rb +39 -6
  27. data/lib/omf_common/eventloop/local_evl.rb +15 -0
  28. data/lib/omf_common/exec_app.rb +29 -15
  29. data/lib/omf_common/message.rb +53 -5
  30. data/lib/omf_common/message/json/json_message.rb +149 -39
  31. data/lib/omf_common/message/xml/message.rb +112 -39
  32. data/lib/omf_common/protocol/6.0.rnc +5 -1
  33. data/lib/omf_common/protocol/6.0.rng +12 -0
  34. data/lib/omf_common/version.rb +1 -1
  35. data/omf_common.gemspec +7 -2
  36. data/test/fixture/omf_test.cert.pem +15 -0
  37. data/test/fixture/omf_test.pem +15 -0
  38. data/test/fixture/omf_test.pub +1 -0
  39. data/test/fixture/omf_test.pub.pem +6 -0
  40. data/test/omf_common/auth/certificate_spec.rb +113 -0
  41. data/test/omf_common/auth/ssh_pub_key_convert_spec.rb +13 -0
  42. data/test/omf_common/comm/topic_spec.rb +175 -0
  43. data/test/omf_common/comm/xmpp/communicator_spec.rb +15 -16
  44. data/test/omf_common/comm/xmpp/topic_spec.rb +63 -10
  45. data/test/omf_common/comm_spec.rb +66 -9
  46. data/test/omf_common/message/xml/message_spec.rb +43 -13
  47. data/test/omf_common/message_spec.rb +14 -0
  48. data/test/test_helper.rb +25 -0
  49. metadata +78 -15
  50. data/bin/send_create.rb +0 -94
@@ -49,7 +49,7 @@ module OmfCommon
49
49
  provider = @@providers[type]
50
50
  end
51
51
  unless provider
52
- raise "Missing Comm provider declaration. Either define 'type', 'provider', or 'url'"
52
+ raise ArgumentError, "Missing Comm provider declaration. Either define 'type', 'provider', or 'url'"
53
53
  end
54
54
 
55
55
  require provider[:require] if provider[:require]
@@ -58,10 +58,18 @@ module OmfCommon
58
58
  provider_class = class_name.split('::').inject(Object) {|c,n| c.const_get(n) }
59
59
  inst = provider_class.new(opts)
60
60
  else
61
- raise "Missing communicator creation info - :constructor"
61
+ raise ArgumentError, "Missing communicator creation info - :constructor"
62
62
  end
63
63
  @@instance = inst
64
- Message.init(provider[:message_provider])
64
+ mopts = provider[:message_provider]
65
+ mopts[:authenticate] = (opts[:auth] != nil)
66
+ Message.init(mopts)
67
+
68
+ if aopts = opts[:auth]
69
+ require 'omf_common/auth'
70
+ OmfCommon::Auth.init(aopts)
71
+ end
72
+
65
73
  inst.init(opts)
66
74
  end
67
75
 
@@ -72,30 +80,51 @@ module OmfCommon
72
80
  # Initialize comms layer
73
81
  #
74
82
  def init(opts = {})
75
- raise "Not implemented"
83
+ end
84
+
85
+ # Return the address used for all 'generic' messages
86
+ # not specifically being sent from a resource
87
+ #
88
+ def local_address()
89
+ @local_topic.address
90
+ end
91
+
92
+ def local_topic()
93
+ @local_topic
76
94
  end
77
95
 
78
96
  # Shut down comms layer
79
97
  def disconnect(opts = {})
80
- raise "Not implemented"
98
+ raise NotImplementedError
81
99
  end
82
100
 
83
101
  def on_connected(&block)
84
- raise "Not implemented"
102
+ raise NotImplementedError
103
+ end
104
+
105
+ # TODO should expand this to on_signal(:INT)
106
+ def on_interrupted(*args, &block)
85
107
  end
86
108
 
87
109
  # Create a new pubsub topic with additional configuration
88
110
  #
89
111
  # @param [String] topic Pubsub topic name
90
112
  def create_topic(topic, opts = {})
91
- raise "Not implemented"
113
+ raise NotImplementedError
92
114
  end
93
115
 
94
116
  # Delete a pubsub topic
95
117
  #
96
118
  # @param [String] topic Pubsub topic name
97
119
  def delete_topic(topic, &block)
98
- raise "Not implemented"
120
+ raise NotImplementedError
121
+ end
122
+
123
+ # Returning connection information
124
+ #
125
+ # @retun [Hash] connection information hash, with type, user and domain.
126
+ def conn_info
127
+ { proto: nil, user: nil, domain: nil }
99
128
  end
100
129
 
101
130
  # Subscribe to a pubsub topic
@@ -109,13 +138,29 @@ module OmfCommon
109
138
  ta = tna.collect do |tn|
110
139
  t = create_topic(tn)
111
140
  if block
112
- block.call(t)
141
+ t.on_subscribed do
142
+ block.call(t)
143
+ end
113
144
  end
114
145
  t
115
146
  end
116
147
  ta[0]
117
148
  end
118
149
 
150
+ # Publish a message on a topic
151
+ #
152
+ # @param [String, Array] topic_name Pubsub topic name
153
+ # @param [OmfCoomon::Message] message
154
+ #
155
+ def publish(topic_name, message)
156
+ #puts "PUBLISH>>>>> #{topic_name}::#{message}"
157
+ tna = (topic_name.is_a? Array) ? topic_name : [topic_name]
158
+ ta = tna.collect do |tn|
159
+ t = create_topic(tn)
160
+ t.publish(message)
161
+ end
162
+ end
163
+
119
164
  # Return the options used to initiate this
120
165
  # communicator.
121
166
  #
@@ -126,6 +171,18 @@ module OmfCommon
126
171
  private
127
172
  def initialize(opts = {})
128
173
  @opts = opts
174
+ unless local_address = opts[:local_address]
175
+ hostname = nil
176
+ begin
177
+ hostname = Socket.gethostbyname(Socket.gethostname)[0]
178
+ rescue
179
+ hostname = (`hostname` || 'unknown').strip
180
+ end
181
+ local_address = "#{hostname}-#{Process.pid}"
182
+ end
183
+ on_connected do
184
+ @local_topic = create_topic(local_address.gsub('.', '-'))
185
+ end
129
186
  end
130
187
 
131
188
  end
@@ -6,7 +6,7 @@ module OmfCommon
6
6
  class Comm
7
7
  class AMQP
8
8
  class Communicator < OmfCommon::Comm
9
-
9
+
10
10
  # def initialize(opts = {})
11
11
  # # ignore arguments
12
12
  # end
@@ -20,25 +20,30 @@ module OmfCommon
20
20
  @address_prefix = @url + '/'
21
21
  ::AMQP.connect(@url) do |connection|
22
22
  @channel = ::AMQP::Channel.new(connection)
23
-
24
- if @on_connected_proc
25
- @on_connected_proc.arity == 1 ? @on_connected_proc.call(self) : @on_connected_proc.call
23
+ @on_connected_procs.each do |proc|
24
+ proc.arity == 1 ? proc.call(self) : proc.call
26
25
  end
27
-
26
+
28
27
  OmfCommon.eventloop.on_stop do
29
28
  connection.close
30
29
  end
31
30
  end
31
+ super
32
+ end
33
+
34
+ def conn_info
35
+ { proto: :amqp, user: ::AMQP.settings[:user], domain: ::AMQP.settings[:host] }
32
36
  end
33
-
37
+
34
38
  # Shut down comms layer
35
39
  def disconnect(opts = {})
36
40
  end
37
-
41
+
42
+ # TODO: Should be thread safe and check if already connected
38
43
  def on_connected(&block)
39
- @on_connected_proc = block
44
+ @on_connected_procs << block
40
45
  end
41
-
46
+
42
47
  # Create a new pubsub topic with additional configuration
43
48
  #
44
49
  # @param [String] topic Pubsub topic name
@@ -46,9 +51,10 @@ module OmfCommon
46
51
  raise "Topic can't be nil or empty" if topic.nil? || topic.empty?
47
52
  opts = opts.dup
48
53
  opts[:channel] = @channel
54
+ topic = topic.to_s
49
55
  if topic.start_with? 'amqp:'
50
56
  # absolute address
51
- unless topic.start_with? @address_prefix
57
+ unless topic.start_with? @address_prefix
52
58
  raise "Cannot subscribe to a topic from different domain (#{topic})"
53
59
  end
54
60
  opts[:address] = topic
@@ -58,7 +64,7 @@ module OmfCommon
58
64
  end
59
65
  OmfCommon::Comm::AMQP::Topic.create(topic, opts)
60
66
  end
61
-
67
+
62
68
  # Delete a pubsub topic
63
69
  #
64
70
  # @param [String] topic Pubsub topic name
@@ -67,9 +73,30 @@ module OmfCommon
67
73
  t.release
68
74
  else
69
75
  warn "Attempt to delete unknown topic '#{topic}"
70
- end
76
+ end
77
+ end
78
+
79
+ def broadcast_file(file_path, topic_name = nil, opts = {}, &block)
80
+ topic_name ||= SecureRandom.uuid
81
+ require 'omf_common/comm/amqp/amqp_file_transfer'
82
+ OmfCommon::Comm::AMQP::FileBroadcaster.new(file_path, @channel, topic_name, opts, &block)
83
+ "bdcst:#{@address_prefix + topic_name}"
84
+ end
85
+
86
+ def receive_file(topic_url, file_path = nil, opts = {}, &block)
87
+ if topic_url.start_with? @address_prefix
88
+ topic_url = topic_url[@address_prefix.length .. -1]
89
+ end
90
+ require 'omf_common/comm/amqp/amqp_file_transfer'
91
+ file_path ||= File.join(Dir.tmpdir, Dir::Tmpname.make_tmpname('bdcast', '.xxx'))
92
+ FileReceiver.new(file_path, @channel, topic_url, opts, &block)
93
+ end
94
+
95
+ private
96
+ def initialize(opts = {})
97
+ @on_connected_procs = []
98
+ super
71
99
  end
72
-
73
100
  end
74
101
  end
75
102
  end
@@ -0,0 +1,259 @@
1
+ require 'set'
2
+ require 'monitor'
3
+
4
+ module OmfCommon
5
+ class Comm::AMQP
6
+
7
+ # Distributes a local file to a set of receivers subscribed to the same
8
+ # topic but may join a various stages.
9
+ #
10
+ class FileBroadcaster
11
+ include MonitorMixin
12
+
13
+ DEF_CHUNK_SIZE = 2**16
14
+ DEF_IDLE_TIME = 60
15
+
16
+ # @param topic[String] Name of topic to send file to
17
+ # @param file_path[String] Path to a local file
18
+ # @param opts[Hash]
19
+ # :chunk_size Max size of data chunk to send
20
+ # :idle_time Min. time in sec to close down broadcaster after having sent last chunk
21
+ #
22
+ def initialize(file_path, channel, topic, opts = {}, &block)
23
+ super() # init monitor mixin
24
+ @block = block
25
+ unless File.readable?(file_path)
26
+ raise "Can't read file '#{file_path}'"
27
+ end
28
+ @mime_type = `file -b --mime-type #{file_path}`.strip
29
+ unless $?.success?
30
+ raise "Can't determine file's mime-type (#{$?})"
31
+ end
32
+ @file_path = file_path
33
+ f = File.open(file_path, 'rb')
34
+ chunk_size = opts[:chunk_size] || DEF_CHUNK_SIZE
35
+ chunk_count = (f.size / chunk_size) + 1
36
+
37
+ @outstanding_chunks = Set.new
38
+ @running = true
39
+ @semaphore = new_cond()
40
+ idle_time = opts[:idle_time] || DEF_IDLE_TIME
41
+
42
+ #chunk_count.times.each {|i| @outstanding_chunks << i}
43
+
44
+ exchange = channel.topic(topic, :auto_delete => true)
45
+ OmfCommon.eventloop.defer do
46
+ _send(f, chunk_size, chunk_count, exchange, idle_time)
47
+ end
48
+
49
+ control_topic = "#{topic}_control"
50
+ control_exchange = channel.topic(control_topic, :auto_delete => true)
51
+ channel.queue("", :exclusive => false) do |queue|
52
+ queue.bind(control_exchange)
53
+ debug "Subscribing to control channel '#{control_topic}'"
54
+ queue.subscribe do |headers, payload|
55
+ hdrs = headers.headers
56
+ debug "Incoming control message '#{hdrs}'"
57
+ from = hdrs['request_from']
58
+ from = 0 if from < 0
59
+ to = hdrs['request_to']
60
+ to = chunk_count - 1 if !to || to >= chunk_count
61
+ synchronize do
62
+ (from .. to).each { |i| @outstanding_chunks << i}
63
+ @semaphore.signal
64
+ end
65
+ end
66
+ @control_queue = queue
67
+ end
68
+ end
69
+
70
+ def _send(f, chunk_size, chunk_count, exchange, idle_time)
71
+ chunks_to_send = nil
72
+ @sent_chunk = false
73
+ _wait_for_closedown(idle_time)
74
+ loop do
75
+ synchronize do
76
+ @semaphore.wait_while { @outstanding_chunks.empty? && @running }
77
+ return unless @running # done!
78
+ chunks_to_send = @outstanding_chunks.to_a
79
+ end
80
+
81
+ chunks_to_send.each do |chunk_id|
82
+ #sleep 3
83
+ synchronize do
84
+ @outstanding_chunks.delete(chunk_id)
85
+ @sent_chunk = true
86
+ end
87
+ offset = chunk_id * chunk_size
88
+ f.seek(offset, IO::SEEK_SET)
89
+ chunk = f.read(chunk_size)
90
+ payload = Base64.encode64(chunk)
91
+ headers = {chunk_id: chunk_id, chunk_count: chunk_count, chunk_offset: offset,
92
+ chunk_size: payload.size,
93
+ path: f.path, file_size: f.size, mime_type: @mime_type}
94
+ debug "Sending chunk #{chunk_id}"
95
+ exchange.publish(payload, {headers: headers})
96
+ end
97
+ end
98
+ end
99
+
100
+ def _wait_for_closedown(idle_time)
101
+ OmfCommon.eventloop.after(idle_time) do
102
+ done = false
103
+ synchronize do
104
+ done = !@sent_chunk && @outstanding_chunks.empty?
105
+ @sent_chunk = false
106
+ end
107
+ if done
108
+ @control_queue.unsubscribe if @control_queue
109
+ @block.call({action: :done}) if @block
110
+ else
111
+ # there was activity in last interval, wait a bit longer
112
+ _wait_for_closedown(idle_time)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ # Receives a file broadcast on 'topic' and stores it in a local file.
119
+ # Optionally, it can report on progress through a provided block.
120
+ #
121
+ class FileReceiver
122
+ include MonitorMixin
123
+
124
+ WAIT_BEFORE_REQUESTING = 2
125
+ WAIT_BEFORE_REQUESTING_EVERYTHING = 3 * WAIT_BEFORE_REQUESTING
126
+
127
+ # @param topic[String] Name of topic to receive file on
128
+ # @param file_path[String] Path to a local file
129
+ # @param opts[Hash]
130
+ # @param block Called on progress.
131
+ #
132
+ def initialize(file_path, channel, topic, opts = {}, &block)
133
+ super() # init monitor mixin
134
+ f = File.open(file_path, 'wb')
135
+ @running = false
136
+ @received_chunks = false
137
+ @outstanding_chunks = Set.new
138
+ @all_requested = false # set to true if we encountered a request for ALL (no 'to')
139
+ @requested_chunks = Set.new
140
+ @received_anything = false
141
+
142
+ control_topic = "#{topic}_control"
143
+ @control_exchange = channel.topic(control_topic, :auto_delete => true)
144
+ channel.queue("", :exclusive => false) do |queue|
145
+ queue.bind(@control_exchange)
146
+ debug "Subscribing to control topic '#{control_topic}'"
147
+ queue.subscribe do |headers, payload|
148
+ hdrs = headers.headers
149
+ debug "Incoming control message '#{hdrs}'"
150
+ from = hdrs['request_from']
151
+ to = hdrs['request_to']
152
+ synchronize do
153
+ if to
154
+ (from .. to).each { |i| @requested_chunks << i}
155
+ else
156
+ debug "Observed request for everything"
157
+ @all_requested = true
158
+ @nothing_received = -1 * WAIT_BEFORE_REQUESTING # Throttle our own desire to request everything
159
+ end
160
+ end
161
+ end
162
+ @control_queue = queue
163
+ end
164
+
165
+ @nothing_received = WAIT_BEFORE_REQUESTING_EVERYTHING - 2 * WAIT_BEFORE_REQUESTING
166
+
167
+ data_exchange = channel.topic(topic, :auto_delete => true)
168
+ channel.queue("", :exclusive => false) do |queue|
169
+ queue.bind(data_exchange)
170
+ queue.subscribe do |headers, payload|
171
+ synchronize do
172
+ @received_chunks = true
173
+ end
174
+ hdrs = headers.headers
175
+ chunk_id = hdrs['chunk_id']
176
+ chunk_offset = hdrs['chunk_offset']
177
+ chunk_count = hdrs['chunk_count']
178
+ unless chunk_id && chunk_offset && chunk_count
179
+ debug "Received message with missing 'chunk_id' or 'chunk_offset' header information (#{hdrs})"
180
+ end
181
+ unless @received_anything
182
+ @outstanding_chunks = chunk_count.times.to_set
183
+ synchronize do
184
+ @running = true
185
+ @received_anything = true
186
+ end
187
+ end
188
+ next unless @outstanding_chunks.include?(chunk_id)
189
+
190
+ debug "Receiving chunk #{chunk_id}"
191
+ f.seek(chunk_offset, IO::SEEK_SET)
192
+ f.write(Base64.decode64(payload))
193
+ @outstanding_chunks.delete(chunk_id)
194
+ received = chunk_count - @outstanding_chunks.size
195
+ if block
196
+ block.call({action: :progress, received: received, progress: 1.0 * received / chunk_count, total: chunk_count})
197
+ end
198
+
199
+ if @outstanding_chunks.empty?
200
+ # got everything
201
+ f.close
202
+ queue.unsubscribe
203
+ @control_queue.unsubscribe if @control_queue
204
+ @timer.cancel
205
+ synchronize { @running = false }
206
+ debug "Fully received #{file_path}"
207
+ if block
208
+ block.call({action: :done, size: hdrs['file_size'],
209
+ path: file_path, mime_type: hdrs['mime_type'],
210
+ received: chunk_count})
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ @timer = OmfCommon.eventloop.every(WAIT_BEFORE_REQUESTING) do
217
+ from = to = nil
218
+ synchronize do
219
+ #puts "RUNNING: #{@running}"
220
+ #break unless @running
221
+ if @received_chunks
222
+ @received_chunks = false
223
+ @nothing_received = 0
224
+ break # ok there is still action
225
+ else
226
+ # nothing happened, so let's ask for something
227
+ if (@nothing_received += WAIT_BEFORE_REQUESTING) >= WAIT_BEFORE_REQUESTING_EVERYTHING
228
+ # something stuck here, let's re-ask for everything
229
+ from = 0
230
+ @nothing_received = 0
231
+ else
232
+ # ask_for is the set of chunks we are still missing but haven't asked for
233
+ ask_for = @outstanding_chunks - @requested_chunks
234
+ break if ask_for.empty? # ok, someone already asked, so better wait
235
+
236
+ # Ask for a single span of consecutive chunks
237
+ aa = ask_for.to_a.sort
238
+ from = to = aa[0]
239
+ aa.each.with_index do |e, i|
240
+ break unless (from + i == e)
241
+ to = e
242
+ @requested_chunks << e
243
+ end
244
+ end
245
+
246
+ end
247
+ end
248
+ if from
249
+ headers = {request_from: from}
250
+ headers[:request_to] = to if to # if nil, ask for everything
251
+ @control_exchange.publish(nil, {headers: headers})
252
+ end
253
+ end
254
+
255
+ end
256
+ end
257
+
258
+ end
259
+ end