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.
- data/bin/xpflow +96 -0
- data/lib/colorado.rb +198 -0
- data/lib/json/add/core.rb +243 -0
- data/lib/json/add/rails.rb +8 -0
- data/lib/json/common.rb +423 -0
- data/lib/json/editor.rb +1369 -0
- data/lib/json/ext.rb +28 -0
- data/lib/json/pure/generator.rb +442 -0
- data/lib/json/pure/parser.rb +320 -0
- data/lib/json/pure.rb +15 -0
- data/lib/json/version.rb +8 -0
- data/lib/json.rb +62 -0
- data/lib/mime/types.rb +881 -0
- data/lib/mime-types.rb +3 -0
- data/lib/restclient/abstract_response.rb +106 -0
- data/lib/restclient/exceptions.rb +193 -0
- data/lib/restclient/net_http_ext.rb +55 -0
- data/lib/restclient/payload.rb +235 -0
- data/lib/restclient/raw_response.rb +34 -0
- data/lib/restclient/request.rb +316 -0
- data/lib/restclient/resource.rb +169 -0
- data/lib/restclient/response.rb +24 -0
- data/lib/restclient.rb +174 -0
- data/lib/xpflow/bash.rb +341 -0
- data/lib/xpflow/bundle.rb +113 -0
- data/lib/xpflow/cmdline.rb +249 -0
- data/lib/xpflow/collection.rb +122 -0
- data/lib/xpflow/concurrency.rb +79 -0
- data/lib/xpflow/data.rb +393 -0
- data/lib/xpflow/dsl.rb +816 -0
- data/lib/xpflow/engine.rb +574 -0
- data/lib/xpflow/ensemble.rb +135 -0
- data/lib/xpflow/events.rb +56 -0
- data/lib/xpflow/experiment.rb +65 -0
- data/lib/xpflow/exts/facter.rb +30 -0
- data/lib/xpflow/exts/g5k.rb +931 -0
- data/lib/xpflow/exts/g5k_use.rb +50 -0
- data/lib/xpflow/exts/gui.rb +140 -0
- data/lib/xpflow/exts/model.rb +155 -0
- data/lib/xpflow/graph.rb +1603 -0
- data/lib/xpflow/graph_xpflow.rb +251 -0
- data/lib/xpflow/import.rb +196 -0
- data/lib/xpflow/library.rb +349 -0
- data/lib/xpflow/logging.rb +153 -0
- data/lib/xpflow/manager.rb +147 -0
- data/lib/xpflow/nodes.rb +1250 -0
- data/lib/xpflow/runs.rb +773 -0
- data/lib/xpflow/runtime.rb +125 -0
- data/lib/xpflow/scope.rb +168 -0
- data/lib/xpflow/ssh.rb +186 -0
- data/lib/xpflow/stat.rb +50 -0
- data/lib/xpflow/stdlib.rb +381 -0
- data/lib/xpflow/structs.rb +369 -0
- data/lib/xpflow/taktuk.rb +193 -0
- data/lib/xpflow/templates/ssh-config.basic +14 -0
- data/lib/xpflow/templates/ssh-config.inria +18 -0
- data/lib/xpflow/templates/ssh-config.proxy +13 -0
- data/lib/xpflow/templates/taktuk +6590 -0
- data/lib/xpflow/templates/utils/batch +4 -0
- data/lib/xpflow/templates/utils/bootstrap +12 -0
- data/lib/xpflow/templates/utils/hostname +3 -0
- data/lib/xpflow/templates/utils/ping +3 -0
- data/lib/xpflow/templates/utils/rsync +12 -0
- data/lib/xpflow/templates/utils/scp +17 -0
- data/lib/xpflow/templates/utils/scp_many +8 -0
- data/lib/xpflow/templates/utils/ssh +3 -0
- data/lib/xpflow/templates/utils/ssh-interactive +4 -0
- data/lib/xpflow/templates/utils/taktuk +19 -0
- data/lib/xpflow/threads.rb +187 -0
- data/lib/xpflow/utils.rb +569 -0
- data/lib/xpflow/visual.rb +230 -0
- data/lib/xpflow/with_g5k.rb +7 -0
- data/lib/xpflow.rb +349 -0
- metadata +135 -0
data/lib/xpflow/bash.rb
ADDED
@@ -0,0 +1,341 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# Features a cool class to interface with Bash.
|
4
|
+
#
|
5
|
+
|
6
|
+
# exceptionally the requirements are present since
|
7
|
+
# this module can be nicely used separately
|
8
|
+
|
9
|
+
require 'digest'
|
10
|
+
require 'open3'
|
11
|
+
|
12
|
+
module Bash
|
13
|
+
|
14
|
+
class BashError < StandardError; end
|
15
|
+
class BashTimeout < BashError; end
|
16
|
+
class BashPaddingError < BashError; end
|
17
|
+
|
18
|
+
class StatusError < BashError
|
19
|
+
|
20
|
+
attr_reader :status
|
21
|
+
attr_reader :output
|
22
|
+
|
23
|
+
def initialize(cmd, status, output)
|
24
|
+
super("'#{cmd}' returned with status = #{status}")
|
25
|
+
@status = status
|
26
|
+
@cmd = cmd
|
27
|
+
@output = output
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
class Bash
|
33
|
+
|
34
|
+
def initialize(stdin, stdout, opts = {})
|
35
|
+
@stdin = stdin
|
36
|
+
@stdout = stdout
|
37
|
+
@opts = {
|
38
|
+
:debug => false,
|
39
|
+
:timeout => 120.0
|
40
|
+
}.merge(opts)
|
41
|
+
@buff = ''
|
42
|
+
@debug = @opts[:debug]
|
43
|
+
@timeout = @opts[:timeout]
|
44
|
+
end
|
45
|
+
|
46
|
+
def _loop
|
47
|
+
while true do
|
48
|
+
x = IO::select([@stdout], [], [], @timeout)
|
49
|
+
raise BashTimeout.new if x.nil?
|
50
|
+
bytes = @stdout.sysread(1024)
|
51
|
+
$stderr.write("\nBASH IN: #{bytes}\n") if @debug
|
52
|
+
@buff << bytes
|
53
|
+
break if yield
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse(&block)
|
58
|
+
return self.instance_exec(&block)
|
59
|
+
end
|
60
|
+
|
61
|
+
def _nonce
|
62
|
+
randee = 4.times.map { rand().to_s }.join('|')
|
63
|
+
return Digest::SHA512.hexdigest(randee).to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
def _run(cmd, opts)
|
67
|
+
# it's a kind of magic
|
68
|
+
$stderr.write("\nBASH CMD: #{cmd}\n") if @debug
|
69
|
+
nonce = _nonce()
|
70
|
+
@stdin.write("#{cmd}; printf '%04d#{nonce}' $?\n")
|
71
|
+
@stdin.flush
|
72
|
+
_loop do
|
73
|
+
@buff.include?(nonce)
|
74
|
+
end
|
75
|
+
raise BashPaddingError.new if !@buff.end_with?(nonce)
|
76
|
+
output = @buff
|
77
|
+
@buff = ''
|
78
|
+
# treat the output
|
79
|
+
output.slice!(-nonce.length..-1)
|
80
|
+
status = output.slice!(-4..-1)
|
81
|
+
raise "Status #{status} > 255?" if status.slice(0..0) != '0'
|
82
|
+
return output, status.to_i
|
83
|
+
end
|
84
|
+
|
85
|
+
def _run_block(cmd, opts)
|
86
|
+
@stdin.write("#{cmd}; printf '%04d#{nonce}' $?\n")
|
87
|
+
end
|
88
|
+
|
89
|
+
def _extend(path, suffix)
|
90
|
+
path = path.chomp('/') if path.end_with?('/')
|
91
|
+
return path + suffix
|
92
|
+
end
|
93
|
+
|
94
|
+
def _unlines(s)
|
95
|
+
return s.lines.map { |l| l.chomp("\n") }
|
96
|
+
end
|
97
|
+
|
98
|
+
def _escape(args)
|
99
|
+
return args.map { |x| "'#{x}'" }.join(' ')
|
100
|
+
end
|
101
|
+
|
102
|
+
def export(name, value)
|
103
|
+
run("export #{name}=#{value}")
|
104
|
+
end
|
105
|
+
|
106
|
+
def run(cmd, opts = {})
|
107
|
+
out, status = _run(cmd, opts)
|
108
|
+
raise StatusError.new(cmd, status, out) if status != 0
|
109
|
+
return out
|
110
|
+
end
|
111
|
+
|
112
|
+
def run_status(cmd, opts = {})
|
113
|
+
out, status = _run(cmd, opts)
|
114
|
+
return status
|
115
|
+
end
|
116
|
+
|
117
|
+
def cd(path)
|
118
|
+
run("cd #{path}")
|
119
|
+
end
|
120
|
+
|
121
|
+
def ls
|
122
|
+
run("ls -1")
|
123
|
+
end
|
124
|
+
|
125
|
+
def sleep(t)
|
126
|
+
run("sleep #{t.to_i}")
|
127
|
+
end
|
128
|
+
|
129
|
+
def pwd
|
130
|
+
run("pwd").strip
|
131
|
+
end
|
132
|
+
|
133
|
+
def untar(name, where = nil)
|
134
|
+
if where.nil?
|
135
|
+
run("tar xvf #{name}")
|
136
|
+
else
|
137
|
+
run("tar -C #{where} -xvf #{name}")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def echo(text)
|
142
|
+
run("echo #{text}")
|
143
|
+
end
|
144
|
+
|
145
|
+
def bc(text)
|
146
|
+
run("echo '#{text}' | bc").strip
|
147
|
+
end
|
148
|
+
|
149
|
+
def cp(a, b)
|
150
|
+
run("cp #{a} #{b}")
|
151
|
+
end
|
152
|
+
|
153
|
+
def mv(a, b)
|
154
|
+
run("mv #{a} #{b}")
|
155
|
+
end
|
156
|
+
|
157
|
+
def rm(*args)
|
158
|
+
run("rm #{_escape(args)}")
|
159
|
+
end
|
160
|
+
|
161
|
+
def remove_dirs(path)
|
162
|
+
rm "-rf", path
|
163
|
+
end
|
164
|
+
|
165
|
+
def build
|
166
|
+
# builds a standard Unix software
|
167
|
+
run("./configure")
|
168
|
+
run("make")
|
169
|
+
end
|
170
|
+
|
171
|
+
def abspath(path)
|
172
|
+
run("readlink -f #{path}").strip
|
173
|
+
end
|
174
|
+
|
175
|
+
def build_tarball(tarball, path)
|
176
|
+
# builds a tarball containing a std Unix software
|
177
|
+
tarball = abspath(tarball)
|
178
|
+
path = abspath(path)
|
179
|
+
remove_dirs(path)
|
180
|
+
tmp = _extend(path, '-tmp')
|
181
|
+
remove_dirs(tmp)
|
182
|
+
make_dirs(tmp)
|
183
|
+
untar(tarball, tmp)
|
184
|
+
cd tmp
|
185
|
+
# we are in the temp dir
|
186
|
+
if exists('./configure')
|
187
|
+
cd '/'
|
188
|
+
mv tmp, path
|
189
|
+
else
|
190
|
+
ds = dirs()
|
191
|
+
raise 'Too many dirs?' if ds.length != 1
|
192
|
+
mv ds.first, path
|
193
|
+
cd '/'
|
194
|
+
remove_dirs(tmp)
|
195
|
+
end
|
196
|
+
cd path
|
197
|
+
build
|
198
|
+
end
|
199
|
+
|
200
|
+
def tmp_file
|
201
|
+
return run('mktemp').strip
|
202
|
+
end
|
203
|
+
|
204
|
+
def save_machines(machines, path = nil)
|
205
|
+
path = tmp_file if path.nil?
|
206
|
+
append_lines(path, machines.map { |m| m.to_s })
|
207
|
+
return path
|
208
|
+
end
|
209
|
+
|
210
|
+
def mpirun(machines, params)
|
211
|
+
machines = save_machines(machines) if machines.is_a?(Array)
|
212
|
+
return run("mpirun --mca btl ^openib -machinefile #{machines} #{params}")
|
213
|
+
end
|
214
|
+
|
215
|
+
def join(*args)
|
216
|
+
return File.join(*args)
|
217
|
+
end
|
218
|
+
|
219
|
+
def exists(path)
|
220
|
+
run_status("[[ -e #{path} ]]") == 0
|
221
|
+
end
|
222
|
+
|
223
|
+
def make_dirs(path)
|
224
|
+
run("mkdir -p #{path}")
|
225
|
+
end
|
226
|
+
|
227
|
+
def mkdir(path)
|
228
|
+
run("mkdir #{path}") unless exists(path)
|
229
|
+
end
|
230
|
+
|
231
|
+
def files(ignore = true, type = 'f')
|
232
|
+
fs = run("find . -maxdepth 1 -type #{type}")
|
233
|
+
fs = _unlines(fs).reject { |f| f == '.' }.map { |f| f[2..-1] }
|
234
|
+
fs = fs.reject { |f| f.end_with?('~') or f.start_with?('.') } if ignore
|
235
|
+
return fs
|
236
|
+
end
|
237
|
+
|
238
|
+
def which(prog)
|
239
|
+
return run("which #{prog}").strip
|
240
|
+
end
|
241
|
+
|
242
|
+
def dirs(ignore = true)
|
243
|
+
return files(ignore, 'd')
|
244
|
+
end
|
245
|
+
|
246
|
+
def get_type(name)
|
247
|
+
return :dir if run_status("[[ -d #{name} ]]") == 0
|
248
|
+
return :file if run_status("[[ -f #{name} ]]") == 0
|
249
|
+
raise "'#{name}' is neither file nor directory"
|
250
|
+
end
|
251
|
+
|
252
|
+
def expand_path(path)
|
253
|
+
return run("echo #{path}").strip
|
254
|
+
end
|
255
|
+
|
256
|
+
def append_line(path, line)
|
257
|
+
return run("echo '#{line}' >> #{path}")
|
258
|
+
end
|
259
|
+
|
260
|
+
def append_lines(path, lines)
|
261
|
+
lines.each { |line|
|
262
|
+
append_line(path, line)
|
263
|
+
}
|
264
|
+
end
|
265
|
+
|
266
|
+
def packages
|
267
|
+
list = run("dpkg -l")
|
268
|
+
list = _unlines(list).map do |p|
|
269
|
+
s, n, v = p.split
|
270
|
+
{ :status => s, :name => n, :version => v }
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def aptget(*args)
|
275
|
+
raise 'Command not given' if args.length == 0
|
276
|
+
cmd = args.first.to_sym
|
277
|
+
args = args.map { |x| x.to_s }.join(' ')
|
278
|
+
status = run_status("DEBIAN_FRONTEND=noninteractive apt-get -y #{args}")
|
279
|
+
if cmd == :purge and status == 100
|
280
|
+
return # ugly hack for the case when the package is not installed
|
281
|
+
end
|
282
|
+
raise StatusError.new('aptget', status, 'none') if status != 0
|
283
|
+
end
|
284
|
+
|
285
|
+
def contents(name)
|
286
|
+
run("cat #{name}")
|
287
|
+
end
|
288
|
+
|
289
|
+
alias cat contents
|
290
|
+
|
291
|
+
def hostname
|
292
|
+
return run('hostname').strip
|
293
|
+
end
|
294
|
+
|
295
|
+
def touch(path)
|
296
|
+
run("touch #{path}")
|
297
|
+
end
|
298
|
+
|
299
|
+
def trunc(path)
|
300
|
+
run("cat /dev/null > #{path}")
|
301
|
+
end
|
302
|
+
|
303
|
+
def distribute(path, dest, *nodes)
|
304
|
+
# distributes a file to the given nodes
|
305
|
+
nodes.flatten.each { |node|
|
306
|
+
run("scp -o 'StrictHostKeyChecking no' #{path} #{node}:#{dest}")
|
307
|
+
}
|
308
|
+
end
|
309
|
+
|
310
|
+
def glob(pattern)
|
311
|
+
out, status = _run("ls -1 #{pattern}", {})
|
312
|
+
return [] if status != 0
|
313
|
+
return out.strip.lines.map { |x| x.strip }
|
314
|
+
end
|
315
|
+
|
316
|
+
end
|
317
|
+
|
318
|
+
|
319
|
+
def self.bash(cmd = 'bash', opts = {}, &block)
|
320
|
+
if not block_given?
|
321
|
+
sin, sout, serr, thr = Open3.popen3(cmd)
|
322
|
+
return Bash.new(sin, sout, opts)
|
323
|
+
end
|
324
|
+
# run bash interpreter using this command
|
325
|
+
result = nil
|
326
|
+
Open3.popen3(cmd) do |sin, sout, serr, thr|
|
327
|
+
dsl = Bash.new(sin, sout, opts)
|
328
|
+
dsl.cd('~') # go to the home dir
|
329
|
+
result = dsl.parse(&block)
|
330
|
+
end
|
331
|
+
return result
|
332
|
+
end
|
333
|
+
|
334
|
+
end
|
335
|
+
|
336
|
+
if __FILE__ == $0
|
337
|
+
Bash.bash("ssh localhost bash") do
|
338
|
+
cd '/tmp'
|
339
|
+
puts glob("/tmp/*.gz").inspect
|
340
|
+
end
|
341
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# builds a one-file-bundle of xpflow
|
4
|
+
#
|
5
|
+
# TODO: it's not finished:
|
6
|
+
# * it won't bundle various external files (e.g., node templates)
|
7
|
+
# * __FILE__ usage should be standarized somehow to make everything work
|
8
|
+
#
|
9
|
+
# We roughly do the following (bundle method):
|
10
|
+
#
|
11
|
+
# - take all files from lib directory
|
12
|
+
# - discard some files that are not used
|
13
|
+
# - pull ../xpflow (entry point for the library) as well
|
14
|
+
# - remove "require" uses all over the place, checking if
|
15
|
+
# we know about them
|
16
|
+
# - then merge them all and wrap in an envelope that
|
17
|
+
# temporarily replaces $0 (so that inline code won't run)
|
18
|
+
# - for interested parties, we set $bundled = true
|
19
|
+
# - finally we append model.rb (TODO: it needs fixing)
|
20
|
+
# - the order of files *must* not matter!
|
21
|
+
#
|
22
|
+
# For executable method, we also:
|
23
|
+
#
|
24
|
+
# - pull main bin/xpflow script
|
25
|
+
# - replace a specially marked region with the result
|
26
|
+
# of bundle() method
|
27
|
+
#
|
28
|
+
# Therefore, a todo list:
|
29
|
+
# 1) Fix __FILE__ uses so that model.rb works
|
30
|
+
# 2) Bundle external files as well (pack them, serialize
|
31
|
+
# and then extract them on-the-fly?)
|
32
|
+
# 3) make everything more robust and less hackish
|
33
|
+
#
|
34
|
+
|
35
|
+
module XPFlow
|
36
|
+
|
37
|
+
class Bundler
|
38
|
+
|
39
|
+
def initialize
|
40
|
+
basic = [ '../colorado', 'utils', 'structs', 'scope', 'library' ]
|
41
|
+
discard = [ 'ensemble', 'ssh', 'bash' ]
|
42
|
+
files = basic + (__get_my_requires__.sort - basic - discard)
|
43
|
+
@here = File.dirname(__FILE__)
|
44
|
+
files += [ '../xpflow']
|
45
|
+
|
46
|
+
@files = files.map { |x| File.join(@here, "#{x}.rb") }
|
47
|
+
end
|
48
|
+
|
49
|
+
def bundle
|
50
|
+
ignored = [ "xpflow/exts/g5k", "colorado" ]
|
51
|
+
libs = [ "thread", "monitor", "pp", "digest", "open3", "tmpdir",
|
52
|
+
"fileutils", "monitor", "erb", "ostruct", "yaml", "shellwords",
|
53
|
+
"etc", "optparse", "pathname" ] # TODO: g5k should be pulled in
|
54
|
+
parts = []
|
55
|
+
puts "Bundling files:"
|
56
|
+
@files.each do |f|
|
57
|
+
puts " - #{f}"
|
58
|
+
contents = IO.read(f)
|
59
|
+
new_cont = contents.gsub(/require\s+(\S+)/) do |req|
|
60
|
+
lib = req.split("'")[1]
|
61
|
+
if (libs + ignored).include?(lib) or lib == "xpflow"
|
62
|
+
""
|
63
|
+
else
|
64
|
+
puts "Warning: untreated library (#{lib})"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
parts.push(new_cont)
|
68
|
+
end
|
69
|
+
|
70
|
+
s = parts.join("\n")
|
71
|
+
|
72
|
+
lib_lines = libs.map { |x| "require('#{x}')" }
|
73
|
+
model = File.join(@here, "exts", "model.rb")
|
74
|
+
|
75
|
+
final = ([ "# encoding: UTF-8" ] + lib_lines + [
|
76
|
+
"$tmp_0 = $0; $0 = 'faked_name'; $bundled = true",
|
77
|
+
s,
|
78
|
+
"$0 = $tmp_0",
|
79
|
+
IO.read(model)
|
80
|
+
]).join("\n")
|
81
|
+
|
82
|
+
return final
|
83
|
+
end
|
84
|
+
|
85
|
+
def executable
|
86
|
+
b = bundle()
|
87
|
+
main_file = File.join(@here, "..", "..", "bin", "xpflow")
|
88
|
+
contents = IO.read(main_file)
|
89
|
+
|
90
|
+
exp = Regexp.new("(\#XSTART.+\#XEND)", Regexp::MULTILINE)
|
91
|
+
main = contents.gsub(exp, b)
|
92
|
+
|
93
|
+
return main
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
if __FILE__ == $0
|
100
|
+
require 'xpflow'
|
101
|
+
|
102
|
+
bundle = ARGV.first
|
103
|
+
|
104
|
+
if bundle.nil?
|
105
|
+
raise "Please provide an output file."
|
106
|
+
end
|
107
|
+
|
108
|
+
bundler = XPFlow::Bundler.new
|
109
|
+
output = bundler.executable()
|
110
|
+
|
111
|
+
IO.write(bundle, output)
|
112
|
+
|
113
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
#
|
4
|
+
# Takes care of command line interface.
|
5
|
+
#
|
6
|
+
|
7
|
+
module XPFlow
|
8
|
+
|
9
|
+
class RunInfo
|
10
|
+
|
11
|
+
attr_reader :name
|
12
|
+
attr_reader :args
|
13
|
+
|
14
|
+
def initialize(name, *args)
|
15
|
+
@name = name
|
16
|
+
@args = args
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
class CmdlineError < Exception
|
22
|
+
|
23
|
+
def ignore?
|
24
|
+
return false
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
class UnknownCommandError < CmdlineError
|
30
|
+
|
31
|
+
def initialize(cmd)
|
32
|
+
super("unknown command `#{cmd}'")
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
class CmdlineNoFileError < CmdlineError
|
38
|
+
|
39
|
+
def initialize(path)
|
40
|
+
super("file `#{path}' does not exist")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class CmdlineIgnoreError < CmdlineError
|
45
|
+
|
46
|
+
def ignore?
|
47
|
+
return true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Options
|
52
|
+
|
53
|
+
attr_reader :args
|
54
|
+
attr_reader :config
|
55
|
+
attr_reader :includes
|
56
|
+
attr_reader :command
|
57
|
+
attr_reader :entry
|
58
|
+
|
59
|
+
def initialize(args)
|
60
|
+
@args = args.clone
|
61
|
+
@command = nil
|
62
|
+
@config = Options.defaults
|
63
|
+
@includes = []
|
64
|
+
@entry = nil
|
65
|
+
@verbose = false
|
66
|
+
parse
|
67
|
+
end
|
68
|
+
|
69
|
+
def verbose?
|
70
|
+
return @verbose
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.defaults
|
74
|
+
# Define a sane default configuration
|
75
|
+
{
|
76
|
+
:labels => [],
|
77
|
+
:ignore_checkpoints => false,
|
78
|
+
:checkpoint => nil,
|
79
|
+
:activity => nil,
|
80
|
+
:instead => [],
|
81
|
+
:after => [],
|
82
|
+
:vars => {}
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
def load_config(conffile)
|
87
|
+
if !File.exist?(conffile)
|
88
|
+
raise CmdlineNoFileError.new(conffile)
|
89
|
+
end
|
90
|
+
type = File.extname(conffile).downcase
|
91
|
+
contents = File.read(conffile)
|
92
|
+
|
93
|
+
data = case type
|
94
|
+
when '.json' then JSON.parse(contents)
|
95
|
+
when '.yaml' then YAML.load(contents)
|
96
|
+
else raise CmdlineError.new("unknown config file type (#{type})")
|
97
|
+
end
|
98
|
+
|
99
|
+
raise CmdlineError.new("bad config file format") unless data.is_a?(Hash)
|
100
|
+
|
101
|
+
return data
|
102
|
+
end
|
103
|
+
|
104
|
+
# Parses options from the command line
|
105
|
+
|
106
|
+
def parse
|
107
|
+
parser = OptionParser.new do |opts|
|
108
|
+
opts.banner = [
|
109
|
+
"Usage: #{$0} <command> [options] ARGUMENTS",
|
110
|
+
"",
|
111
|
+
"Available commands:",
|
112
|
+
"",
|
113
|
+
" * help -- show help",
|
114
|
+
" * run <files*> -- run a workflow from a file",
|
115
|
+
" * workflow <files*> -o <output> -- generate a workflow from a file",
|
116
|
+
"",
|
117
|
+
"Options:"
|
118
|
+
].join("\n")
|
119
|
+
|
120
|
+
opts.on("-v", "--verbose", "Run verbosely") do
|
121
|
+
@config[:labels] += [ :verbose ]
|
122
|
+
@verbose = true
|
123
|
+
end
|
124
|
+
opts.on("-q", "--quiet", "Run quietly") do
|
125
|
+
@config[:labels] = [ :normal ]
|
126
|
+
end
|
127
|
+
opts.on("-p", "--paranoiac", "Run paranoically") do
|
128
|
+
@config[:labels] += [ :verbose, :paranoic ]
|
129
|
+
end
|
130
|
+
opts.on("-l", "--labels LABELS", "Log messages labeled with LABELS") do |labels|
|
131
|
+
@config[:labels] += labels.split(',').map { |x| x.downcase.to_sym }
|
132
|
+
end
|
133
|
+
|
134
|
+
opts.on("-I", "--ignore-checkpoints", "Ignore automatically saved checkpoints") do
|
135
|
+
@config[:ignore_checkpoints] = true
|
136
|
+
end
|
137
|
+
# opts.on("-C", "--checkpoint NAME", "Jump to checkpoint NAME (if exists)") do |name|
|
138
|
+
# @config[:checkpoint] = name
|
139
|
+
# end
|
140
|
+
# opts.on("-c", "--list-checkpoints", "List available checkpoints") do
|
141
|
+
# @config[:instead] += [ RunInfo.new(:list_checkpoints) ]
|
142
|
+
# end
|
143
|
+
# opts.on("-i", "--info NAME", "Show information NAME") do |name|
|
144
|
+
# @config[:instead] += [ RunInfo.new(name.to_sym) ]
|
145
|
+
# end
|
146
|
+
# opts.on("-L", "--list", "List declared activities") do
|
147
|
+
# @config[:instead] += [ RunInfo.new(:activities) ]
|
148
|
+
# end
|
149
|
+
opts.on("-g", "--gantt", "Show Gantt diagram after execution") do
|
150
|
+
@config[:after] += [ RunInfo.new(:show_gantt) ]
|
151
|
+
end
|
152
|
+
opts.on("-G", "--save-gantt FILE", "Save Gantt diagram information to FILE") do |name|
|
153
|
+
@config[:after] += [ RunInfo.new(:save_gantt, name) ]
|
154
|
+
end
|
155
|
+
opts.on("-V", "--vars SPEC", "Set variables from the cmdline") do |spec|
|
156
|
+
pairs = spec.split(',').map { |x| x.split('=') }
|
157
|
+
unless pairs.all? { |x| x.length == 2 }
|
158
|
+
raise CmdlineError.new("Wrong vars syntax - <name>=<value> expected.")
|
159
|
+
end
|
160
|
+
@config[:vars].merge!(Hash[*pairs.flatten])
|
161
|
+
end
|
162
|
+
opts.on("-f", "--file FILE", "Set variables from a YAML file") do |file|
|
163
|
+
params = load_config(file)
|
164
|
+
@config[:vars].merge!(params)
|
165
|
+
end
|
166
|
+
opts.on("-o", "--output FILE", "Set output file for some commands") do |file|
|
167
|
+
@config[:output] = file
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
parser.parse!(@args)
|
172
|
+
|
173
|
+
@config[:labels] += [ :normal ]
|
174
|
+
@config[:labels].uniq!
|
175
|
+
|
176
|
+
@command = @args.first
|
177
|
+
@params = @args.tail
|
178
|
+
|
179
|
+
if command == "help" || command.nil?
|
180
|
+
show_usage(parser)
|
181
|
+
raise CmdlineIgnoreError.new
|
182
|
+
elsif command == "version"
|
183
|
+
raise CmdlineError.new("Unsupported.")
|
184
|
+
elsif command == "run"
|
185
|
+
load_files()
|
186
|
+
elsif command == "workflow"
|
187
|
+
if @config[:output].nil?
|
188
|
+
raise CmdlineError.new("output file must be provided with -o switch")
|
189
|
+
end
|
190
|
+
load_files()
|
191
|
+
else
|
192
|
+
# try to parse command as a filename
|
193
|
+
begin
|
194
|
+
@entry = parse_activity_spec(command)
|
195
|
+
rescue CmdlineNoFileError
|
196
|
+
raise UnknownCommandError.new(command)
|
197
|
+
end
|
198
|
+
@command = "run"
|
199
|
+
@params = @args
|
200
|
+
load_files()
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def load_files
|
205
|
+
# loads files from command line
|
206
|
+
if @params.length == 0
|
207
|
+
raise CmdlineError.new("at least one file must be given")
|
208
|
+
end
|
209
|
+
@entry = parse_activity_spec(@params.first)
|
210
|
+
@params.each do |spec|
|
211
|
+
x = parse_activity_spec(spec)
|
212
|
+
@includes.push(x.first)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def parse_activity_spec(path)
|
217
|
+
parts = path.split(":", 2)
|
218
|
+
activity = nil
|
219
|
+
if parts.length == 1
|
220
|
+
activity = "main" # by default use 'main' activity
|
221
|
+
else
|
222
|
+
path, activity = parts
|
223
|
+
end
|
224
|
+
if !File.exist?(path)
|
225
|
+
raise CmdlineNoFileError.new(path)
|
226
|
+
end
|
227
|
+
return [ path, activity ]
|
228
|
+
end
|
229
|
+
|
230
|
+
def vars
|
231
|
+
return @config[:vars]
|
232
|
+
end
|
233
|
+
|
234
|
+
def dispatch(obj)
|
235
|
+
if @command == "run"
|
236
|
+
return obj.execute_run(*@entry)
|
237
|
+
elsif @command == "workflow"
|
238
|
+
return obj.execute_workflow(*@entry)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def show_usage(parser)
|
243
|
+
Kernel.puts(parser.banner)
|
244
|
+
Kernel.puts(parser.summarize); Kernel.puts
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|