nuri 0.5.1 → 0.5.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d02365a26a71b3202e77accdc43826f5da8c63ca
4
- data.tar.gz: 00b72bf869f9802a32fe1bc4a2f4b350ffb889d2
3
+ metadata.gz: 5713187406a70381abeffa8e382892172e1cab3a
4
+ data.tar.gz: 589c3291070bf16e6ee05e4846192ae3d0ddca81
5
5
  SHA512:
6
- metadata.gz: 63f4c79c68d7e624ec3ac944fdb7445edd8a78ee5370122c5133e4c24c2fe7d998ed01900ccd7a3c4bd0315ab33d4005473b8f13a8d0db0dae1c26ff028ed7fe
7
- data.tar.gz: ee932dd09a18574c7ec58c9f55ca21a2085bad5be3be735d90de26be4610e599106a40eb273168db60834647b7e308b035c619fd5ba0fef6056b94ecdd3d627e
6
+ metadata.gz: 5917ac035100aeb2e1dc75ff326034d9bbc7b5ef8a8679307581d389cca20dc8b9138bafddcdddccfbe9e6457184418eaa1ffb42ea6ddfd6c5fed6d8d30b8728
7
+ data.tar.gz: d14d4cfd96af587cdf02a4f622e1ad7773db83052eef68de3e304f1196313814aca4e0673d3069d721f662fba89db8158bef6a91b1b2f09eeb3e0ee13248c01a
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
- Nuri
1
+ [Nuri](http://herry13.github.io/nuri)
2
2
  ====
3
3
  - author: Herry [herry13@gmail.com]
4
4
  - license: BSD
5
5
 
6
+ [![Build Status](https://travis-ci.org/herry13/nuri.png?branch=master)](https://travis-ci.org/herry13/nuri)
6
7
  [![Gem Version](https://badge.fury.io/rb/nuri.png)](https://badge.fury.io/rb/nuri)
7
8
 
8
9
  Nuri is an automated workflow configuration tool. It allows us to define the desired state of
@@ -29,19 +30,19 @@ Requirements
29
30
  ------------
30
31
  - Ruby (>= 1.9.1)
31
32
  - Ruby Gems:
32
- - sfplanner (>= 0.1.1)
33
- - colorize (>= 0.5.8)
34
- - coderay (>= 1.0.9)
33
+ - sfplanner
34
+ - sfpagent
35
+ - colorize
36
+ - coderay
35
37
 
36
- Tested on: **Ubuntu 12.04**, **Debian Squeeze**
38
+ Tested on Linux: **Ubuntu 12.04**, **Debian Squeeze**, and **Fedora 13**
37
39
 
38
40
 
39
41
  To install
40
42
  ----------
41
43
 
42
- $ apt-get install git make gcc curl ruby1.9.1 ruby1.9.1-dev libz-dev libaugeas-ruby1.9.1 libxml2-dev libxslt-dev
43
- $ gem install sfplanner colorize coderay
44
- $ git clone https://github.com/herry13/nuri
44
+ $ apt-get install curl ruby1.9.1 ruby1.9.1-dev libz-dev libaugeas-ruby1.9.1 libxml2-dev libxslt-dev make gcc
45
+ $ gem install nuri
45
46
 
46
47
 
47
48
  Usage
@@ -49,16 +50,46 @@ Usage
49
50
  Assume that the model of your system is in file "model.sfp".
50
51
  - to get the current state
51
52
 
52
- $ ./bin/nuri -h -m model.sfp -s
53
+ $ nuri state -m model.sfp
53
54
 
54
55
  - to generate the plan
55
56
 
56
- $ ./bin/nuri -h -m model.sfp -p
57
+ $ nuri plan -m model.sfp
57
58
 
58
59
  press 'Y' and enter to execute the plan (if the plan exists).
59
60
 
60
61
  - to execute any generated plan
61
62
 
62
- $ ./bin/nuri -h -m model.sfp -a
63
+ $ nuri plan -m model.sfp -a
63
64
 
64
65
 
66
+ Console mode
67
+ ------------
68
+ We could enter Nuri console mode and do operations:
69
+ - enter console mode
70
+
71
+ $ nuri console
72
+
73
+ - edit the model which is stored in ~/.nuri/main.sfp
74
+
75
+ nuri@user> edit
76
+
77
+ - compile and print the model
78
+
79
+ nuri@user> model
80
+
81
+ - generate the current state of the system based on the model
82
+
83
+ nuri@user> state
84
+
85
+ - generate the plan
86
+
87
+ nuri@user> plan
88
+
89
+ - orchestrate the execution of any plan generated by Nuri
90
+
91
+ nuri@user> plan -a
92
+
93
+ - choreograph the execution of any plan generated by Nuri
94
+
95
+ nuri@user> bsig -a
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.1
1
+ 0.5.2
@@ -1,11 +1,35 @@
1
- #!/bin/bash
2
-
3
- if [[ "$1" == "" ]]
4
- then
5
- echo "Usage: delete_modules <address> [port]"
6
- elif [[ "$2" == "" ]]
7
- then
8
- curl -i -X PUT $1:1314/modules
9
- else
10
- curl -i -X PUT $1:$2/modules
11
- fi
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'uri'
4
+ require 'net/http'
5
+
6
+ module Nuri
7
+ module Util
8
+ def self.delete_modules(address, port, protocol="http")
9
+ url = "#{protocol}://#{address}:#{port}/modules"
10
+ uri = URI.parse(url)
11
+ http = Net::HTTP.new(uri.host, uri.port)
12
+ request = Net::HTTP::Delete.new(uri.request_uri)
13
+ response = http.request(request)
14
+ (response.code == '200')
15
+ end
16
+ end
17
+ end
18
+
19
+ if $0 == __FILE__
20
+ DefaultPort = 1314
21
+
22
+ if ARGV.length < 1
23
+ puts "Usage: delete_modules <address> [port]"
24
+ exit(1)
25
+ end
26
+
27
+ address = ARGV.shift
28
+ port = (ARGV[0].nil? ? DefaultPort : ARGV[0].to_i)
29
+
30
+ if Nuri::Util.delete_modules(address, port)
31
+ puts '{"status":"ok"}'
32
+ else
33
+ puts '{"status":"failed"}'
34
+ end
35
+ end
@@ -1,65 +1,54 @@
1
- #!/bin/bash
2
-
3
- DefaultPort=1314
4
-
5
- # verify the arguments
6
- if [[ "$1" == "" ]] || [[ "$2" == "" ]]; then
7
- echo "Usage: install_module <address> [port] <module-name> ..."
8
- exit 1
9
- fi
10
-
11
- # set the agent's address
12
- address=$1
13
- shift
14
-
15
- # set the agent's port number
16
- re='^[0-9]+$'
17
- if [[ $1 =~ $re ]]; then
18
- port=$1
19
- shift
20
- else
21
- port=$DefaultPort
22
- fi
23
-
24
- # set a template command for sending the modules
25
- cmd="curl -s -i -X PUT $address:$port/modules"
26
-
27
- # setup directory for temporary files
28
- dir="/tmp/"$(date +%s%N)"$RANDOM"
29
- mkdir -p $dir
30
-
31
- # for every module in the arguments:
32
- # - archive the module's files to a temporary file
33
- # - update the sending command by adding the module
34
- # if the module is not exist then the program will
35
- # set missing_module flag and then break from the loop
36
- missing_module=0
37
- for module in "$@" ; do
38
- if [[ -d "$module" ]]; then
39
- tar cvzhf $dir/$module.tgz $module 1>/dev/null
40
- cmd="$cmd -F $module=@$dir/$module.tgz"
41
- else
42
- echo "Module $module is not exist!"
43
- missing_module=`expr $missing_module + 1`
44
- fi
45
- done
46
-
47
- if [[ $missing_module == 0 ]]; then
48
- # execute the sending command there is no missing module
49
- result=`$cmd`
50
- re='.*HTTP/1\.1 200 OK.*'
51
- if ! [[ $result =~ $re ]]; then
52
- missing_module=`expr $missing_module + 1`
53
- fi
54
- fi
55
-
56
- # delete directory that holds temporary files
57
- rm -rf $dir
58
-
59
- if [[ $missing_module == 0 ]]; then
60
- echo "status: ok"
61
- else
62
- echo "status: failed"
63
- fi
64
-
65
- exit $missing_module
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'json'
4
+ require 'uri'
5
+ require 'net/http'
6
+ require File.dirname(__FILE__) + '/../lib/nuri/targz'
7
+
8
+ module Nuri
9
+ module Util
10
+ TarGzip = Object.new.extend(Util::Tar)
11
+
12
+ def self.install_modules(address, port, modules, protocol="http")
13
+ data = {}
14
+ modules.each do |module_name|
15
+ raise Exception, "Module #{module_name} is not exist!" if not ::File.directory?(module_name)
16
+ data[module_name] = TarGzip.targzip(module_name, module_name).read
17
+ end
18
+
19
+ url = "#{protocol}://#{address}:#{port}/modules"
20
+ uri = URI.parse(url)
21
+ http = Net::HTTP.new(uri.host, uri.port)
22
+ request = Net::HTTP::Put.new(uri.request_uri)
23
+ request.set_form_data(data)
24
+ response = http.request(request)
25
+
26
+ (response.code == '200')
27
+ end
28
+ end
29
+ end
30
+
31
+ if $0 == __FILE__
32
+ if ARGV.length < 2
33
+ puts "Usage: install_module <address> [port] <module-name> ..."
34
+ exit(1)
35
+ end
36
+
37
+ DefaultPort = 1314
38
+
39
+ address = ARGV.shift
40
+ port = (ARGV.length >= 2 ? ARGV.shift : DefaultPort)
41
+ modules = ARGV
42
+ missing = modules.select { |mod| not ::File.directory?(mod) }
43
+ modules = modules - missing
44
+
45
+ success = Nuri::Util.install_modules(address, port, modules),
46
+ output = {
47
+ :status => success,
48
+ :installed_modules => modules,
49
+ :missing_modules => missing
50
+ }
51
+ puts JSON.generate(output)
52
+
53
+ exit (success ? 0 : 1)
54
+ end
data/bin/nuri CHANGED
@@ -6,8 +6,6 @@ require "#{dir}/lib/nuri"
6
6
  Version = File.read(File.dirname(__FILE__) + "/../VERSION").strip
7
7
  About = "Nuri #{Version} (c) 2013"
8
8
 
9
- HTTP = Object.new.extend(Nuri::Net::Helper)
10
-
11
9
  Nuri.init
12
10
 
13
11
  def console(*args)
@@ -15,8 +13,9 @@ def console(*args)
15
13
  $stdout.puts(args)
16
14
  end
17
15
 
18
- class Sfp::Console
19
- PrettyStateGenerator = Sfp::Visitor::PrettyStateGenerator.new
16
+ class Nuri::Console
17
+ include Nuri::Helper
18
+
20
19
  ParentEliminator = Sfp::Visitor::ParentEliminator.new
21
20
 
22
21
  def process_args(args, parser)
@@ -77,6 +76,7 @@ EOS
77
76
  opt :plain, 'print output in plain JSON'
78
77
  opt :no_interactive, 'disable interactive input'
79
78
  opt :no_push_module, 'disable automatic push module'
79
+ opt :image, 'image graph of the generated plan', :default => Dir.home + '/.nuri/plan.png'
80
80
  end
81
81
  help, args = check_help(args)
82
82
  opts = process_args args, parser
@@ -91,6 +91,13 @@ EOS
91
91
  plan = master.get_plan(opts)
92
92
  if plan.is_a?(Hash) and !plan['workflow'].nil?
93
93
  if plan['workflow'].length > 0
94
+ plan_file = Dir.home + '/.nuri/plan.json'
95
+ File.open(plan_file, 'w') { |f| f.write(JSON.generate(plan)) }
96
+
97
+ if opts[:image]
98
+ system "#{File.dirname(__FILE__)}/sfw2graph #{plan_file} #{opts[:image]}"
99
+ end
100
+
94
101
  if opts[:plain]
95
102
  puts JSON.generate(plan)
96
103
  else
@@ -120,6 +127,7 @@ EOS
120
127
  puts "Execution failed!".red
121
128
  end
122
129
  end
130
+
123
131
  elsif plan['workflow'].length == 0
124
132
  puts (opts[:plain] ? 'Goal state has been achieved.' : 'Goal state has been achieved.').green
125
133
  end
@@ -212,7 +220,7 @@ where <subcommand> is:
212
220
  model get current model
213
221
  bsig get current Behavioural Signature model
214
222
  exec <action> execute given action description
215
- <action> = <path> [param1=value1 param2=value2 ...]
223
+ <action> := <path> [param1=value1 param2=value2 ...]
216
224
  module get modules list
217
225
  log get last 100 lines of logs
218
226
  list list of available agents from model file
@@ -224,6 +232,7 @@ EOS
224
232
  opt :port, "port", :default => 1314
225
233
  opt :ssh_user, "SSH username", :short => '-u', :default => 'root'
226
234
  opt :ssh_port, "SSH port", :short => '-p', :default => '22'
235
+ opt :raw, "print a RAW output"
227
236
  end
228
237
  help, args = check_help(args)
229
238
  subcommand = (args.length > 0 ? args.shift : '')
@@ -237,7 +246,7 @@ EOS
237
246
 
238
247
  if help
239
248
  parser.educate(STDOUT)
240
- elsif `which sfpagent`.strip.length > 0
249
+ elsif `which sfpagent`.strip.length > 0 or ssh_opt.length > 0
241
250
  case subcommand
242
251
  when 'install'
243
252
  system("#{ssh_opt} sudo gem install sfpagent --no-ri --no-rdoc")
@@ -259,32 +268,52 @@ EOS
259
268
  system("#{ssh_opt} sudo gem update sfpagent --no-ri --no-rdoc")
260
269
 
261
270
  when 'state'
262
- code, state = HTTP.get_data(opts[:address], 1314, "/state")
271
+ code, state = get_data(opts[:address], 1314, "/state")
263
272
  if code == '200' and state =~ /{.*}/
264
273
  state = JSON[state]['state']
265
- state.keys.each { |key| state.delete(key) if state[key]['_context'] != 'object' }
266
- puts CodeRay.encode(JSON.pretty_generate(state), :json, :terminal)
274
+ if not opts[:raw]
275
+ state.keys.each { |key| state.delete(key) if state[key]['_context'] != 'object' }
276
+ state.accept(Sfp::Helper::Sfp2Ruby)
277
+ end
278
+ puts CodeRay.encode(YAML.dump(state), :yaml, :terminal)
267
279
  else
268
280
  puts state
269
281
  end
270
282
 
271
283
  when 'model'
272
- code, model = HTTP.get_data(opts[:address], 1314, "/model")
284
+ code, model = get_data(opts[:address], 1314, "/model")
273
285
  if code == '200' and model =~ /{.*}/
274
286
  model = JSON[model]
275
- model.keys.each { |key| model.delete(key) if model[key]['_context'] != 'object' }
276
- puts CodeRay.encode(JSON.pretty_generate(model), :json, :terminal)
287
+ if not opts[:raw]
288
+ model.select! { |k,v| k[0,1] != '_' and (not v.is_a?(Hash) or v['_context'] == 'object') }
289
+ model.accept(Sfp::Helper::Sfp2Ruby)
290
+ end
291
+ puts CodeRay.encode(YAML.dump(model), :yaml, :terminal)
292
+ elsif code == '404'
293
+ puts "The model of desired state is not exist!"
277
294
  else
278
- puts model
295
+ $stderr.puts "Error: #{code}"
279
296
  end
280
297
 
281
298
  when 'bsig'
282
- code, json = HTTP.get_data(opts[:address], 1314, "/bsig")
283
- puts (code == '200' and json.length >= 2 ? CodeRay.encode(JSON.pretty_generate(JSON[json]), :json, :terminal) : json)
299
+ code, json = get_data(opts[:address], 1314, "/bsig")
300
+ if code == '200' and json.length > 0
301
+ puts CodeRay.encode(JSON.pretty_generate(JSON[json]), :json, :terminal)
302
+ elsif code == '404'
303
+ puts 'Behavioural Signature model is not exist!'
304
+ else
305
+ $stderr.puts "Error: #{code}"
306
+ end
284
307
 
285
308
  when 'module'
286
- code, json = HTTP.get_data(opts[:address], 1314, "/bsig")
287
- puts (code == '200' and json.length >= 2 ? CodeRay.encode(JSON.pretty_generate(JSON[json]), :json, :terminal) : json)
309
+ code, json = get_data(opts[:address], 1314, "/modules")
310
+ if code == '200' and json.length >= 2
311
+ puts CodeRay.encode(YAML.dump(JSON[json]), :yaml, :terminal)
312
+ elsif code == '404'
313
+ puts 'No module is available!'
314
+ else
315
+ $stderr.puts "Error: #{code}"
316
+ end
288
317
 
289
318
  when 'exec', 'execute'
290
319
  if args.length > 0
@@ -298,7 +327,7 @@ EOS
298
327
  end
299
328
  end
300
329
  data = { 'action' => JSON.generate({ 'name' => name, 'parameters' => parameters }) }
301
- code, _ = HTTP.post_data(opts[:address], 1314, "/execute", data)
330
+ code, _ = post_data(opts[:address], 1314, "/execute", data)
302
331
  puts (code == '200' ? "Executing #{name} [OK]".green : "Executing #{name} [Failed]".red)
303
332
  else
304
333
  $stderr.puts 'Invalid parameters (usage: agent exec <action-path> [action-arguments]).'
@@ -497,23 +526,4 @@ EOS
497
526
  end
498
527
  end
499
528
 
500
- module Sfp::Helper
501
- Sfp2Ruby = Object.new
502
- def Sfp2Ruby.visit(name, value, parent)
503
- if name[0] == '_'
504
- parent.delete(name)
505
- elsif value.is_a?(Hash)
506
- case value['_context']
507
- when 'null'
508
- parent[name] = nil
509
- when 'any_value', 'constraint', 'procedure'
510
- parent.delete(name)
511
- when 'set'
512
- parent[name] = value['_values']
513
- end
514
- end
515
- true
516
- end
517
- end
518
-
519
- Sfp::Console.new.run if $0 == __FILE__
529
+ Nuri::Console.new.run if $0 == __FILE__
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'json'
5
+
6
+ module Sfp
7
+ end
8
+
9
+ module Sfp::Graph
10
+ ActionColor = 'white'
11
+ ActionLabelWithParameters = false
12
+
13
+ def self.dot2image(dot, image_file)
14
+ dot_file = "/tmp/#{Time.now.getutc.to_i}.dot"
15
+ File.open(dot_file, 'w') { |f|
16
+ f.write(dot)
17
+ f.flush
18
+ }
19
+ !!system("dot -Tpng -o #{image_file} #{dot_file}")
20
+ ensure
21
+ File.delete(dot_file) if File.exist?(dot_file)
22
+ end
23
+
24
+ def self.clean(value)
25
+ return value[2, value.length-2] if value[0,2] == '$.'
26
+ return value
27
+ end
28
+
29
+ def self.get_label(action, withparameters=true)
30
+ label = clean(action["name"])
31
+ if withparameters and ActionLabelWithParameters
32
+ label += "("
33
+ if action["parameters"].length > 0
34
+ action["parameters"].each { |key,value|
35
+ label += "#{clean(key)}=#{clean(value.to_s)},"
36
+ }
37
+ label.chop!
38
+ end
39
+ label += ')'
40
+ end
41
+ return label
42
+ end
43
+
44
+ def self.partial2dot(json)
45
+ dot = "digraph {\n"
46
+
47
+ dot += "init_state [label=\"\", shape=doublecircle, fixedsize=true, width=0.35];\n"
48
+ dot += "final_state [label=\"\", shape=doublecircle, style=filled, fillcolor=black, fixedsize=true, width=0.35];\n"
49
+ last_actions = Hash.new
50
+ json["workflow"].each { |action|
51
+ dot += "#{action["id"]}[label=\"#{get_label(action)}\", shape=rect, style=filled, fillcolor=#{ActionColor}];\n"
52
+ last_actions[action["id"].to_s] = action
53
+ }
54
+
55
+ json["workflow"].each { |action|
56
+ has_predecessor = false
57
+ action["predecessors"].each { |prevId|
58
+ dot += "#{prevId} -> #{action["id"]};\n"
59
+ has_predecessor = true
60
+ last_actions.delete(prevId.to_s)
61
+ }
62
+ if not has_predecessor
63
+ dot += "init_state -> #{action["id"]};\n"
64
+ end
65
+ }
66
+
67
+ last_actions.each { |id,action|
68
+ dot += "#{id} -> final_state;\n"
69
+ }
70
+
71
+ dot += "}"
72
+
73
+ return dot
74
+ end
75
+
76
+ def self.stage2dot(json)
77
+ dot = "digraph {\n"
78
+
79
+ dot += "init_state [label=\"\", shape=doublecircle, fixedsize=true, width=0.35];\n"
80
+ dot += "final_state [label=\"\", shape=doublecircle, style=filled, fillcolor=black, fixedsize=true, width=0.35];\n"
81
+ index = 0
82
+ prevState = "init_state"
83
+ json["workflow"].each { |stage|
84
+ id = 0
85
+ stage.each { |action|
86
+ dot += "a" + index.to_s + "a" + id.to_s + '[label="' + get_label(action) + '", shape=rect]' + ";\n"
87
+ dot += prevState + " -> a" + index.to_s + "a" + id.to_s + ";\n"
88
+ id += 1
89
+ }
90
+ if index < json["workflow"].length-1
91
+ dot += "state" + index.to_s + ' [label="", shape=circle, fixedsize=true, width=0.35]' + ";\n"
92
+ prevState = "state" + index.to_s
93
+ id = 0
94
+ stage.each { |action|
95
+ dot += "a" + index.to_s + "a" + id.to_s + " -> " + prevState + ";\n"
96
+ id += 1
97
+ }
98
+ else
99
+ id = 0
100
+ stage.each { |action|
101
+ dot += "a" + index.to_s + "a" + id.to_s + " -> final_state;\n"
102
+ id += 1
103
+ }
104
+ end
105
+ index += 1
106
+ }
107
+ dot += "}"
108
+ return dot
109
+ end
110
+
111
+ def self.sequential2dot(json)
112
+ dot = "digraph {\n"
113
+
114
+ dot += "init_state [label=\"\", shape=doublecircle, fixedsize=true, width=0.35];\n"
115
+ dot += "final_state [label=\"\", shape=doublecircle, style=filled, fillcolor=black, fixedsize=true, width=0.35];\n"
116
+ id = 0
117
+ json["workflow"].each { |action|
118
+ dot += id.to_s + '[label="' + get_label(action) + '", shape=rect]' + ";\n"
119
+ id += 1
120
+ }
121
+
122
+ id = 0
123
+ prevActionId = nil
124
+ json["workflow"].each { |action|
125
+ if id == 0
126
+ dot += "init_state -> " + id.to_s + ";\n"
127
+ elsif id == json["workflow"].length-1
128
+ dot += id.to_s + " -> final_state;\n"
129
+ end
130
+ if prevActionId != nil
131
+ dot += prevActionId.to_s + " -> " + id.to_s + ";\n"
132
+ end
133
+ prevActionId = id
134
+ id += 1
135
+ }
136
+ dot += "}"
137
+ return dot
138
+ end
139
+ end
140
+
141
+ def main
142
+ if ARGV.length < 1
143
+ puts "Convert SFP plan to an image graph with Graphviz (dot).\n\nUsage: sfw2dot.rb <input-file> [output-file]\n\n"
144
+ exit
145
+ end
146
+
147
+ fp = open(ARGV[0], "rb")
148
+ json = JSON.parse(fp.read)
149
+ fp.close
150
+
151
+ dot = ""
152
+ case json["type"]
153
+ when 'partial-order', 'parallel'
154
+ dot = Sfp::Graph::partial2dot(json)
155
+ when 'sequential'
156
+ dot = Sfp::Graph::sequential2dot(json)
157
+ when 'stage'
158
+ dot = Sfp::Graph::stage2dot(json)
159
+ else
160
+ throw Exception, "Unrecognised type of workflow: #{json['type']}"
161
+ end
162
+
163
+ outfile = "/tmp/#{Time.now.to_f}.dot"
164
+ fp = open(outfile, "w");
165
+ fp.write(dot)
166
+ fp.close
167
+
168
+ cmd = 'dot -Tpng -o';
169
+ if ARGV.length > 1
170
+ cmd += "#{ARGV[1]} #{outfile}"
171
+ else
172
+ cmd += ARGV[0].sub(/\.[a-zA-Z]*/,'') + ".png < #{outfile}"
173
+ end
174
+ system(cmd)
175
+ File.delete(outfile)
176
+ end
177
+
178
+ main if __FILE__ == $0