nuri 0.5.2 → 0.5.3
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 +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
|