nuri 0.5.2 → 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/VERSION +1 -1
- data/bin/nuri +110 -82
- data/bin/{install_module → nuri-install-module} +7 -4
- data/bin/{sfw2graph → nuri-sfwgraph} +7 -6
- data/bin/nuri-uninstall-module +45 -0
- data/examples/.gitignore +1 -0
- data/examples/mockcloud/generator.rb +14 -58
- data/examples/mockcloud/generator.rb.bak +66 -0
- data/examples/mockcloud/hadoop2.sfp.template +25 -0
- data/lib/nuri.rb +5 -7
- data/lib/nuri/choreographer.rb +8 -8
- data/lib/nuri/helper.rb +10 -3
- data/lib/nuri/master.rb +53 -15
- data/lib/nuri/orchestrator.rb +34 -29
- data/modules/apache2/apache2.rb +9 -0
- data/modules/apache2/apache2.sfp +54 -0
- data/modules/file/file.rb +1 -1
- data/modules/file/file.sfp +1 -1
- data/modules/machine/machine.rb +17 -0
- data/modules/package2/package2.rb +85 -0
- data/modules/package2/package2.sfp +23 -0
- data/modules/pyfile/main +133 -0
- data/modules/pyfile/pyfile.sfp +23 -0
- data/modules/service2/service2.rb +62 -0
- data/modules/service2/service2.sfp +24 -0
- data/nuri.gemspec +1 -1
- metadata +17 -11
- data/bin/delete_modules +0 -35
- data/bin/nuri.old +0 -183
- data/modules/install_module +0 -54
- data/modules/service/model.json +0 -1
- data/modules/service/test.sfp +0 -6
@@ -30,19 +30,22 @@ end
|
|
30
30
|
|
31
31
|
if $0 == __FILE__
|
32
32
|
if ARGV.length < 2
|
33
|
-
puts "Usage:
|
33
|
+
puts "Usage: #{$0.split('/').last} <address>:[port] <module-name>+
|
34
|
+
|
35
|
+
"
|
34
36
|
exit(1)
|
35
37
|
end
|
36
38
|
|
37
39
|
DefaultPort = 1314
|
38
40
|
|
39
|
-
address = ARGV.shift
|
40
|
-
port =
|
41
|
+
address, port = ARGV.shift.split(':', 2)
|
42
|
+
port = port.to_s.to_i
|
43
|
+
port = DefaultPort unless port > 0
|
41
44
|
modules = ARGV
|
42
45
|
missing = modules.select { |mod| not ::File.directory?(mod) }
|
43
46
|
modules = modules - missing
|
44
47
|
|
45
|
-
success = Nuri::Util.install_modules(address, port, modules)
|
48
|
+
success = Nuri::Util.install_modules(address, port, modules)
|
46
49
|
output = {
|
47
50
|
:status => success,
|
48
51
|
:installed_modules => modules,
|
@@ -3,10 +3,10 @@
|
|
3
3
|
require 'rubygems'
|
4
4
|
require 'json'
|
5
5
|
|
6
|
-
module
|
6
|
+
module Nuri
|
7
7
|
end
|
8
8
|
|
9
|
-
module
|
9
|
+
module Nuri::Graph
|
10
10
|
ActionColor = 'white'
|
11
11
|
ActionLabelWithParameters = false
|
12
12
|
|
@@ -140,7 +140,8 @@ end
|
|
140
140
|
|
141
141
|
def main
|
142
142
|
if ARGV.length < 1
|
143
|
-
puts "Convert SFP plan to
|
143
|
+
puts "Convert SFP plan to a PNG image graph using Graphviz library.
|
144
|
+
Usage: #{$0.split('/').last} <input-file> [output-file]\n\n"
|
144
145
|
exit
|
145
146
|
end
|
146
147
|
|
@@ -151,11 +152,11 @@ def main
|
|
151
152
|
dot = ""
|
152
153
|
case json["type"]
|
153
154
|
when 'partial-order', 'parallel'
|
154
|
-
dot =
|
155
|
+
dot = Nuri::Graph::partial2dot(json)
|
155
156
|
when 'sequential'
|
156
|
-
dot =
|
157
|
+
dot = Nuri::Graph::sequential2dot(json)
|
157
158
|
when 'stage'
|
158
|
-
dot =
|
159
|
+
dot = Nuri::Graph::stage2dot(json)
|
159
160
|
else
|
160
161
|
throw Exception, "Unrecognised type of workflow: #{json['type']}"
|
161
162
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'net/http'
|
5
|
+
|
6
|
+
module Nuri
|
7
|
+
module Util
|
8
|
+
def self.delete_module(address, port, modules, protocol="http")
|
9
|
+
def self.send_request(url)
|
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
|
+
if modules.length <= 0
|
17
|
+
send_request("#{protocol}://#{address}:#{port}/modules")
|
18
|
+
else
|
19
|
+
modules.each do |mod|
|
20
|
+
return false if not send_request("#{protocol}://#{address}:#{port}/modules/#{mod}")
|
21
|
+
end
|
22
|
+
true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
if $0 == __FILE__
|
29
|
+
DefaultPort = 1314
|
30
|
+
|
31
|
+
if ARGV.length < 1
|
32
|
+
puts "Usage: delete_modules <address>:[port] [module-name]*"
|
33
|
+
exit(1)
|
34
|
+
end
|
35
|
+
|
36
|
+
address, port = ARGV.shift.split(':', 2)
|
37
|
+
port = port.to_s.to_i
|
38
|
+
port = DefaultPort unless port > 0
|
39
|
+
|
40
|
+
if Nuri::Util.delete_module(address, port, ARGV)
|
41
|
+
puts '{"status":"ok"}'
|
42
|
+
else
|
43
|
+
puts '{"status":"failed"}'
|
44
|
+
end
|
45
|
+
end
|
data/examples/.gitignore
CHANGED
@@ -1,66 +1,22 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
include "../modules/mockcloud/mockcloud.sfp"
|
6
|
-
include "../modules/vm/vm.sfp"
|
7
|
-
include "../modules/apache/apache.sfp"
|
8
|
-
include "../modules/mysql/mysql.sfp"
|
9
|
-
include "../modules/wordpresscluster/wordpresscluster.sfp"
|
3
|
+
require 'erb'
|
4
|
+
require 'ostruct'
|
10
5
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
}
|
15
|
-
'
|
16
|
-
vmlb = 'vmlb isa VM {
|
17
|
-
apache isa Apache {
|
18
|
-
running is true
|
19
|
-
is_load_balancer is true
|
20
|
-
lb_members is (%members%)
|
21
|
-
}
|
22
|
-
}'
|
23
|
-
vmapp = '%vmapp% isa VM {
|
24
|
-
apache isa Apache {
|
25
|
-
running is true
|
26
|
-
}
|
27
|
-
wp_web isa WordpressWeb {
|
28
|
-
installed is true
|
29
|
-
http is %vmapp%.apache
|
30
|
-
database is vmdb.wp_db
|
31
|
-
}
|
32
|
-
}'
|
33
|
-
vmdb = 'vmdb isa VM {
|
34
|
-
mysql isa Mysql {
|
35
|
-
running is true
|
36
|
-
}
|
37
|
-
wp_db isa WordpressDB {
|
38
|
-
installed is true
|
39
|
-
mysql is vmdb.mysql
|
40
|
-
}
|
41
|
-
}'
|
42
|
-
output = ''
|
43
|
-
members = ''
|
44
|
-
global = "global {\n"
|
45
|
-
1.upto(number_apps) do |i|
|
46
|
-
name = "vmapp#{i}"
|
47
|
-
output += vmapp.gsub(/%vmapp%/, name) + "\n"
|
48
|
-
members += "#{name},"
|
49
|
-
global += "\tif vmlb.apache.running is true then #{name}.apache.running is true\n"
|
50
|
-
global += "\tif #{name}.apache.running is true then vmdb.mysql.running is true\n"
|
6
|
+
class ErbBinding < OpenStruct
|
7
|
+
def render(template)
|
8
|
+
ERB.new(template).result(binding)
|
51
9
|
end
|
52
|
-
global += "}\n"
|
53
|
-
output = header +
|
54
|
-
vmlb.sub(/%members%/, members.chop) + "\n" +
|
55
|
-
output + vmdb + "\n" + global
|
56
|
-
output
|
57
10
|
end
|
58
11
|
|
59
|
-
if
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
12
|
+
if ARGV.length <= 0
|
13
|
+
puts "Usage: generator.rb <template-file> [key=value]*"
|
14
|
+
else
|
15
|
+
template = File.read(ARGV.shift)
|
16
|
+
data = {}
|
17
|
+
ARGV.each do |arg|
|
18
|
+
key, value = arg.split('=')
|
19
|
+
data[key] = value
|
65
20
|
end
|
21
|
+
puts ErbBinding.new(data).render(template)
|
66
22
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
def generate(number_apps)
|
4
|
+
header = 'include "../modules/node/node.sfp"
|
5
|
+
include "../modules/mockcloud/mockcloud.sfp"
|
6
|
+
include "../modules/vm/vm.sfp"
|
7
|
+
include "../modules/apache/apache.sfp"
|
8
|
+
include "../modules/mysql/mysql.sfp"
|
9
|
+
include "../modules/wordpresscluster/wordpresscluster.sfp"
|
10
|
+
|
11
|
+
proxy isa Node {
|
12
|
+
sfpAddress is "localhost"
|
13
|
+
cloud isa MockCloud
|
14
|
+
}
|
15
|
+
'
|
16
|
+
vmlb = 'vmlb isa VM {
|
17
|
+
apache isa Apache {
|
18
|
+
running is true
|
19
|
+
is_load_balancer is true
|
20
|
+
lb_members is (%members%)
|
21
|
+
}
|
22
|
+
}'
|
23
|
+
vmapp = '%vmapp% isa VM {
|
24
|
+
apache isa Apache {
|
25
|
+
running is true
|
26
|
+
}
|
27
|
+
wp_web isa WordpressWeb {
|
28
|
+
installed is true
|
29
|
+
http is %vmapp%.apache
|
30
|
+
database is vmdb.wp_db
|
31
|
+
}
|
32
|
+
}'
|
33
|
+
vmdb = 'vmdb isa VM {
|
34
|
+
mysql isa Mysql {
|
35
|
+
running is true
|
36
|
+
}
|
37
|
+
wp_db isa WordpressDB {
|
38
|
+
installed is true
|
39
|
+
mysql is vmdb.mysql
|
40
|
+
}
|
41
|
+
}'
|
42
|
+
output = ''
|
43
|
+
members = ''
|
44
|
+
global = "global {\n"
|
45
|
+
1.upto(number_apps) do |i|
|
46
|
+
name = "vmapp#{i}"
|
47
|
+
output += vmapp.gsub(/%vmapp%/, name) + "\n"
|
48
|
+
members += "#{name},"
|
49
|
+
global += "\tif vmlb.apache.running is true then #{name}.apache.running is true\n"
|
50
|
+
global += "\tif #{name}.apache.running is true then vmdb.mysql.running is true\n"
|
51
|
+
end
|
52
|
+
global += "}\n"
|
53
|
+
output = header +
|
54
|
+
vmlb.sub(/%members%/, members.chop) + "\n" +
|
55
|
+
output + vmdb + "\n" + global
|
56
|
+
output
|
57
|
+
end
|
58
|
+
|
59
|
+
if $0 == __FILE__
|
60
|
+
if ARGV[0] == 'help'
|
61
|
+
puts "Usage: generator.rb [total-app-layer]"
|
62
|
+
else
|
63
|
+
number_apps = (ARGV.length > 0 ? ARGV[0].to_i : 3)
|
64
|
+
puts generate(number_apps)
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
/***
|
2
|
+
*
|
3
|
+
* Required parameters:
|
4
|
+
* - total : number of slaves
|
5
|
+
*
|
6
|
+
*/
|
7
|
+
|
8
|
+
include "modules/vm/vm.sfp"
|
9
|
+
include "modules/mockcloud/mockcloud.sfp"
|
10
|
+
include "modules/hadoop2/hadoop2.sfp"
|
11
|
+
proxy isa Node {
|
12
|
+
sfpAddress is "localhost"
|
13
|
+
cloud isa MockCloud
|
14
|
+
}
|
15
|
+
master isa VM {
|
16
|
+
hadoop isa Hadoop2Master
|
17
|
+
}
|
18
|
+
slave1 isa VM {
|
19
|
+
hadoop isa Hadoop2Slave {
|
20
|
+
master is master.hadoop
|
21
|
+
}
|
22
|
+
}
|
23
|
+
<% (2..total.to_i).each do |i| %>
|
24
|
+
slave<%= i %> extends slave1
|
25
|
+
<% end %>
|
data/lib/nuri.rb
CHANGED
@@ -53,10 +53,8 @@ module Nuri
|
|
53
53
|
end
|
54
54
|
|
55
55
|
# internal dependencies
|
56
|
-
libdir = File.
|
57
|
-
|
58
|
-
|
59
|
-
require libdir
|
60
|
-
|
61
|
-
#require libdir + '/nuri/server.rb'
|
62
|
-
require libdir + '/nuri/master.rb'
|
56
|
+
libdir = File.dirname(__FILE__) << '/nuri'
|
57
|
+
['constraint_helper.rb', 'helper.rb', 'orchestrator.rb',
|
58
|
+
'choreographer.rb', 'master.rb'].each do |file|
|
59
|
+
require "#{libdir}/#{file}"
|
60
|
+
end
|
data/lib/nuri/choreographer.rb
CHANGED
@@ -42,7 +42,7 @@ module Nuri::Choreographer
|
|
42
42
|
local_bsigs[name] = local
|
43
43
|
end
|
44
44
|
end
|
45
|
-
puts "Choreographing
|
45
|
+
puts "Choreographing " + format_benchmark(choreographing_time)
|
46
46
|
|
47
47
|
# bsig
|
48
48
|
local_bsigs
|
@@ -62,7 +62,7 @@ module Nuri::Choreographer
|
|
62
62
|
address = @model.at?("$.#{name}.sfpAddress")
|
63
63
|
port = @model.at?("$.#{name}.sfpPort")
|
64
64
|
if !address.is_a?(String) or address.length <= 0 or !port.is_a?(Fixnum) or port <= 0
|
65
|
-
puts "Agent #{name} is not exist!"
|
65
|
+
puts (p[:color] ? "[Warn]".yellow : "[Warn]") + " Agent #{name} is not exist!"
|
66
66
|
next
|
67
67
|
end
|
68
68
|
|
@@ -73,9 +73,9 @@ module Nuri::Choreographer
|
|
73
73
|
rescue
|
74
74
|
end
|
75
75
|
if code == '200'
|
76
|
-
puts "Deploying BSig model to #{name}@#{address}:#{port} [OK]".green
|
76
|
+
puts "Deploying BSig model to #{name}@#{address}:#{port} " + (p[:color] ? "[OK]".green : "[OK]")
|
77
77
|
else
|
78
|
-
$stderr.puts "Deploying BSig model to #{name}@#{address}:#{port} [Failed]".red
|
78
|
+
$stderr.puts "Deploying BSig model to #{name}@#{address}:#{port} " + (p[:color] ? "[Failed]".red : "[Failed]")
|
79
79
|
success = false
|
80
80
|
end
|
81
81
|
end
|
@@ -106,9 +106,9 @@ module Nuri::Choreographer
|
|
106
106
|
rescue
|
107
107
|
end
|
108
108
|
if code == '200'
|
109
|
-
puts "Sending model of #{name} to #{address}:#{port} [OK]".green
|
109
|
+
puts "Sending model of #{name} to #{address}:#{port} " + (p[:color] ? "[OK]".green : "[OK]")
|
110
110
|
else
|
111
|
-
$stderr.puts "Sending model of #{name} to #{address}:#{port} [Failed]".red
|
111
|
+
$stderr.puts "Sending model of #{name} to #{address}:#{port} " + (p[:color] ? "[Failed]".red : "[Failed]")
|
112
112
|
return false
|
113
113
|
end
|
114
114
|
end
|
@@ -139,9 +139,9 @@ module Nuri::Choreographer
|
|
139
139
|
rescue
|
140
140
|
end
|
141
141
|
if code1 == '200' and code2 == '200'
|
142
|
-
puts "Purging BSig model: #{name}@#{address}:#{port} [OK]".green
|
142
|
+
puts "Purging BSig model: #{name}@#{address}:#{port} " + (p[:color] ? "[OK]".green : "[OK]")
|
143
143
|
else
|
144
|
-
$stderr.puts "Purging BSig model: #{name}@#{address}:#{port} [Failed]".red
|
144
|
+
$stderr.puts "Purging BSig model: #{name}@#{address}:#{port} " + (p[:color] ? "[Failed]".red : "[Failed]")
|
145
145
|
success = false
|
146
146
|
end
|
147
147
|
end
|
data/lib/nuri/helper.rb
CHANGED
@@ -18,16 +18,23 @@ module Sfp::Helper
|
|
18
18
|
when 'null'
|
19
19
|
nil
|
20
20
|
when 'any_value'
|
21
|
-
|
21
|
+
isa = value['_isa']
|
22
|
+
if isa.is_a?(String) and isa.isref
|
23
|
+
'$.Any' + '.' + isa[2, isa.length-2]
|
24
|
+
else
|
25
|
+
'$.Any'
|
26
|
+
end
|
22
27
|
when 'set'
|
23
28
|
value['_values']
|
24
29
|
else
|
25
30
|
value
|
26
31
|
end
|
27
32
|
elsif value.is_a?(Sfp::Unknown)
|
28
|
-
|
33
|
+
t = value.type.to_s
|
34
|
+
"$.Unknown" + (t.length > 2 ? ".#{t[2, t.length-2]}" : "")
|
29
35
|
elsif value.is_a?(Sfp::Undefined)
|
30
|
-
|
36
|
+
t = value.type.to_s
|
37
|
+
"$.Undefined" + (t.length > 2 ? ".#{t[2, t.length-2]}" : "")
|
31
38
|
else
|
32
39
|
value
|
33
40
|
end
|
data/lib/nuri/master.rb
CHANGED
@@ -12,6 +12,8 @@ class Nuri::Master
|
|
12
12
|
CloudSchema = '$.Cloud'
|
13
13
|
VMSchema = '$.VM'
|
14
14
|
|
15
|
+
InstallModule = File.dirname(__FILE__) + '/../../bin/nuri-install-module'
|
16
|
+
|
15
17
|
attr_reader :model
|
16
18
|
|
17
19
|
def initialize(p={})
|
@@ -62,12 +64,16 @@ class Nuri::Master
|
|
62
64
|
p[:sfp] = create_plan_task(p)
|
63
65
|
p[:sas_post_processor] = SASPostProcessor
|
64
66
|
|
67
|
+
print "Planning "
|
68
|
+
|
65
69
|
plan = nil
|
66
70
|
planning_time = Benchmark.measure do
|
67
71
|
planner = Sfp::Planner.new
|
68
72
|
plan = planner.solve(p)
|
69
73
|
end
|
70
|
-
|
74
|
+
|
75
|
+
print (p[:color] ? "[Finish] ".green : "[Finish] ")
|
76
|
+
puts format_benchmark(planning_time)
|
71
77
|
|
72
78
|
plan
|
73
79
|
end
|
@@ -122,12 +128,23 @@ class Nuri::Master
|
|
122
128
|
end
|
123
129
|
|
124
130
|
protected
|
131
|
+
def format_benchmark(benchmark)
|
132
|
+
"cpu-time: user=#{benchmark.cutime.round(2)} sys=#{benchmark.cstime.round(2)} total=#{benchmark.total.round(2)}"
|
133
|
+
end
|
134
|
+
|
125
135
|
def create_plan_task(p={})
|
126
136
|
task = get_schemata
|
127
137
|
|
128
|
-
|
129
|
-
|
130
|
-
|
138
|
+
print "Getting current state "
|
139
|
+
puts (p[:color] ? "[Wait]".yellow : "[Wait]")
|
140
|
+
|
141
|
+
b = Benchmark.measure do
|
142
|
+
task['initial'] = to_state('initial', get_state(p))
|
143
|
+
end
|
144
|
+
|
145
|
+
print "Getting current state "
|
146
|
+
print (p[:color] ? "[OK]".green : "[OK]")
|
147
|
+
puts " " + format_benchmark(b)
|
131
148
|
|
132
149
|
task['initial'].accept(Sfp::Visitor::SfpGenerator.new(task))
|
133
150
|
f1 = Sfp::Helper::SfpFlatten.new
|
@@ -149,17 +166,24 @@ class Nuri::Master
|
|
149
166
|
dead_nodes.each_key { |name|
|
150
167
|
task['initial'].delete(name)
|
151
168
|
task['goal'].keep_if { |k,v| !(k =~ /(\$\.#{name}\.|\$\.#{name}$)/) }
|
152
|
-
|
169
|
+
print (p[:color] ? "[Warn]".red : "[Warn]")
|
170
|
+
puts " Removing node #{name} from the task."
|
153
171
|
}
|
154
172
|
|
155
173
|
# print the status of goal state
|
156
|
-
puts "Goal state:"
|
174
|
+
puts "Goal state:"
|
157
175
|
goalgen.results.each { |k,v|
|
158
176
|
next if k[0,1] == '_'
|
159
|
-
|
177
|
+
|
178
|
+
print " #{k}: "
|
179
|
+
value = Sfp::Helper::Sfp2Ruby.val(v['_value']).to_s
|
180
|
+
print (p[:color] ? value.green : value) + " "
|
181
|
+
|
160
182
|
if f1.results.has_key?(k) and f1.results[k] != v['_value']
|
161
|
-
|
183
|
+
value = Sfp::Helper::Sfp2Ruby.val(f1.results[k]).to_s
|
184
|
+
print (p[:color] ? value.red : value)
|
162
185
|
end
|
186
|
+
|
163
187
|
puts ""
|
164
188
|
}
|
165
189
|
|
@@ -310,6 +334,15 @@ class Nuri::Master
|
|
310
334
|
false
|
311
335
|
end
|
312
336
|
|
337
|
+
###############
|
338
|
+
#
|
339
|
+
# Push required modules to agent based on schemata available in agent's model
|
340
|
+
#
|
341
|
+
# @param agent_model agent's model
|
342
|
+
# @param address agent's address (IP or DNS address)
|
343
|
+
# @param port agent's port
|
344
|
+
#
|
345
|
+
###############
|
313
346
|
def push_modules(agent_model, address=nil, port=nil)
|
314
347
|
if address.nil? or port.nil?
|
315
348
|
return false if !agent_model.is_a?(Hash) or !agent_model['sfpAddress'].is_a?(String)
|
@@ -330,15 +363,20 @@ class Nuri::Master
|
|
330
363
|
raise Exception, "Unable to get modules list from #{name}" if code.to_i != 200
|
331
364
|
|
332
365
|
modules = JSON[body]
|
333
|
-
|
334
|
-
schemata.each
|
335
|
-
|
336
|
-
|
337
|
-
|
366
|
+
tobe_installed_modules = []
|
367
|
+
schemata.each do |name|
|
368
|
+
module_dir = "#{@modules_dir}/#{name}"
|
369
|
+
if File.exist?(module_dir) and
|
370
|
+
( not modules.has_key?(name) or modules[name] != get_local_module_hash(name).to_s )
|
371
|
+
tobe_installed_modules << name
|
372
|
+
end
|
373
|
+
end
|
338
374
|
|
339
|
-
return true if
|
375
|
+
return true if tobe_installed_modules.length <= 0
|
340
376
|
|
341
|
-
|
377
|
+
### install new modules and replace old ones
|
378
|
+
list = tobe_installed_modules.join(" ")
|
379
|
+
output = JSON.parse(`cd #{@modules_dir}; #{InstallModule} #{address}:#{port} #{list}`)
|
342
380
|
if output['installed_modules'].length > 0
|
343
381
|
puts ("Push modules: " + output['installed_modules'].join(" ") + " to agent #{name} [OK]").green
|
344
382
|
end
|