omf_common 6.0.0 → 6.0.2.pre.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 (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