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

Sign up to get free protection for your applications and to get access to all the features.
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