xpflow 0.1b

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 (74) hide show
  1. data/bin/xpflow +96 -0
  2. data/lib/colorado.rb +198 -0
  3. data/lib/json/add/core.rb +243 -0
  4. data/lib/json/add/rails.rb +8 -0
  5. data/lib/json/common.rb +423 -0
  6. data/lib/json/editor.rb +1369 -0
  7. data/lib/json/ext.rb +28 -0
  8. data/lib/json/pure/generator.rb +442 -0
  9. data/lib/json/pure/parser.rb +320 -0
  10. data/lib/json/pure.rb +15 -0
  11. data/lib/json/version.rb +8 -0
  12. data/lib/json.rb +62 -0
  13. data/lib/mime/types.rb +881 -0
  14. data/lib/mime-types.rb +3 -0
  15. data/lib/restclient/abstract_response.rb +106 -0
  16. data/lib/restclient/exceptions.rb +193 -0
  17. data/lib/restclient/net_http_ext.rb +55 -0
  18. data/lib/restclient/payload.rb +235 -0
  19. data/lib/restclient/raw_response.rb +34 -0
  20. data/lib/restclient/request.rb +316 -0
  21. data/lib/restclient/resource.rb +169 -0
  22. data/lib/restclient/response.rb +24 -0
  23. data/lib/restclient.rb +174 -0
  24. data/lib/xpflow/bash.rb +341 -0
  25. data/lib/xpflow/bundle.rb +113 -0
  26. data/lib/xpflow/cmdline.rb +249 -0
  27. data/lib/xpflow/collection.rb +122 -0
  28. data/lib/xpflow/concurrency.rb +79 -0
  29. data/lib/xpflow/data.rb +393 -0
  30. data/lib/xpflow/dsl.rb +816 -0
  31. data/lib/xpflow/engine.rb +574 -0
  32. data/lib/xpflow/ensemble.rb +135 -0
  33. data/lib/xpflow/events.rb +56 -0
  34. data/lib/xpflow/experiment.rb +65 -0
  35. data/lib/xpflow/exts/facter.rb +30 -0
  36. data/lib/xpflow/exts/g5k.rb +931 -0
  37. data/lib/xpflow/exts/g5k_use.rb +50 -0
  38. data/lib/xpflow/exts/gui.rb +140 -0
  39. data/lib/xpflow/exts/model.rb +155 -0
  40. data/lib/xpflow/graph.rb +1603 -0
  41. data/lib/xpflow/graph_xpflow.rb +251 -0
  42. data/lib/xpflow/import.rb +196 -0
  43. data/lib/xpflow/library.rb +349 -0
  44. data/lib/xpflow/logging.rb +153 -0
  45. data/lib/xpflow/manager.rb +147 -0
  46. data/lib/xpflow/nodes.rb +1250 -0
  47. data/lib/xpflow/runs.rb +773 -0
  48. data/lib/xpflow/runtime.rb +125 -0
  49. data/lib/xpflow/scope.rb +168 -0
  50. data/lib/xpflow/ssh.rb +186 -0
  51. data/lib/xpflow/stat.rb +50 -0
  52. data/lib/xpflow/stdlib.rb +381 -0
  53. data/lib/xpflow/structs.rb +369 -0
  54. data/lib/xpflow/taktuk.rb +193 -0
  55. data/lib/xpflow/templates/ssh-config.basic +14 -0
  56. data/lib/xpflow/templates/ssh-config.inria +18 -0
  57. data/lib/xpflow/templates/ssh-config.proxy +13 -0
  58. data/lib/xpflow/templates/taktuk +6590 -0
  59. data/lib/xpflow/templates/utils/batch +4 -0
  60. data/lib/xpflow/templates/utils/bootstrap +12 -0
  61. data/lib/xpflow/templates/utils/hostname +3 -0
  62. data/lib/xpflow/templates/utils/ping +3 -0
  63. data/lib/xpflow/templates/utils/rsync +12 -0
  64. data/lib/xpflow/templates/utils/scp +17 -0
  65. data/lib/xpflow/templates/utils/scp_many +8 -0
  66. data/lib/xpflow/templates/utils/ssh +3 -0
  67. data/lib/xpflow/templates/utils/ssh-interactive +4 -0
  68. data/lib/xpflow/templates/utils/taktuk +19 -0
  69. data/lib/xpflow/threads.rb +187 -0
  70. data/lib/xpflow/utils.rb +569 -0
  71. data/lib/xpflow/visual.rb +230 -0
  72. data/lib/xpflow/with_g5k.rb +7 -0
  73. data/lib/xpflow.rb +349 -0
  74. metadata +135 -0
@@ -0,0 +1,230 @@
1
+ # encoding: UTF-8
2
+
3
+ #
4
+ # Visualization of engine execution/data.
5
+ #
6
+
7
+ def dict(h)
8
+ return XPFlow::Visual::Dict.new(h)
9
+ end
10
+
11
+ module XPFlow
12
+
13
+ module Visual
14
+
15
+ class Dict
16
+
17
+ def initialize(h)
18
+ @h = h
19
+ end
20
+
21
+ def to_h
22
+ return @h
23
+ end
24
+
25
+ def method_missing(method)
26
+ return @h[method]
27
+ end
28
+
29
+ def [](key)
30
+ return @h[key]
31
+ end
32
+
33
+ def to_s
34
+ return "d(#{@h.to_s})"
35
+ end
36
+
37
+ end
38
+
39
+ def self.pair_activities(events)
40
+ # returns a list with pairs named
41
+ # after the activities, and values
42
+ # being start, finish
43
+
44
+ starts = events.select { |x| x.type == :start_activity }
45
+ starts = starts.sort { |x, y| x.meta[:time] <=> y.meta[:time] }
46
+ ends = events.select { |x| x.type == :finish_activity }
47
+ contents = ends.map { |x| [ x.meta[:name], x.meta[:time] ] }
48
+ ends = Hash[ contents ]
49
+
50
+ return starts.map do |ev|
51
+ name = ev.meta[:name]
52
+ start = ev.meta[:time]
53
+ finish = ends[name]
54
+ dict({ :name => name, :start => start, :finish => finish, :meta => ev.meta })
55
+ end
56
+ end
57
+
58
+ def self.get_times(activities)
59
+ t_start = activities.map(&:start).min
60
+ t_finish = activities.map(&:finish).compact.max
61
+ return [ t_start, t_finish, t_finish - t_start ]
62
+ end
63
+
64
+ def self.terminal_size
65
+ begin
66
+ return `tput cols`.to_i - 14
67
+ rescue Errno::ENOENT
68
+ return nil
69
+ end
70
+ end
71
+
72
+ def self.show_gantt(events)
73
+
74
+ if events.empty?
75
+ puts
76
+ puts "=== GANTT DIAGRAM IS EMPTY ===".red
77
+ puts
78
+ return
79
+ end
80
+
81
+ times = compute_times(events)
82
+ activities = times[:activities]
83
+ t_start, t_finish, t_span = times[:time_start], times[:time_finish], times[:time_span]
84
+ left_size = activities.map { |x| x[:name].length }.max # length of names
85
+
86
+ width = terminal_size()
87
+ width = width - left_size unless width.nil?
88
+ width = 80 if (width.nil? or width < 80)
89
+
90
+ puts
91
+ puts "=== ACTIVITY GANTT DIAGRAM ===".red
92
+ puts
93
+ activities.each do |a|
94
+ a = dict(a)
95
+ chars = (width * a.graph_span).to_i
96
+ margin = (width * a.graph_start).to_i
97
+ bar = ('=' * chars).yellow
98
+ indent = (' ' * margin)
99
+ status = (a.finished ? '%.3f s' % a.secs_span : 'running')
100
+ puts "%s %s%s (%s)" % \
101
+ [ a.name.ljust(left_size).white!, indent, bar, status ]
102
+ end
103
+ end
104
+
105
+ def self.compute_times(events)
106
+ activities = pair_activities(events)
107
+ activities = activities.select { |x| !x.name.start_with?("__") }
108
+ t_start, t_finish, t_span = get_times(activities)
109
+ activities = activities.map do |x|
110
+ finished = (!x.finish.nil?)
111
+ v = {
112
+ :name => x.name,
113
+ :finished => finished,
114
+ :time_start => x.start,
115
+ :time_finish => x.finish,
116
+ :time_span => finished ? x.finish - x.start : nil,
117
+ :secs_start => x.start - t_start,
118
+ :secs_finish => finished ? x.finish - t_start : t_span,
119
+ :secs_span => finished ? x.finish - x.start : t_finish - x.start,
120
+ }
121
+ v[:graph_start] = v[:secs_start] / t_span
122
+ v[:graph_finish] = v[:secs_finish] / t_span
123
+ v[:graph_span] = v[:secs_span] / t_span
124
+ v
125
+ end
126
+ return {
127
+ :time_start => t_start,
128
+ :time_finish => t_finish,
129
+ :time_span => t_span,
130
+ :activities => activities
131
+ }
132
+ end
133
+
134
+ def self.save_gantt(filename, events)
135
+ times = compute_times(events)
136
+ IO.write(filename, times.to_yaml)
137
+ end
138
+
139
+ def self.show_activities()
140
+ events = $engine.runtime.events()
141
+ activities = pair_activities(events)
142
+ activities = activities.select { |x| x.finish.nil? }
143
+ $console.synchronize do
144
+ bar = ("=" * 80).red
145
+ tab = ' ' * 4
146
+ puts; puts bar
147
+ puts "CURRENT ACTIVITIES".blue
148
+ activities.each do |x|
149
+ puts "#{tab}#{x.meta[:name].green} #{x.meta.inspect}"
150
+ end
151
+ puts bar
152
+ end
153
+ end
154
+ end
155
+
156
+ def self.show_stacktrace(e)
157
+ trace = e.stacktrace
158
+
159
+ puts
160
+ puts "=== ERRORS ===".red
161
+
162
+ tab = ' ' * 4
163
+ for error in trace
164
+ msg, frames = error
165
+ puts tab + "#{msg}".yellow
166
+ for f in frames
167
+ line = (tab * 2) + "#{f.location_long}"
168
+ line += " (at #{f.obj.class})" if f.obj
169
+ puts line
170
+ end
171
+ end
172
+ end
173
+
174
+ class TerminalThread
175
+ # a thread that listens on the terminal
176
+ # to collect events
177
+ # assumes stdin is tty
178
+
179
+ def initialize
180
+ @config = %x(stty --save).strip
181
+ @r, @w = IO.pipe
182
+ @t = nil
183
+ end
184
+
185
+ def cols
186
+ size = %x(stty size).split
187
+ return size.last.to_i
188
+ end
189
+
190
+ def start
191
+ system("stty -icanon -echo")
192
+ @t = Thread.new do
193
+ loop do
194
+ ready = IO.select([ STDIN, @r ], [], []).first
195
+ if ready.include?(STDIN)
196
+ char = STDIN.readpartial(1)
197
+ c = cols()
198
+ case char
199
+ when 'i' then Visual.show_activities()
200
+ else begin
201
+ $console.synchronize do
202
+ puts "Key #{char.inspect} pressed.".rjust(c).blue
203
+ end
204
+ end
205
+
206
+ end
207
+ end
208
+ break if ready.include?(@r)
209
+ end
210
+ end
211
+ end
212
+
213
+ def stop
214
+ system("stty #{@config}")
215
+ @w.close
216
+ @t.join(1.0)
217
+ end
218
+
219
+ def self.start_thread
220
+ if STDIN.tty? and not ENV.key?("BATCH")
221
+ $__terminal_thread__ = TerminalThread.new
222
+ $__terminal_thread__.start
223
+ at_exit { $__terminal_thread__.stop }
224
+ end
225
+ end
226
+
227
+ end
228
+
229
+ end
230
+
@@ -0,0 +1,7 @@
1
+
2
+ # this is a shortcut to ease the pain
3
+
4
+ require 'xpflow'
5
+ require 'xpflow/exts/g5k'
6
+
7
+ $engine.import_library(:g5k, XPFlow::G5K::Library.new)
data/lib/xpflow.rb ADDED
@@ -0,0 +1,349 @@
1
+
2
+ require 'thread'
3
+ require 'digest'
4
+ require 'etc'
5
+ require 'open3'
6
+ require 'yaml'
7
+ require 'optparse'
8
+ require 'monitor'
9
+ require 'colorado'
10
+ require 'pathname'
11
+ require 'highline'
12
+
13
+ def __get_my_requires__
14
+ me = File.dirname(__FILE__)
15
+ files = Dir.entries(File.join(me, 'xpflow'))
16
+ files = files.select { |f| File.extname(f) == '.rb' }
17
+ files = files.select { |f| !f.start_with?('with') }
18
+ modules = files.map { |f| File.basename(f, File.extname(f)) }
19
+ return modules
20
+ end
21
+
22
+ # some Ruby version trickery
23
+
24
+ _, $version = RUBY_VERSION.split('.')
25
+ $version = $version.to_i
26
+ $ruby18 = ($version == 8)
27
+ $ruby19 = ($version == 9)
28
+
29
+ class Symbol
30
+
31
+ def <=>(x)
32
+ return (self.to_s <=> x.to_s)
33
+ end
34
+
35
+ end if $ruby18
36
+
37
+ if $bundled.nil?
38
+ # necessary includes
39
+ require('xpflow/utils')
40
+ require('xpflow/structs')
41
+ require('xpflow/library')
42
+ require('xpflow/scope')
43
+
44
+ for m in __get_my_requires__ do
45
+ require('xpflow/' + m)
46
+ end
47
+ end
48
+
49
+ Engine = $engine = XPFlow::Engine.new
50
+
51
+ def engine(&block)
52
+ $engine.instance_exec(&block)
53
+ end
54
+
55
+ def process(*args, &block)
56
+ engine { process(*args, &block) }
57
+ end
58
+
59
+ def activity(*args, &block)
60
+ engine { activity(*args, &block) }
61
+ end
62
+
63
+ def macro(*args, &block)
64
+ engine { macro(*args, &block) }
65
+ end
66
+
67
+ def dsl(name, *args, &block)
68
+ return XPFlow::ProcessDSL.new(name, $engine, &block)
69
+ end
70
+
71
+ def empty_activity(*args)
72
+ args.each do |name|
73
+ activity(name) do
74
+ end
75
+ end
76
+ end
77
+
78
+ def script(name, path)
79
+ # TODO: put more magic
80
+ process(name) do |nodes|
81
+ execute_many(nodes, path)
82
+ end
83
+ single = "#{name}_seq".to_sym
84
+ process(single) do |node|
85
+ execute(node, path)
86
+ end
87
+ end
88
+
89
+ def activity_visibility(name)
90
+ return true
91
+ end
92
+
93
+ def __parse_variables__()
94
+ opts = XPFlow::Options.new(ARGV.dup)
95
+ $variables = opts.vars
96
+ end
97
+
98
+ def parse_g5k_query(query, opts = { :max => 1 })
99
+ lib = XPFlow::G5K::Library.new
100
+ lib.logging = proc { |x| puts x }
101
+ sites = lib.sites.map { |x| x['uid'] }
102
+ best_sites = [ "nancy", "rennes", "sophia" ]
103
+ sites = best_sites + (sites - best_sites)
104
+
105
+ max = opts[:max]
106
+
107
+ uniq_jobs = proc { |js| Hash[js.map { |j| [ [j['uid'], j['site']], j ] }].values }
108
+ jobs_at_site = proc do |site|
109
+ js = lib.jobs(site)
110
+ js = js.select { |x| x['state'] == 'running' }
111
+ js.select { |x| x['user_uid'] == lib.g5k.user }
112
+ end
113
+ site_from_job = proc { |j| j['links'].select { |l| l['rel'] == 'parent' }.first['href'].split("/").last }
114
+
115
+ jobs = []
116
+
117
+ query.split(":").each do |word|
118
+ if word == '*'
119
+ sites.each do |s|
120
+ jobs = uniq_jobs.call(jobs + jobs_at_site.call(s))
121
+ break if jobs.length >= max
122
+ end
123
+ break if jobs.length >= max
124
+ elsif (m = word.match(/^([a-z]+)\/(.+)/)) and (sites.include?(m.captures.first))
125
+ query_site, query_str = m.captures
126
+ site_jobs = jobs_at_site.call(query_site)
127
+ if query_str == '*'
128
+ jobs = uniq_jobs.call(jobs + site_jobs)
129
+ break if jobs.length >= max
130
+ elsif query_str.match(/^[0-9]+$/)
131
+ job_uid = query_str.to_i
132
+ site_job = site_jobs.select { |x| x['uid'] == job_uid }
133
+ if site_job.length == 0
134
+ raise "No job with id = #{job_uid} in '#{query_site}'."
135
+ end
136
+ jobs = uniq_jobs.call(jobs + [ site_job.first ])
137
+ break if jobs.length >= max
138
+ else
139
+ raise "Cannot parse '#{query_str}'"
140
+ end
141
+ else
142
+ raise "Cannot parse '#{word}'"
143
+ end
144
+ end
145
+
146
+ grouped_jobs = Hash.new { |h, k| h[k] = [] }
147
+ jobs.each { |j| grouped_jobs[site_from_job.call(j)].push(j) }
148
+ jobs_info = grouped_jobs.each_pair.map { |site, jobs| "{" + jobs.map { |x| x['uid'] }.join(",") + "}@#{site}" }.join(" ")
149
+
150
+ puts "Found #{jobs.length} jobs: #{jobs_info} (but :max is #{max})"
151
+
152
+ jobs = jobs[0...max]
153
+ return jobs
154
+ end
155
+
156
+ def parse_g5k_query_one_wait(query)
157
+ lib = XPFlow::G5K::Library.new
158
+ lib.logging = proc { |x| puts x }
159
+ jobs = parse_g5k_query(query, :max => 1)
160
+ if jobs.length == 0
161
+ raise "Could not find any jobs."
162
+ end
163
+ job = jobs.first
164
+ j = lib.g5k.get_json_raw(job.rel_self)
165
+ j = lib.wait_for_job(j)
166
+ return j
167
+ end
168
+
169
+ def _bool_var(v)
170
+ return v if v.is_a?(TrueClass) or v.is_a?(FalseClass)
171
+ v = v.strip
172
+ if (v.to_i == 1) or ([ "true", "yes" ].include?(v.downcase))
173
+ return true
174
+ end
175
+ return false
176
+ end
177
+
178
+ class Seq
179
+
180
+ def initialize(arr)
181
+ @arr = arr
182
+ end
183
+
184
+ def range
185
+ return Seq.new(@arr.uniq.sort)
186
+ end
187
+
188
+ def self.parse(spec)
189
+ nums = []
190
+ parts = spec.strip.split("+").map(&:strip)
191
+ parts.each do |p|
192
+ if p.match(/^(\d+)$/)
193
+ nums += [ p.to_i ]
194
+ elsif (m = p.match(/^(\d+)(:|-|\.\.)(\d+)$/))
195
+ s, _, e = m.captures.map(&:to_i)
196
+ nums += (s..e).to_a
197
+ elsif (m = p.match(/^(\d+)(:|-|\.\.)(\d+)\/(\d+)$/))
198
+ s, _, e, div = m.captures.map(&:to_i)
199
+ raise "Dividing by zero here" if div == 0
200
+ raise "Division must be > 1" if div == 1
201
+ if (e - s) % (div - 1) == 0
202
+ d = (e - s) / (div - 1) # use integer arithmetic if possible
203
+ else
204
+ d = (e - s).to_f / (div - 1).to_f
205
+ end
206
+ pts = []
207
+ (1..(div - 2)).each do |i|
208
+ p = i * d + s
209
+ pts.push(p.to_i)
210
+ end
211
+ nums += ([ s ] + pts + [ e ])
212
+ elsif (m = p.match(/^(\d+):(\d+):(\d+)$/))
213
+ s, d, e = m.captures.map(&:to_i)
214
+ if d == 0
215
+ raise "Zero range increment (#{p})"
216
+ end
217
+ while s <= e
218
+ nums.push(s)
219
+ s += d
220
+ end
221
+ else
222
+ raise "Wrong range specification"
223
+ end
224
+ end
225
+ return Seq.new(nums)
226
+ end
227
+
228
+ def to_s
229
+ return "seq(#{@arr.inspect})"
230
+ end
231
+
232
+ def to_list
233
+ return @arr.dup
234
+ end
235
+
236
+ def max
237
+ return @arr.max
238
+ end
239
+
240
+ end
241
+
242
+ $__highline__ = HighLine.new
243
+
244
+ def ask_for_type(name, type, opts)
245
+ text = "Provide value of :#{name} (type :#{type}): "
246
+ text = opts[:text] if opts[:text]
247
+ if [ :seq, :str, :range, :pass ].include?(type)
248
+ value = $__highline__.ask(text) do |q|
249
+ q.echo = "*" if type == :pass
250
+ end
251
+ return value
252
+ elsif type == :int
253
+ value = $__highline__.ask(text, Integer)
254
+ return value
255
+ elsif type == :bool
256
+ value = $__highline__.agree(text) { |q| q.default = "no" }
257
+ return value
258
+ else
259
+ # TODO, support more variables
260
+ raise "Unknown value for variable :#{name} (type :#{type})"
261
+ end
262
+ end
263
+
264
+ def var(name, type = :str, default = nil, opts = {})
265
+ # TODO
266
+ if default.is_a?(Hash)
267
+ opts = default
268
+ default = opts[:default]
269
+ end
270
+ v = $variables[name.to_s]
271
+ if v.nil?
272
+ if default.nil?
273
+ # try to ask for a variable
274
+ if !STDIN.tty?
275
+ raise "Unknown value for variable :#{name} (type :#{type})"
276
+ end
277
+ v = $variables[name.to_s] = ask_for_type(name, type, opts)
278
+ if opts[:callback]
279
+ opts[:callback].call(v)
280
+ end
281
+ else
282
+ return default
283
+ end
284
+ end
285
+
286
+ result = case type
287
+ when :str then v
288
+ when :int then v.to_i
289
+ when :float then v.to_f
290
+ when :bool then _bool_var(v)
291
+ when :range then Seq.parse(v).range
292
+ when :seq then Seq.parse(v)
293
+ when :pass then v
294
+ when :g5k then parse_g5k_query_one_wait(v)
295
+ else
296
+ raise "Unknown type '#{type}'"
297
+ end
298
+ return result
299
+ end
300
+
301
+ def set_var(name, value)
302
+ $variables[name.to_s] = value
303
+ return value
304
+ end
305
+
306
+ def experiment(name, &block)
307
+ body_name = :"#{name}/body"
308
+ before_name = :"#{name}/before"
309
+ after_name = :"#{name}/after"
310
+ $engine.process(body_name, &block)
311
+ $engine.process(before_name) { }
312
+ $engine.process(after_name) { }
313
+ $engine.process(name) do |*args|
314
+ run :"/new_experiment" # create a new experiment
315
+ log "Starting experiment '#{name}'"
316
+ run(before_name, *args)
317
+ result = run(body_name, *args)
318
+ run(after_name, *args)
319
+ log "Finished experiment '#{name}'"
320
+ value(result)
321
+ end
322
+ end
323
+
324
+ def after_activity(name, &block)
325
+ # we replace the given activity with
326
+ # a one that executes the previous one and then
327
+ # the one given
328
+
329
+ after_block = block
330
+ activity(name) do |*args|
331
+ value = parent(*args)
332
+ self.set_result(value)
333
+ value = self.instance_exec(*args, &after_block)
334
+ value
335
+ end
336
+ end
337
+
338
+ def before_activity(name, &block)
339
+ # similarly to after_activity
340
+ before_block = block
341
+ activity(name) do |*args|
342
+ self.instance_exec(*args, &before_block)
343
+ parent(*args)
344
+ end
345
+ end
346
+
347
+ def realize(path)
348
+ return Pathname.new(path).realpath.to_s
349
+ end