omf_common 6.0.0.pre.6 → 6.0.0.pre.7

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/lib/omf_common.rb CHANGED
@@ -5,8 +5,12 @@ require "omf_common/version"
5
5
  require "omf_common/message"
6
6
  require "omf_common/comm"
7
7
  require "omf_common/command"
8
+ require "omf_common/topic"
9
+ require "omf_common/topic_message"
8
10
  require "omf_common/core_ext/string"
11
+ require "omf_common/core_ext/object"
9
12
 
13
+ # Use global default logger from logging gem
10
14
  include Logging.globally
11
15
 
12
16
  Logging.appenders.stdout('stdout',
@@ -0,0 +1,21 @@
1
+ class Object
2
+ def stub name, val_or_callable, &block
3
+ new_name = "__minitest_stub__#{name}"
4
+ metaclass = class << self; self; end
5
+ metaclass.send :alias_method, new_name, name
6
+ metaclass.send :define_method, name do |*args, &stub_block|
7
+ if val_or_callable.respond_to? :call then
8
+ val_or_callable.call(*args, &stub_block)
9
+ else
10
+ val_or_callable
11
+ end
12
+ end
13
+ yield self
14
+ ensure
15
+ metaclass.send :undef_method, name
16
+ metaclass.send :alias_method, name, new_name
17
+ metaclass.send :undef_method, new_name
18
+ end
19
+ end
20
+
21
+
@@ -33,55 +33,105 @@ module OmfCommon
33
33
  # Create a new pubsub topic with additional configuration
34
34
  #
35
35
  # @param [String] topic Pubsub topic name
36
- # @param [String] host Pubsub host address
37
- def create_topic(topic, host, &block)
38
- pubsub.create(topic, prefix_host(host), PUBSUB_CONFIGURE, &callback_logging(__method__, topic, &block))
36
+ def create_topic(topic, &block)
37
+ pubsub.create(topic, default_host, PUBSUB_CONFIGURE, &callback_logging(__method__, topic, &block))
39
38
  end
40
39
 
41
40
  # Delete a pubsub topic
42
41
  #
43
42
  # @param [String] topic Pubsub topic name
44
- # @param [String] host Pubsub host address
45
- def delete_topic(topic, host, &block)
46
- pubsub.delete(topic, prefix_host(host), &callback_logging(__method__, topic, &block))
43
+ def delete_topic(topic, &block)
44
+ pubsub.delete(topic, default_host, &callback_logging(__method__, topic, &block))
47
45
  end
48
46
 
49
47
  # Subscribe to a pubsub topic
50
48
  #
51
49
  # @param [String] topic Pubsub topic name
52
- # @param [String] host Pubsub host address
53
- def subscribe(topic, host, &block)
54
- pubsub.subscribe(topic, nil, prefix_host(host), &callback_logging(__method__, topic, &block))
50
+ def subscribe(topic, &block)
51
+ pubsub.subscribe(topic, nil, default_host, &callback_logging(__method__, topic, &block))
55
52
  end
56
53
 
57
54
  # Un-subscribe all existing subscriptions from all pubsub topics.
58
- #
59
- # @param [String] host Pubsub host address
60
- def unsubscribe(host)
61
- pubsub.subscriptions(prefix_host(host)) do |m|
55
+ def unsubscribe
56
+ pubsub.subscriptions(default_host) do |m|
62
57
  m[:subscribed] && m[:subscribed].each do |s|
63
- pubsub.unsubscribe(s[:node], nil, s[:subid], prefix_host(host), &callback_logging(__method__, s[:node], s[:subid]))
58
+ pubsub.unsubscribe(s[:node], nil, s[:subid], default_host, &callback_logging(__method__, s[:node], s[:subid]))
64
59
  end
65
60
  end
66
61
  end
67
62
 
68
- def affiliations(host, &block)
69
- pubsub.affiliations(prefix_host(host), &callback_logging(__method__, &block))
63
+ def affiliations(&block)
64
+ pubsub.affiliations(default_host, &callback_logging(__method__, &block))
70
65
  end
71
66
 
72
67
  # Publish to a pubsub topic
73
68
  #
74
69
  # @param [String] topic Pubsub topic name
75
70
  # @param [String] message Any XML fragment to be sent as payload
76
- # @param [String] host Pubsub host address
77
- def publish(topic, message, host, &block)
78
- pubsub.publish(topic, message, prefix_host(host), &callback_logging(__method__, topic, message.operation, &block))
71
+ def publish(topic, message, &block)
72
+ raise StandardError, "Invalid message" unless message.valid?
73
+ pubsub.publish(topic, message, default_host, &callback_logging(__method__, topic, message.operation, &block))
74
+ end
75
+
76
+ # Generate OMF related message
77
+ %w(create configure request inform release).each do |m_name|
78
+ define_method("#{m_name}_message") do |*args, &block|
79
+ message =
80
+ if block
81
+ Message.send(m_name, *args, &block)
82
+ elsif args[0].kind_of? Array
83
+ Message.send(m_name) do |v|
84
+ args[0].each do |opt|
85
+ if opt.kind_of? Hash
86
+ opt.each_pair do |key, value|
87
+ v.property(key, value)
88
+ end
89
+ else
90
+ v.property(opt)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ OmfCommon::TopicMessage.new(message, self)
97
+ end
98
+ end
99
+
100
+ # Event machine related method delegation
101
+ %w(add_timer add_periodic_timer).each do |m_name|
102
+ define_method(m_name) do |*args, &block|
103
+ EM.send(m_name, *args, &block)
104
+ end
105
+ end
106
+
107
+ %w(created status released failed).each do |inform_type|
108
+ define_method("on_#{inform_type}_message") do |*args, &message_block|
109
+ context_id = args[0].context_id if args[0]
110
+ event_block = proc do |event|
111
+ message_block.call(Message.parse(event.items.first.payload))
112
+ end
113
+ guard_block = proc do |event|
114
+ (event.items?) && (!event.delayed?) &&
115
+ event.items.first.payload &&
116
+ (omf_message = Message.parse(event.items.first.payload)) &&
117
+ omf_message.operation == :inform &&
118
+ omf_message.read_content(:inform_type) == inform_type.upcase &&
119
+ (context_id ? (omf_message.context_id == context_id) : true)
120
+ end
121
+ pubsub_event(guard_block, &callback_logging(__method__, &event_block))
122
+ end
79
123
  end
80
124
 
81
125
  # Event callback for pubsub topic event(item published)
82
126
  #
83
127
  def topic_event(*args, &block)
84
- pubsub_event(:items?, *args, &callback_logging(__method__, &block))
128
+ pubsub_event(*args, &callback_logging(__method__, &block))
129
+ end
130
+
131
+ # Return a topic object represents pubsub topic
132
+ #
133
+ def get_topic(topic_id)
134
+ OmfCommon::Topic.new(topic_id, self)
85
135
  end
86
136
 
87
137
  private
@@ -96,8 +146,8 @@ module OmfCommon
96
146
  end
97
147
  end
98
148
 
99
- def prefix_host(host)
100
- "#{HOST_PREFIX}.#{host}"
149
+ def default_host
150
+ "#{HOST_PREFIX}.#{client.jid.domain}"
101
151
  end
102
152
  end
103
153
  end
@@ -0,0 +1,163 @@
1
+ #
2
+ # Copyright (c) 2006-2012 National ICT Australia (NICTA), Australia
3
+ #
4
+ # Copyright (c) 2004-2009 WINLAB, Rutgers University, USA
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+ #
24
+ #
25
+ # Library of client side helpers
26
+ #
27
+ require 'fcntl'
28
+
29
+ #
30
+ # Run an application on the client.
31
+ #
32
+ # Borrows from Open3
33
+ #
34
+ class ExecApp
35
+
36
+ # Holds the pids for all active apps
37
+ @@all_apps = Hash.new
38
+
39
+ # True if this active app is being killed by a proper
40
+ # call to ExecApp.signal_all() or signal()
41
+ # (i.e. when the caller of ExecApp decided to stop the application,
42
+ # as far as we are concerned, this is a 'clean' exit)
43
+ @clean_exit = false
44
+
45
+ # Return an application instance based on its ID
46
+ #
47
+ # @param [String] id of the application to return
48
+ def ExecApp.[](id)
49
+ app = @@all_apps[id]
50
+ logger.info "Unknown application '#{id}/#{id.class}'" if app.nil?
51
+ return app
52
+ end
53
+
54
+ def ExecApp.signal_all(signal = 'KILL')
55
+ @@all_apps.each_value { |app| app.signal(signal) }
56
+ end
57
+
58
+ def stdin(line)
59
+ logger.debug "Writing '#{line}' to app '#{@id}'"
60
+ @stdin.write("#{line}\n")
61
+ @stdin.flush
62
+ end
63
+
64
+ def signal(signal = 'KILL')
65
+ @clean_exit = true
66
+ Process.kill(signal, @pid)
67
+ end
68
+
69
+ #
70
+ # Run an application 'cmd' in a separate thread and monitor
71
+ # its stdout. Also send status reports to the 'observer' by
72
+ # calling its "on_app_event(eventType, appId, message")"
73
+ #
74
+ # @param id ID of application (used for reporting)
75
+ # @param observer Observer of application's progress
76
+ # @param cmd Command path and args
77
+ # @param map_std_err_to_out If true report stderr as stdin [false]
78
+ #
79
+ def initialize(id, observer, cmd, map_std_err_to_out = false)
80
+
81
+ @id = id
82
+ @observer = observer
83
+ @@all_apps[id] = self
84
+
85
+ pw = IO::pipe # pipe[0] for read, pipe[1] for write
86
+ pr = IO::pipe
87
+ pe = IO::pipe
88
+
89
+ logger.debug "Starting application '#{id}' - cmd: '#{cmd}'"
90
+ @observer.on_app_event(:STARTED, id, cmd)
91
+ @pid = fork {
92
+ # child will remap pipes to std and exec cmd
93
+ pw[1].close
94
+ STDIN.reopen(pw[0])
95
+ pw[0].close
96
+
97
+ pr[0].close
98
+ STDOUT.reopen(pr[1])
99
+ pr[1].close
100
+
101
+ pe[0].close
102
+ STDERR.reopen(pe[1])
103
+ pe[1].close
104
+
105
+ begin
106
+ exec(cmd)
107
+ rescue => ex
108
+ cmd = cmd.join(' ') if cmd.kind_of?(Array)
109
+ STDERR.puts "exec failed for '#{cmd}' (#{$!}): #{ex}"
110
+ end
111
+ # Should never get here
112
+ exit!
113
+ }
114
+
115
+ pw[0].close
116
+ pr[1].close
117
+ pe[1].close
118
+ monitor_pipe(:stdout, pr[0])
119
+ monitor_pipe(map_std_err_to_out ? :stdout : :stderr, pe[0])
120
+ # Create thread which waits for application to exit
121
+ Thread.new(id, @pid) do |id, pid|
122
+ ret = Process.waitpid(pid)
123
+ status = $?
124
+ @@all_apps.delete(@id)
125
+ # app finished
126
+ if (status == 0) || @clean_exit
127
+ s = "OK"
128
+ logger.debug "Application '#{id}' finished"
129
+ else
130
+ s = "ERROR"
131
+ logger.debug "Application '#{id}' failed (code=#{status})"
132
+ end
133
+ @observer.on_app_event("DONE.#{s}", @id, "status: #{status}")
134
+ end
135
+ @stdin = pw[1]
136
+ end
137
+
138
+ private
139
+
140
+ #
141
+ # Create a thread to monitor the process and its output
142
+ # and report that back to the server
143
+ #
144
+ # @param name Name of app stream to monitor (should be :stdout, :stderr)
145
+ # @param pipe Pipe to read from
146
+ #
147
+ def monitor_pipe(name, pipe)
148
+ Thread.new() do
149
+ begin
150
+ while true do
151
+ s = pipe.readline.chomp
152
+ @observer.on_app_event(name.to_s.upcase, @id, s)
153
+ end
154
+ rescue EOFError
155
+ # do nothing
156
+ rescue Exception => err
157
+ logger.error "monitorApp(#{@id}): #{err}"
158
+ ensure
159
+ pipe.close
160
+ end
161
+ end
162
+ end
163
+ end
@@ -10,23 +10,25 @@ module OmfCommon
10
10
  #
11
11
  # Message.request do |message|
12
12
  # message.property('os', 'debian')
13
- # message.property('memory', 2) do |p|
14
- # p.element('unit', 'gb')
15
- # end
13
+ # message.property('memory', { value: 2, unit: 'gb' })
16
14
  # end
17
15
  #
18
16
  class Message < Niceogiri::XML::Node
19
17
  OMF_NAMESPACE = "http://schema.mytestbed.net/#{OmfCommon::PROTOCOL_VERSION}/protocol"
20
- SCHEMA_FILE = "#{File.dirname(__FILE__)}/protocol.rng"
18
+ SCHEMA_FILE = "#{File.dirname(__FILE__)}/protocol/#{OmfCommon::PROTOCOL_VERSION}.rng"
21
19
  OPERATION = %w(create configure request release inform)
22
20
 
23
21
  class << self
24
22
  OPERATION.each do |operation|
25
23
  define_method(operation) do |*args, &block|
26
24
  xml = new(operation, nil, OMF_NAMESPACE)
27
- xml.element('context_id', operation == 'inform' ? args[0] : SecureRandom.uuid)
25
+ if operation == 'inform'
26
+ xml.element('context_id', args[1] || SecureRandom.uuid)
27
+ xml.element('inform_type', args[0])
28
+ else
29
+ xml.element('context_id', SecureRandom.uuid)
30
+ end
28
31
  xml.element('publish_to', args[0]) if operation == 'request'
29
- xml.element('inform_type', args[1]) if operation == 'inform'
30
32
  block.call(xml) if block
31
33
  xml.sign
32
34
  end
@@ -40,17 +42,62 @@ module OmfCommon
40
42
 
41
43
  # Construct a property xml node
42
44
  #
43
- def property(key, value = nil, &block)
45
+ def property(key, value = nil)
44
46
  key_node = Message.new('property')
45
47
  key_node.write_attr('key', key)
48
+
49
+ unless value.nil?
50
+ key_node.write_attr('type', value.class.to_s.downcase)
51
+ c_node = value_node_set(value)
52
+
53
+ if c_node.class == Array
54
+ c_node.each { |c_n| key_node.add_child(c_n) }
55
+ else
56
+ key_node.add_child(c_node)
57
+ end
58
+ end
46
59
  add_child(key_node)
47
- if block
48
- key_node.element('value', value) if value
49
- block.call(key_node)
60
+ key_node
61
+ end
62
+
63
+ def value_node_set(value, key = nil)
64
+ case value
65
+ when Hash
66
+ [].tap do |array|
67
+ value.each_pair do |k, v|
68
+ n = Message.new(k)
69
+ n.write_attr('type', v.class.to_s.downcase)
70
+
71
+ c_node = value_node_set(v, k)
72
+ if c_node.class == Array
73
+ c_node.each { |c_n| n.add_child(c_n) }
74
+ else
75
+ n.add_child(c_node)
76
+ end
77
+ array << n
78
+ end
79
+ end
80
+ when Array
81
+ value.map do |v|
82
+ n = Message.new('item')
83
+ n.write_attr('type', v.class.to_s.downcase)
84
+
85
+ c_node = value_node_set(v, 'item')
86
+ if c_node.class == Array
87
+ c_node.each { |c_n| n.add_child(c_n) }
88
+ else
89
+ n.add_child(c_node)
90
+ end
91
+ n
92
+ end
50
93
  else
51
- key_node.content = value if value
94
+ if key.nil?
95
+ value.to_s
96
+ else
97
+ n = Message.new(key)
98
+ n.add_child(value.to_s)
99
+ end
52
100
  end
53
- key_node
54
101
  end
55
102
 
56
103
  # Generate SHA1 of canonicalised xml and write into the ID attribute of the message
@@ -63,21 +110,26 @@ module OmfCommon
63
110
  # Validate against relaxng schema
64
111
  #
65
112
  def valid?
66
- validation = Nokogiri::XML::RelaxNG(File.open(SCHEMA_FILE)).validate(document)
113
+ validation = Nokogiri::XML::RelaxNG(File.open(SCHEMA_FILE)).validate(self.document)
67
114
  if validation.empty?
68
115
  true
69
116
  else
70
117
  logger.error validation.map(&:message).join("\n")
118
+ logger.debug self.to_s
71
119
  false
72
120
  end
73
121
  end
74
122
 
75
123
  # Short cut for adding xml node
76
124
  #
77
- def element(key, value)
78
- key_node = Niceogiri::XML::Node.new(key)
79
- key_node.content = value
125
+ def element(key, value = nil, &block)
126
+ key_node = Message.new(key)
80
127
  add_child(key_node)
128
+ if block
129
+ block.call(key_node)
130
+ else
131
+ key_node.content = value if value
132
+ end
81
133
  end
82
134
 
83
135
  # The root element_name represents operation
@@ -98,7 +150,18 @@ module OmfCommon
98
150
  # We just want to know the content of an non-repeatable element
99
151
  #
100
152
  def read_content(element_name)
101
- read_element("//#{element_name}").first.content rescue nil
153
+ element_content = read_element("//#{element_name}").first.content rescue nil
154
+ element_content.empty? ? nil : element_content
155
+ end
156
+
157
+ # Context ID will be requested quite often
158
+ def context_id
159
+ read_property(:context_id) || read_content(:context_id)
160
+ end
161
+
162
+ # Resource ID is another frequent requested property
163
+ def resource_id
164
+ read_property(:resource_id) || read_content(:resource_id)
102
165
  end
103
166
 
104
167
  # Get a property by key
@@ -109,17 +172,38 @@ module OmfCommon
109
172
  def read_property(key)
110
173
  key = key.to_s
111
174
  e = read_element("//property[@key='#{key}']").first
112
- if e
113
- if e.children.size == 1
114
- e.content.ducktype
115
- else
116
- Hashie::Mash.new.tap do |mash|
117
- e.element_children.each do |child|
118
- mash[child.element_name] ||= child.content.ducktype
119
- end
120
- end
175
+ reconstruct_data(e) if e
176
+ end
177
+
178
+ def reconstruct_data(node)
179
+ case node.attr('type')
180
+ when 'array'
181
+ mash ||= Hashie::Mash.new
182
+ mash[:items] = node.element_children.map do |child|
183
+ reconstruct_data(child)
121
184
  end
185
+ mash
186
+ when /hash/
187
+ mash ||= Hashie::Mash.new
188
+ node.element_children.each do |child|
189
+ mash[child.attr('key') || child.element_name] ||= reconstruct_data(child)
190
+ end
191
+ mash
192
+ else
193
+ node.content.empty? ? nil : node.content.ducktype
122
194
  end
123
195
  end
196
+
197
+ # Iterate each property element
198
+ #
199
+ def each_property(&block)
200
+ read_element("//property").each { |v| block.call(v) }
201
+ end
202
+
203
+ # Pretty print for application event message
204
+ #
205
+ def print_app_event
206
+ "APP_EVENT (#{read_property(:app)}, ##{read_property(:seq)}, #{read_property(:event)}): #{read_property(:msg)}"
207
+ end
124
208
  end
125
209
  end