sfpagent 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sfpagent might be problematic. Click here for more details.
- data/.gitignore +2 -0
- data/README.md +22 -0
- data/bin/cert.rb +40 -0
- data/bin/sfpagent +76 -0
- data/lib/sfpagent/agent.rb +590 -0
- data/lib/sfpagent/executor.rb +207 -0
- data/lib/sfpagent/module.rb +34 -0
- data/lib/sfpagent/runtime.rb +246 -0
- data/lib/sfpagent.rb +14 -0
- data/sfpagent.gemspec +19 -0
- metadata +71 -0
data/.gitignore
ADDED
data/README.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
SFP Agent for Ruby
|
2
|
+
==================
|
3
|
+
- Author: Herry (herry13@gmail.com)
|
4
|
+
- Version: 0.0.1
|
5
|
+
- License: [BSD License](https://github.com/herry13/sfp-ruby/blob/master/LICENSE)
|
6
|
+
|
7
|
+
A gem that provides a Ruby interface to an SFP agent.
|
8
|
+
|
9
|
+
|
10
|
+
To install
|
11
|
+
----------
|
12
|
+
|
13
|
+
$ gem install sfpagent
|
14
|
+
|
15
|
+
|
16
|
+
Requirements
|
17
|
+
------------
|
18
|
+
- Ruby (>= 1.8.7)
|
19
|
+
- Rubygems
|
20
|
+
- sfp (>= 0.3.0)
|
21
|
+
- antlr3
|
22
|
+
- json
|
data/bin/cert.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'openssl'
|
5
|
+
|
6
|
+
key = OpenSSL::PKey::RSA.new(1024)
|
7
|
+
public_key = key.public_key
|
8
|
+
|
9
|
+
subject = "/C=BE/O=Test/OU=Test/CN=Test"
|
10
|
+
|
11
|
+
cert = OpenSSL::X509::Certificate.new
|
12
|
+
cert.subject = cert.issuer = OpenSSL::X509::Name.parse(subject)
|
13
|
+
cert.not_before = Time.now
|
14
|
+
cert.not_after = Time.now + 365 * 24 * 60 * 60
|
15
|
+
cert.public_key = public_key
|
16
|
+
cert.serial = 0x0
|
17
|
+
cert.version = 2
|
18
|
+
|
19
|
+
ef = OpenSSL::X509::ExtensionFactory.new
|
20
|
+
ef.subject_certificate = cert
|
21
|
+
ef.issuer_certificate = cert
|
22
|
+
cert.extensions = [
|
23
|
+
ef.create_extension("basicConstraints","CA:TRUE", true),
|
24
|
+
ef.create_extension("subjectKeyIdentifier", "hash"),
|
25
|
+
# ef.create_extension("keyUsage", "cRLSign,keyCertSign", true),
|
26
|
+
]
|
27
|
+
cert.add_extension ef.create_extension("authorityKeyIdentifier",
|
28
|
+
"keyid:always,issuer:always")
|
29
|
+
|
30
|
+
cert.sign key, OpenSSL::Digest::SHA1.new
|
31
|
+
|
32
|
+
if ARGV.length < 3
|
33
|
+
puts cert.to_pem
|
34
|
+
puts key.to_pem
|
35
|
+
puts public_key.to_pem
|
36
|
+
else
|
37
|
+
File.open(ARGV[0], 'w') { |f| f.write(cert.to_pem) }
|
38
|
+
File.open(ARGV[1], 'w') { |f| f.write(key.to_pem) }
|
39
|
+
File.open(ARGV[2], 'w') { |f| f.write(public_key.to_pem) }
|
40
|
+
end
|
data/bin/sfpagent
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
libdir = File.expand_path(File.dirname(__FILE__))
|
4
|
+
require "#{libdir}/../lib/sfpagent"
|
5
|
+
|
6
|
+
opts = Trollop::options do
|
7
|
+
version "sfpagent 0.0.1 (c) 2013 Herry"
|
8
|
+
banner <<-EOS
|
9
|
+
SFP Agent that provides a Ruby framework for managing system configurations. The configurations are modelled in SFP language.
|
10
|
+
|
11
|
+
Usage:
|
12
|
+
sfpagent [options] [model-file] [plan-file]
|
13
|
+
|
14
|
+
where [options] are:
|
15
|
+
EOS
|
16
|
+
|
17
|
+
opt :start, "Start the agent. If --daemon option is set true, then the agent will start as a daemon."
|
18
|
+
opt :stop, "Stop the daemon agent."
|
19
|
+
opt :status, "Print the status of the daemon agent."
|
20
|
+
opt :state, "Given a model, print the state of all modules. (Note: [model-file] should be specified.)"
|
21
|
+
opt :execute, "Given a model, execute a plan in given file. (Note: [model-file] should be specified.)"
|
22
|
+
opt :pretty, "Print the result in a pretty JSON format."
|
23
|
+
opt :port, "Port number of the daemon agent should listen to.", :default => Sfp::Agent::DefaultPort
|
24
|
+
opt :daemon, "Start the agent as a daemon.", :default => true
|
25
|
+
opt :ssl, "Set the agent to use HTTPS instead of HTTP.", :default => false
|
26
|
+
opt :certfile, "Certificate file for HTTPS.", :default => ''
|
27
|
+
opt :keyfile, "Private key file for HTTPS.", :default => ''
|
28
|
+
opt :modules_dir, "A directory that holds all SFP modules.", :default => ''
|
29
|
+
end
|
30
|
+
|
31
|
+
def parse(filepath)
|
32
|
+
home_dir = File.expand_path(File.dirname(filepath))
|
33
|
+
parser = Sfp::Parser.new({:home_dir => home_dir})
|
34
|
+
parser.parse(File.read(filepath))
|
35
|
+
parser
|
36
|
+
end
|
37
|
+
|
38
|
+
model_file = ARGV[0].to_s
|
39
|
+
plan_file = ARGV[1].to_s
|
40
|
+
|
41
|
+
if opts[:start]
|
42
|
+
Sfp::Agent.start(opts)
|
43
|
+
|
44
|
+
elsif opts[:stop]
|
45
|
+
Sfp::Agent.stop
|
46
|
+
|
47
|
+
elsif opts[:status]
|
48
|
+
Sfp::Agent.status
|
49
|
+
|
50
|
+
elsif opts[:state]
|
51
|
+
abort "[model-file] is not specified!\nUse \"sfpagent -h\" for more details.\n" if model_file == ''
|
52
|
+
abort "File #{model_file} is not exist!" if not File.exist?(model_file)
|
53
|
+
|
54
|
+
opts[:daemon] = false
|
55
|
+
opts = Sfp::Agent.check_config(opts)
|
56
|
+
Sfp::Agent.load_modules(opts)
|
57
|
+
state = Sfp::Runtime.new(parse(model_file)).get_state(true)
|
58
|
+
puts JSON.pretty_generate(state)
|
59
|
+
|
60
|
+
elsif opts[:execute]
|
61
|
+
abort "[model-file] is not specified!\nUse \"sfpagent -h\" for more details.\n" if model_file == ''
|
62
|
+
abort "[plan-file] is not specified!\nUse \"sfpagent -h\" for more details.\n" if plan_file == ''
|
63
|
+
abort "File #{model_file} is not exist!" if not File.exist?(model_file)
|
64
|
+
abort "File #{plan_file} is not exist!" if not File.exist?(plan_file)
|
65
|
+
|
66
|
+
opts[:daemon] = false
|
67
|
+
opts = Sfp::Agent.check_config(opts)
|
68
|
+
Sfp::Agent.load_modules(opts)
|
69
|
+
runtime = Sfp::Runtime.new(parse(model_file))
|
70
|
+
runtime.get_state
|
71
|
+
puts (runtime.execute_plan(File.read(plan_file)) ? "Success!" : "Failed!")
|
72
|
+
|
73
|
+
else
|
74
|
+
Trollop::help
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1,590 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'webrick'
|
3
|
+
require 'webrick/https'
|
4
|
+
require 'openssl'
|
5
|
+
require 'thread'
|
6
|
+
require 'uri'
|
7
|
+
require 'net/http'
|
8
|
+
require 'logger'
|
9
|
+
|
10
|
+
module Sfp
|
11
|
+
module Agent
|
12
|
+
if Process.euid == 0
|
13
|
+
CachedDir = '/var/sfpagent'
|
14
|
+
else
|
15
|
+
CachedDir = File.expand_path('~/.sfpagent')
|
16
|
+
end
|
17
|
+
Dir.mkdir(CachedDir, 0700) if not File.exist?(CachedDir)
|
18
|
+
|
19
|
+
DefaultPort = 1314
|
20
|
+
PIDFile = "#{CachedDir}/sfpagent.pid"
|
21
|
+
LogFile = "#{CachedDir}/sfpagent.log"
|
22
|
+
ModelFile = "#{CachedDir}/sfpagent.model"
|
23
|
+
AgentsDataFile = "#{CachedDir}/sfpagent.agents"
|
24
|
+
|
25
|
+
@@logger = WEBrick::Log.new(LogFile, WEBrick::BasicLog::INFO ||
|
26
|
+
WEBrick::BasicLog::ERROR ||
|
27
|
+
WEBrick::BasicLog::FATAL ||
|
28
|
+
WEBrick::BasicLog::WARN)
|
29
|
+
|
30
|
+
@@model_lock = Mutex.new
|
31
|
+
|
32
|
+
def self.logger
|
33
|
+
@@logger
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.check_config(p={})
|
37
|
+
# check modules directory, and create it if it's not exist
|
38
|
+
p[:modules_dir] = "#{CachedDir}/modules" if p[:modules_dir].to_s.strip == ''
|
39
|
+
p[:modules_dir] = File.expand_path(p[:modules_dir].to_s)
|
40
|
+
p[:modules_dir].chop! if p[:modules_dir][-1,1] == '/'
|
41
|
+
Dir.mkdir(p[:modules_dir], 0700) if not File.exists?(p[:modules_dir])
|
42
|
+
p
|
43
|
+
end
|
44
|
+
|
45
|
+
# Start the agent.
|
46
|
+
#
|
47
|
+
# options:
|
48
|
+
# :daemon => true if running as a daemon, false if as a normal application
|
49
|
+
# :port
|
50
|
+
# :ssl
|
51
|
+
# :certfile
|
52
|
+
# :keyfile
|
53
|
+
#
|
54
|
+
def self.start(p={})
|
55
|
+
begin
|
56
|
+
@@config = p = check_config(p)
|
57
|
+
|
58
|
+
server_type = (p[:daemon] ? WEBrick::Daemon : WEBrick::SimpleServer)
|
59
|
+
port = (p[:port] ? p[:port] : DefaultPort)
|
60
|
+
|
61
|
+
config = {:Host => '0.0.0.0', :Port => port, :ServerType => server_type,
|
62
|
+
:Logger => @@logger}
|
63
|
+
if p[:ssl]
|
64
|
+
config[:SSLEnable] = true
|
65
|
+
config[:SSLVerifyClient] = OpenSSL::SSL::VERIFY_NONE
|
66
|
+
config[:SSLCertificate] = OpenSSL::X509::Certificate.new(File.open(p[:certfile]).read)
|
67
|
+
config[:SSLPrivateKey] = OpenSSL::PKey::RSA.new(File.open(p[:keyfile]).read)
|
68
|
+
config[:SSLCertName] = [["CN", WEBrick::Utils::getservername]]
|
69
|
+
end
|
70
|
+
|
71
|
+
load_modules(p)
|
72
|
+
reload_model
|
73
|
+
|
74
|
+
server = WEBrick::HTTPServer.new(config)
|
75
|
+
server.mount("/", Sfp::Agent::Handler, @@logger)
|
76
|
+
|
77
|
+
fork {
|
78
|
+
begin
|
79
|
+
# send request to save PID
|
80
|
+
sleep 2
|
81
|
+
url = URI.parse("http://127.0.0.1:#{config[:Port]}/pid")
|
82
|
+
http = Net::HTTP.new(url.host, url.port)
|
83
|
+
if p[:ssl]
|
84
|
+
http.use_ssl = p[:ssl]
|
85
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
86
|
+
end
|
87
|
+
req = Net::HTTP::Get.new(url.path)
|
88
|
+
http.request(req)
|
89
|
+
puts "\nSFP Agent is running with PID #{File.read(PIDFile)}" if File.exist?(PIDFile)
|
90
|
+
rescue Exception => e
|
91
|
+
Sfp::Agent.logger.warn "Cannot request /pid #{e}"
|
92
|
+
end
|
93
|
+
}
|
94
|
+
|
95
|
+
trap('INT') { server.shutdown }
|
96
|
+
|
97
|
+
server.start
|
98
|
+
rescue Exception => e
|
99
|
+
@@logger.error "Starting the agent [Failed] #{e}"
|
100
|
+
raise e
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Stop the agent's daemon.
|
105
|
+
#
|
106
|
+
def self.stop
|
107
|
+
pid = (File.exist?(PIDFile) ? File.read(PIDFile).to_i : nil)
|
108
|
+
if not pid.nil? and `ps h #{pid}`.strip =~ /.*sfpagent.*/
|
109
|
+
print "Stopping SFP Agent with PID #{pid} "
|
110
|
+
Process.kill('KILL', pid)
|
111
|
+
puts "[OK]"
|
112
|
+
@@logger.info "SFP Agent daemon has been stopped."
|
113
|
+
else
|
114
|
+
puts "SFP Agent is not running."
|
115
|
+
end
|
116
|
+
File.delete(PIDFile) if File.exist?(PIDFile)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Print the status of the agent.
|
120
|
+
#
|
121
|
+
def self.status
|
122
|
+
pid = (File.exist?(PIDFile) ? File.read(PIDFile).to_i : nil)
|
123
|
+
if pid.nil?
|
124
|
+
puts "SFP Agent is not running."
|
125
|
+
else
|
126
|
+
if `ps hf #{pid}`.strip =~ /.*sfpagent.*/
|
127
|
+
puts "SFP Agent is running with PID #{pid}"
|
128
|
+
else
|
129
|
+
File.delete(PIDFile)
|
130
|
+
puts "SFP Agent is not running."
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Save given model to cached file, and then reload the model.
|
136
|
+
#
|
137
|
+
def self.set_model(model)
|
138
|
+
begin
|
139
|
+
@@model_lock.synchronize {
|
140
|
+
@@logger.info "Setting the model [Wait]"
|
141
|
+
File.open(ModelFile, 'w', 0600) { |f|
|
142
|
+
f.write(JSON.generate(model))
|
143
|
+
f.flush
|
144
|
+
}
|
145
|
+
}
|
146
|
+
reload_model
|
147
|
+
@@logger.info "Setting the model [OK]"
|
148
|
+
return true
|
149
|
+
rescue Exception => e
|
150
|
+
@@logger.error "Setting the model [Failed] #{e}"
|
151
|
+
end
|
152
|
+
false
|
153
|
+
end
|
154
|
+
|
155
|
+
# Return the model which is read from cached file.
|
156
|
+
#
|
157
|
+
def self.get_model
|
158
|
+
return nil if not File.exist?(ModelFile)
|
159
|
+
begin
|
160
|
+
@@model_lock.synchronize {
|
161
|
+
return JSON[File.read(ModelFile)]
|
162
|
+
}
|
163
|
+
rescue Exception => e
|
164
|
+
@@logger.error "Get the model [Failed] #{e}\n#{e.backtrace}"
|
165
|
+
end
|
166
|
+
false
|
167
|
+
end
|
168
|
+
|
169
|
+
# Reload the model from cached file.
|
170
|
+
#
|
171
|
+
def self.reload_model
|
172
|
+
model = get_model
|
173
|
+
if model.nil?
|
174
|
+
@@logger.info "There is no model in cache."
|
175
|
+
else
|
176
|
+
begin
|
177
|
+
@@runtime = Sfp::Runtime.new(model)
|
178
|
+
@@logger.info "Reloading the model in cache [OK]"
|
179
|
+
rescue Exception => e
|
180
|
+
@@logger.error "Reloading the model in cache [Failed] #{e}"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Return the current state of the model.
|
186
|
+
#
|
187
|
+
def self.get_state(as_sfp=true)
|
188
|
+
return nil if !defined? @@runtime or @@runtime.nil?
|
189
|
+
begin
|
190
|
+
return @@runtime.get_state(as_sfp)
|
191
|
+
rescue Exception => e
|
192
|
+
@@logger.error "Get state [Failed] #{e}\n#{e.backtrace.join("\n")}"
|
193
|
+
end
|
194
|
+
false
|
195
|
+
end
|
196
|
+
|
197
|
+
# Execute an action
|
198
|
+
#
|
199
|
+
# @param action contains the action's schema.
|
200
|
+
#
|
201
|
+
def self.execute_action(action)
|
202
|
+
logger = (@@config[:daemon] ? @@logger : Logger.new(STDOUT))
|
203
|
+
begin
|
204
|
+
result = @@runtime.execute_action(action)
|
205
|
+
logger.info "Executing #{action['name']} " + (result ? "[OK]" : "[Failed]")
|
206
|
+
return result
|
207
|
+
rescue Exception => e
|
208
|
+
logger.info "Executing #{action['name']} [Failed] #{e}\n#{e.backtrace.join("\n")}"
|
209
|
+
logger.error "#{e}\n#{e.bracktrace.join("\n")}"
|
210
|
+
end
|
211
|
+
false
|
212
|
+
end
|
213
|
+
|
214
|
+
# Load all modules in given directory.
|
215
|
+
#
|
216
|
+
# options:
|
217
|
+
# :dir => directory that holds all modules
|
218
|
+
#
|
219
|
+
def self.load_modules(p={})
|
220
|
+
dir = p[:modules_dir]
|
221
|
+
|
222
|
+
logger = @@logger # (p[:daemon] ? @@logger : Logger.new(STDOUT))
|
223
|
+
@@modules = []
|
224
|
+
counter = 0
|
225
|
+
if dir != '' and File.exist?(dir)
|
226
|
+
logger.info "Modules directory: #{dir}"
|
227
|
+
Dir.entries(dir).each { |name|
|
228
|
+
next if name == '.' or name == '..' or File.file?("#{dir}/#{name}")
|
229
|
+
module_file = "#{dir}/#{name}/#{name}.rb"
|
230
|
+
next if not File.exist?(module_file)
|
231
|
+
begin
|
232
|
+
load module_file #require module_file
|
233
|
+
logger.info "Loading module #{dir}/#{name} [OK]"
|
234
|
+
counter += 1
|
235
|
+
@@modules << name
|
236
|
+
rescue Exception => e
|
237
|
+
logger.warn "Loading module #{dir}/#{name} [Failed]\n#{e}"
|
238
|
+
end
|
239
|
+
}
|
240
|
+
end
|
241
|
+
logger.info "Successfully loading #{counter} modules."
|
242
|
+
end
|
243
|
+
|
244
|
+
def self.get_schemata(module_name)
|
245
|
+
dir = @@config[:modules_dir]
|
246
|
+
|
247
|
+
filepath = "#{dir}/#{module_name}/#{module_name}.sfp"
|
248
|
+
sfp = parse(filepath).root
|
249
|
+
sfp.accept(Sfp::Visitor::ParentEliminator.new)
|
250
|
+
JSON.generate(sfp)
|
251
|
+
end
|
252
|
+
|
253
|
+
def self.get_module_hash(name)
|
254
|
+
return nil if @@config[:modules_dir].to_s == ''
|
255
|
+
|
256
|
+
module_dir = "#{@@config[:modules_dir]}/#{name}"
|
257
|
+
if File.directory? module_dir
|
258
|
+
if `which md5sum`.strip.length > 0
|
259
|
+
return `find #{module_dir} -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | awk '{print $1}'`.strip
|
260
|
+
elsif `which md5`.strip.length > 0
|
261
|
+
return `find #{module_dir} -type f -exec md5 {} + | awk '{print $4}' | sort | md5`.strip
|
262
|
+
end
|
263
|
+
end
|
264
|
+
nil
|
265
|
+
end
|
266
|
+
|
267
|
+
def self.get_modules
|
268
|
+
return [] if not (defined? @@modules and @@modules.is_a? Array)
|
269
|
+
data = {}
|
270
|
+
@@modules.each { |m| data[m] = get_module_hash(m) }
|
271
|
+
data
|
272
|
+
#(defined?(@@modules) and @@modules.is_a?(Array) ? @@modules : [])
|
273
|
+
end
|
274
|
+
|
275
|
+
def self.uninstall_all_modules(p={})
|
276
|
+
return true if @@config[:modules_dir] == ''
|
277
|
+
if system("rm -rf #{@@config[:modules_dir]}/*")
|
278
|
+
load_modules(@@config)
|
279
|
+
@@logger.info "Deleting all modules [OK]"
|
280
|
+
return true
|
281
|
+
end
|
282
|
+
@@logger.info "Deleting all modules [Failed]"
|
283
|
+
false
|
284
|
+
end
|
285
|
+
|
286
|
+
def self.uninstall_module(name)
|
287
|
+
return false if @@config[:modules_dir] == ''
|
288
|
+
|
289
|
+
module_dir = "#{@@config[:modules_dir]}/#{name}"
|
290
|
+
if File.directory?(module_dir)
|
291
|
+
result = !!system("rm -rf #{module_dir}")
|
292
|
+
else
|
293
|
+
result = true
|
294
|
+
end
|
295
|
+
load_modules(@@config)
|
296
|
+
@@logger.info "Deleting module #{name} " + (result ? "[OK]" : "[Failed]")
|
297
|
+
result
|
298
|
+
end
|
299
|
+
|
300
|
+
def self.install_module(name, data)
|
301
|
+
return false if @@config[:modules_dir].to_s == ''
|
302
|
+
|
303
|
+
if !File.directory? @@config[:modules_dir]
|
304
|
+
File.delete @@config[:modules_dir] if File.exist? @@config[:modules_dir]
|
305
|
+
Dir.mkdir(@@config[:modules_dir], 0700)
|
306
|
+
end
|
307
|
+
|
308
|
+
# delete old files
|
309
|
+
module_dir = "#{@@config[:modules_dir]}/#{name}"
|
310
|
+
system("rm -rf #{module_dir}") if File.exist? module_dir
|
311
|
+
|
312
|
+
# save the archive
|
313
|
+
Dir.mkdir("#{module_dir}", 0700)
|
314
|
+
File.open("#{module_dir}/data.tgz", 'wb', 0600) { |f| f.syswrite data }
|
315
|
+
|
316
|
+
# extract the archive and the files
|
317
|
+
system("cd #{module_dir}; tar xvf data.tgz")
|
318
|
+
Dir.entries(module_dir).each { |name|
|
319
|
+
next if name == '.' or name == '..'
|
320
|
+
if File.directory? "#{module_dir}/#{name}"
|
321
|
+
system("cd #{module_dir}/#{name}; mv * ..; cd ..; rm -rf #{name}")
|
322
|
+
end
|
323
|
+
system("cd #{module_dir}; rm data.tgz")
|
324
|
+
}
|
325
|
+
load_modules(@@config)
|
326
|
+
@@logger.info "Installing module #{name} [OK]"
|
327
|
+
|
328
|
+
true
|
329
|
+
end
|
330
|
+
|
331
|
+
def self.get_log(n=0)
|
332
|
+
return '' if not File.exist?(LogFile)
|
333
|
+
if n <= 0
|
334
|
+
File.read(LogFile)
|
335
|
+
else
|
336
|
+
`tail -n #{n} #{LogFile}`
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def self.set_agents(agents)
|
341
|
+
File.open(AgentsDataFile, 'w', 0600) do |f|
|
342
|
+
raise Exception, "Invalid agents list." if not agents.is_a?(Hash)
|
343
|
+
buffer = {}
|
344
|
+
agents.each { |name,data|
|
345
|
+
raise Exception "Invalid agents list." if not data.is_a?(Hash) or
|
346
|
+
not data.has_key?('address') or data['address'].to_s.strip == '' or
|
347
|
+
not data.has_key?('port')
|
348
|
+
buffer[name] = {}
|
349
|
+
buffer[name]['address'] = data['address'].to_s
|
350
|
+
buffer[name]['port'] = data['port'].to_s.strip.to_i
|
351
|
+
buffer[name]['port'] = DefaultPort if buffer[name]['port'] == 0
|
352
|
+
}
|
353
|
+
f.write(JSON.generate(buffer))
|
354
|
+
f.flush
|
355
|
+
end
|
356
|
+
true
|
357
|
+
end
|
358
|
+
|
359
|
+
def self.get_agents
|
360
|
+
return {} if not File.exist?(AgentsDataFile)
|
361
|
+
JSON[File.read(AgentsDataFile)]
|
362
|
+
end
|
363
|
+
|
364
|
+
# A class that handles each request.
|
365
|
+
#
|
366
|
+
class Handler < WEBrick::HTTPServlet::AbstractServlet
|
367
|
+
def initialize(server, logger)
|
368
|
+
@logger = logger
|
369
|
+
end
|
370
|
+
|
371
|
+
# Process HTTP Get request
|
372
|
+
#
|
373
|
+
# uri:
|
374
|
+
# /pid => save daemon's PID to a file
|
375
|
+
# /state => return the current state
|
376
|
+
# /model => return the current model
|
377
|
+
# /schemata => return the schemata of a module
|
378
|
+
# /modules => return a list of available modules
|
379
|
+
#
|
380
|
+
def do_GET(request, response)
|
381
|
+
status = 400
|
382
|
+
content_type, body = ''
|
383
|
+
if not trusted(request.peeraddr[2])
|
384
|
+
status = 403
|
385
|
+
else
|
386
|
+
path = (request.path[-1,1] == '/' ? request.path.chop : request.path)
|
387
|
+
if path == '/pid' and (request.peeraddr[2] == 'localhost' or request.peeraddr[3] == '127.0.0.1')
|
388
|
+
status, content_type, body = save_pid
|
389
|
+
|
390
|
+
elsif path == '/state'
|
391
|
+
status, content_type, body = get_state
|
392
|
+
|
393
|
+
elsif path == '/sfpstate'
|
394
|
+
status, content_type, body = get_state({:as_sfp => true})
|
395
|
+
|
396
|
+
elsif path =~ /^\/state\/.+/
|
397
|
+
status, content_type, body = get_state({:path => path[7, path.length-7]})
|
398
|
+
|
399
|
+
elsif path =~ /^\/sfpstate\/.+/
|
400
|
+
status, content_type, body = get_state({:path => path[10, path.length-10]})
|
401
|
+
|
402
|
+
elsif path == '/model'
|
403
|
+
status, content_type, body = get_model
|
404
|
+
|
405
|
+
elsif path =~ /^\/schemata\/.+/
|
406
|
+
status, content_type, body = get_schemata({:module => path[10, path.length-10]})
|
407
|
+
|
408
|
+
elsif path == '/modules'
|
409
|
+
status, content_type, body = [200, 'application/json', JSON.generate(Sfp::Agent.get_modules)]
|
410
|
+
|
411
|
+
elsif path == '/log'
|
412
|
+
status, content_type, body = [200, 'text/plain', Sfp::Agent.get_log(100)]
|
413
|
+
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
response.status = status
|
418
|
+
response['Content-Type'] = content_type
|
419
|
+
response.body = body
|
420
|
+
end
|
421
|
+
|
422
|
+
# Handle HTTP Post request
|
423
|
+
#
|
424
|
+
# uri:
|
425
|
+
# /execute => receive an action's schema and execute it
|
426
|
+
#
|
427
|
+
def do_POST(request, response)
|
428
|
+
status = 400
|
429
|
+
content_type, body = ''
|
430
|
+
if not self.trusted(request.peeraddr[2])
|
431
|
+
status = 403
|
432
|
+
else
|
433
|
+
path = (request.path[-1,1] == '/' ? ryyequest.path.chop : request.path)
|
434
|
+
if path == '/execute'
|
435
|
+
status, content_type, body = self.execute({:query => request.query})
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
response.status = status
|
440
|
+
response['Content-Type'] = content_type
|
441
|
+
response.body = body
|
442
|
+
end
|
443
|
+
|
444
|
+
# uri:
|
445
|
+
# /model => receive a new model and save to cached file
|
446
|
+
# /modules => save the module if parameter "module" is provided
|
447
|
+
# delete the module if parameter "module" is not provided
|
448
|
+
# /agents => save the agents' list if parameter "agents" is provided
|
449
|
+
# delete all agents if parameter "agents" is not provided
|
450
|
+
def do_PUT(request, response)
|
451
|
+
status = 400
|
452
|
+
content_type, body = ''
|
453
|
+
if not self.trusted(request.peeraddr[2])
|
454
|
+
status = 403
|
455
|
+
else
|
456
|
+
path = (request.path[-1,1] == '/' ? ryyequest.path.chop : request.path)
|
457
|
+
|
458
|
+
if path == '/model'
|
459
|
+
status, content_type, body = self.set_model({:query => request.query})
|
460
|
+
|
461
|
+
elsif path =~ /\/module\/.+/
|
462
|
+
status, content_type, body = self.manage_modules({:name => path[8, path.length-8],
|
463
|
+
:query => request.query})
|
464
|
+
|
465
|
+
elsif path =~ /\/modules\/.+/
|
466
|
+
status, content_type, body = self.manage_modules({:name => path[9, path.length-9],
|
467
|
+
:query => request.query})
|
468
|
+
|
469
|
+
elsif path == '/modules'
|
470
|
+
status, content_type, body = self.manage_modules({:delete => true})
|
471
|
+
|
472
|
+
elsif path == '/agents'
|
473
|
+
status, content_type, body = self.manage_agents({:query => request.query})
|
474
|
+
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
response.status = status
|
479
|
+
response['Content-Type'] = content_type
|
480
|
+
response.body = body
|
481
|
+
end
|
482
|
+
|
483
|
+
def manage_agents(p={})
|
484
|
+
begin
|
485
|
+
if p[:query].has_key?('agents')
|
486
|
+
return [200, '', ''] if Sfp::Agent.set_agents(JSON[p[:query]['agents']])
|
487
|
+
else
|
488
|
+
return [200, '', ''] if Sfp::Agent.set_agents({})
|
489
|
+
end
|
490
|
+
rescue Exception => e
|
491
|
+
@logger.error "Saving agents list [Failed]\n#{e}\n#{e.backtrace.join("\n")}"
|
492
|
+
end
|
493
|
+
[500, '', '']
|
494
|
+
end
|
495
|
+
|
496
|
+
def manage_modules(p={})
|
497
|
+
if p[:delete]
|
498
|
+
return [200, '', ''] if Sfp::Agent.uninstall_all_modules
|
499
|
+
else
|
500
|
+
p[:name], _ = p[:name].split('/', 2)
|
501
|
+
if p[:query].has_key?('module')
|
502
|
+
return [200, '', ''] if Sfp::Agent.install_module(p[:name], p[:query]['module'])
|
503
|
+
else
|
504
|
+
return [200, '', ''] if Sfp::Agent.uninstall_module(p[:name])
|
505
|
+
end
|
506
|
+
end
|
507
|
+
[500, '', '']
|
508
|
+
end
|
509
|
+
|
510
|
+
def get_schemata(p={})
|
511
|
+
begin
|
512
|
+
module_name, _ = p[:module].split('/', 2)
|
513
|
+
return [200, 'application/json', Sfp::Agent.get_schemata(module_name)]
|
514
|
+
rescue Exception => e
|
515
|
+
@logger.error "Sending schemata [Failed]\n#{e}"
|
516
|
+
end
|
517
|
+
[500, '', '']
|
518
|
+
end
|
519
|
+
|
520
|
+
def get_state(p={})
|
521
|
+
state = Sfp::Agent.get_state(!!p[:as_sfp])
|
522
|
+
|
523
|
+
# The model is not exist.
|
524
|
+
return [404, 'text/plain', 'There is no model!'] if state.nil?
|
525
|
+
|
526
|
+
if !!state
|
527
|
+
state = state.at?("$." + p[:path].gsub(/\//, '.')) if !!p[:path]
|
528
|
+
return [200, 'application/json', JSON.generate({'state'=>state})]
|
529
|
+
end
|
530
|
+
|
531
|
+
# There is an error when retrieving the state of the model!
|
532
|
+
[500, '', '']
|
533
|
+
end
|
534
|
+
|
535
|
+
def set_model(p={})
|
536
|
+
if p[:query].has_key?('model')
|
537
|
+
# Setting the model was success, and then return '200' status.
|
538
|
+
return [200, '', ''] if Sfp::Agent.set_model(JSON[p[:query]['model']])
|
539
|
+
else
|
540
|
+
# Remove the existing model by setting an empty model
|
541
|
+
return [200, '', ''] if Sfp::Agent.set_model({})
|
542
|
+
end
|
543
|
+
|
544
|
+
# There is an error on setting the model!
|
545
|
+
[500, '', '']
|
546
|
+
end
|
547
|
+
|
548
|
+
def get_model
|
549
|
+
model = Sfp::Agent.get_model
|
550
|
+
|
551
|
+
# The model is not exist.
|
552
|
+
return [404, '', ''] if model.nil?
|
553
|
+
|
554
|
+
# The model is exist, and then send the model in JSON.
|
555
|
+
return [200, 'application/json', JSON.generate(model)] if !!model
|
556
|
+
|
557
|
+
# There is an error when retrieving the model!
|
558
|
+
[500, '', '']
|
559
|
+
end
|
560
|
+
|
561
|
+
def execute(p={})
|
562
|
+
return [400, '', ''] if not p[:query].has_key?('action')
|
563
|
+
begin
|
564
|
+
return [200, '', ''] if Sfp::Agent.execute_action(JSON[p[:query]['action']])
|
565
|
+
rescue
|
566
|
+
end
|
567
|
+
[500, '', '']
|
568
|
+
end
|
569
|
+
|
570
|
+
def save_pid
|
571
|
+
begin
|
572
|
+
File.open(PIDFile, 'w', 0644) { |f| f.write($$.to_s) }
|
573
|
+
return [200, '', $$.to_s]
|
574
|
+
rescue Exception
|
575
|
+
end
|
576
|
+
[500, '', '']
|
577
|
+
end
|
578
|
+
|
579
|
+
def trusted(address)
|
580
|
+
true
|
581
|
+
end
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
def self.require(gem, pack=nil)
|
586
|
+
::Kernel.require gem
|
587
|
+
rescue LoadError => e
|
588
|
+
::Kernel.require gem if system("gem install #{pack||gem} --no-ri --no-rdoc")
|
589
|
+
end
|
590
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'thread'
|
5
|
+
|
6
|
+
module Sfp
|
7
|
+
module Executor
|
8
|
+
class ExecutionException < Exception; end
|
9
|
+
class ParallelExecutionException < Exception; end
|
10
|
+
class SequentialExecutionException < Exception; end
|
11
|
+
|
12
|
+
# @param :plan the plan to be executed
|
13
|
+
# @param :owner an object that implement the action
|
14
|
+
# @param :retry number of retries (default: 2) when execution is failed
|
15
|
+
#
|
16
|
+
def execute_plan(params={})
|
17
|
+
if params[:plan].nil? or not params[:plan].is_a?(Hash)
|
18
|
+
raise ExecutionException, 'Plan is not available.'
|
19
|
+
elsif params[:plan]['type'].to_s == 'parallel' or
|
20
|
+
params[:plan][:type].to_s == 'parallel'
|
21
|
+
return self.execute_parallel_plan(params)
|
22
|
+
elsif params[:plan]['type'].to_s == 'sequential' or
|
23
|
+
params[:plan][:type].to_s == 'sequential'
|
24
|
+
return self.execute_sequential_plan(params)
|
25
|
+
else
|
26
|
+
raise ExecutionException, 'Unknown type of plan!'
|
27
|
+
end
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param :plan the plan to be executed
|
32
|
+
# @param :owner an object that implement the action
|
33
|
+
# @param :retry number of retries (default: 2) when execution is failed
|
34
|
+
#
|
35
|
+
def execute_parallel_plan(params={})
|
36
|
+
def assign_action_with_id(id)
|
37
|
+
thread_id = next_thread_id
|
38
|
+
action = @actions[id]
|
39
|
+
action[:executor] = thread_id
|
40
|
+
self.thread_execute_action(thread_id, action)
|
41
|
+
end
|
42
|
+
|
43
|
+
def next_thread_id
|
44
|
+
id = 0
|
45
|
+
@mutex.synchronize { @thread_id = id = @thread_id + 1 }
|
46
|
+
id
|
47
|
+
end
|
48
|
+
|
49
|
+
def action_to_string(action)
|
50
|
+
"#{action['id']}:#{action['name']}#{JSON.generate(action['parameters'])}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def thread_execute_action(tid, action)
|
54
|
+
t = Thread.new {
|
55
|
+
@mutex.synchronize { @threads << tid }
|
56
|
+
|
57
|
+
while not @failed and not action[:executed]
|
58
|
+
# execute the action
|
59
|
+
op_str = action_to_string(action)
|
60
|
+
#Nuri::Util.puts "[ExecutorThread: #{tid}] #{op_str}"
|
61
|
+
success = false
|
62
|
+
num = @retry
|
63
|
+
begin
|
64
|
+
success = @owner.execute_action { action }
|
65
|
+
num -= 1
|
66
|
+
end while not success and num > 0
|
67
|
+
|
68
|
+
# check if execution failed
|
69
|
+
if success
|
70
|
+
next_actions = []
|
71
|
+
@mutex.synchronize {
|
72
|
+
# set executed
|
73
|
+
action[:executed] = true
|
74
|
+
# select next action to be executed from all predecessor actions
|
75
|
+
# if each action has not been assigned to any thread yet
|
76
|
+
if action['successors'].length > 0
|
77
|
+
action['successors'].each { |id|
|
78
|
+
if @actions[id][:executor].nil?
|
79
|
+
predecessors_ok = true
|
80
|
+
@actions[id]['predecessors'].each { |pid|
|
81
|
+
predecessors_ok = (predecessors_ok and @actions[pid][:executed])
|
82
|
+
}
|
83
|
+
next_actions << id if predecessors_ok
|
84
|
+
end
|
85
|
+
}
|
86
|
+
end
|
87
|
+
next_actions.each { |id| @actions[id][:executor] = tid }
|
88
|
+
}
|
89
|
+
if next_actions.length > 0
|
90
|
+
# execute next actions
|
91
|
+
action = @actions[next_actions[0]]
|
92
|
+
if next_actions.length > 1
|
93
|
+
for i in 1..(next_actions.length-1)
|
94
|
+
assign_action_with_id(next_actions[i])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
else
|
100
|
+
Nuri::Util.error "Failed executing #{op_str}!"
|
101
|
+
@mutex.synchronize {
|
102
|
+
@failed = true # set global flag
|
103
|
+
@actions_failed << action
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
@mutex.synchronize { @threads.delete(tid) }
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
if params[:plan].nil? or not params[:plan].is_a?(Hash)
|
113
|
+
raise ParallelExecutionException, 'Plan is not available.'
|
114
|
+
elsif params[:plan]['type'].to_s == 'parallel' or
|
115
|
+
params[:plan][:type].to_s == 'parallel'
|
116
|
+
else
|
117
|
+
raise ParallelExecutionException, 'Not a parallel plan.'
|
118
|
+
end
|
119
|
+
|
120
|
+
@owner = params[:owner]
|
121
|
+
@retry = (params[:retry].nil? ? 2 : params[:retry].to_i)
|
122
|
+
|
123
|
+
@actions = params[:plan]['workflow']
|
124
|
+
@actions.sort! { |x,y| x['id'] <=> y['id'] }
|
125
|
+
@actions.each { |op| op[:executed] = false; op[:executor] = nil; }
|
126
|
+
|
127
|
+
@threads = []
|
128
|
+
@actions_failed = []
|
129
|
+
@mutex = Mutex.new
|
130
|
+
@failed = false
|
131
|
+
@thread_id = 0
|
132
|
+
|
133
|
+
params[:plan]['init'].each { |op_id| assign_action_with_id(op_id) }
|
134
|
+
|
135
|
+
begin
|
136
|
+
sleep 1
|
137
|
+
end while @threads.length > 0
|
138
|
+
|
139
|
+
Nuri::Util.log "Using #{@thread_id} threads in execution."
|
140
|
+
|
141
|
+
return (not @failed)
|
142
|
+
end
|
143
|
+
|
144
|
+
# @param :plan the plan to be executed
|
145
|
+
# @param :owner an object that implement the action
|
146
|
+
# @param :retry number of retries (default: 2) when execution is failed
|
147
|
+
#
|
148
|
+
def execute_sequential_plan(params={})
|
149
|
+
if params[:plan].nil? or not params[:plan].is_a?(Hash)
|
150
|
+
raise ParallelExecutionException, 'Plan is not available.'
|
151
|
+
elsif params[:plan]['type'].to_s == 'sequential' or
|
152
|
+
params[:plan][:type].to_s == 'sequential'
|
153
|
+
else
|
154
|
+
raise ParallelExecutionException, 'Not a parallel plan.'
|
155
|
+
end
|
156
|
+
|
157
|
+
@owner = params[:owner]
|
158
|
+
@retry = (params[:retry].nil? ? 2 : params[:retry].to_i)
|
159
|
+
params[:plan]['workflow'].each { |action|
|
160
|
+
success = false
|
161
|
+
num = @retry
|
162
|
+
begin
|
163
|
+
success, data = @owner.execute_action { action }
|
164
|
+
puts data.to_s if params[:print_output]
|
165
|
+
num -= 1
|
166
|
+
end while not success and num > 0
|
167
|
+
return false if not success
|
168
|
+
}
|
169
|
+
true
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
class RubyExecutor
|
174
|
+
def execute_plan(params={})
|
175
|
+
exec = Object.new
|
176
|
+
exec.extend(Sfp::Executor)
|
177
|
+
params[:owner] = self
|
178
|
+
exec.execute_plan(params)
|
179
|
+
end
|
180
|
+
|
181
|
+
def execute_action
|
182
|
+
# TODO
|
183
|
+
action = yield
|
184
|
+
puts "Exec: #{action.inspect}"
|
185
|
+
[true, nil]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
class BashExecutor < RubyExecutor
|
190
|
+
def execute_action
|
191
|
+
# TODO
|
192
|
+
action = yield
|
193
|
+
module_dir = (ENV.has_key?("SFP_HOME") ? ENV['SFP_HOME'] : ".")
|
194
|
+
script_path = "#{action['name'].sub!(/^\$\./, '')}"
|
195
|
+
script_path = "#{module_dir}/#{script_path.gsub!(/\./, '/')}"
|
196
|
+
cmd = "/bin/bash #{script_path}"
|
197
|
+
action['parameters'].each { |p| cmd += " '#{p}'" }
|
198
|
+
begin
|
199
|
+
data = `#{cmd}`
|
200
|
+
rescue Exception => exp
|
201
|
+
$stderr.puts "#{exp}\n#{exp.backtrace}"
|
202
|
+
[false, nil]
|
203
|
+
end
|
204
|
+
[true, data]
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'etc'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Sfp::Resource
|
5
|
+
attr_reader :state, :model
|
6
|
+
|
7
|
+
def init(model, default)
|
8
|
+
@model = {}
|
9
|
+
model.each { |k,v| @model[k] = v }
|
10
|
+
@state = {}
|
11
|
+
@default = {}
|
12
|
+
#default.each { |k,v| @state[k] = @default[k] = v }
|
13
|
+
end
|
14
|
+
|
15
|
+
def update_state
|
16
|
+
@state = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_model
|
20
|
+
@state = {}
|
21
|
+
@model.each { |k,v| @state[k] = v }
|
22
|
+
end
|
23
|
+
|
24
|
+
alias_method :reset, :to_model
|
25
|
+
|
26
|
+
protected
|
27
|
+
def exec_seq(*commands)
|
28
|
+
commands = [commands.to_s] if not commands.is_a?(Array)
|
29
|
+
commands.each { |c| raise Exception, "Cannot execute: #{c}" if !system(c) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module Sfp::Module
|
34
|
+
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
class Sfp::Runtime
|
2
|
+
def initialize(parser)
|
3
|
+
@parser = parser
|
4
|
+
@root = @parser.root
|
5
|
+
end
|
6
|
+
|
7
|
+
def execute_action(action)
|
8
|
+
def normalise_parameters(params)
|
9
|
+
p = {}
|
10
|
+
params.each { |k,v| p[k[2,k.length-2]] = v }
|
11
|
+
p
|
12
|
+
end
|
13
|
+
|
14
|
+
self.get_state if not defined? @modules
|
15
|
+
|
16
|
+
module_path, method_name = action['name'].pop_ref
|
17
|
+
mod = @modules.at?(module_path)[:_self]
|
18
|
+
raise Exception, "Module #{module_path} cannot be found!" if mod.nil?
|
19
|
+
raise Exception, "Cannot execute #{action['name']}!" if not mod.respond_to?(method_name)
|
20
|
+
|
21
|
+
params = normalise_parameters(action['parameters'])
|
22
|
+
mod.send method_name.to_sym, params
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_state(as_sfp=false)
|
26
|
+
def cleanup(value)
|
27
|
+
#value.accept(SfpState.new)
|
28
|
+
#value
|
29
|
+
value.select { |k,v| k[0,1] != '_' and !(v.is_a?(Hash) and v['_context'] != 'object') }
|
30
|
+
#value.keys.each { |k| value[k] = cleanup(value[k]) if value[k].is_a?(Hash) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Load the implementation of an object, and return its current state
|
34
|
+
# @param value a Hash
|
35
|
+
# @return a Hash which is the state of the object
|
36
|
+
#
|
37
|
+
def get_module_state(value, root, as_sfp=false)
|
38
|
+
# extract class name
|
39
|
+
class_name = value['_isa'].sub(/^\$\./, '') # [2, value['_isa'].length]
|
40
|
+
|
41
|
+
# throw an exception if schema's implementation is not exist!
|
42
|
+
raise Exception, "Implementation of schema #{class_name} is not available!" if
|
43
|
+
not Sfp::Module.const_defined?(class_name)
|
44
|
+
|
45
|
+
# create an instance of the schema
|
46
|
+
mod = Sfp::Module::const_get(class_name).new
|
47
|
+
default = cleanup(root.at?(value['_isa']))
|
48
|
+
model = cleanup(value)
|
49
|
+
mod.init(model, default)
|
50
|
+
|
51
|
+
# update and get state
|
52
|
+
mod.update_state
|
53
|
+
state = mod.state
|
54
|
+
|
55
|
+
# insert all hidden attributes, except "_parent"
|
56
|
+
value.each do |k,v|
|
57
|
+
state[k] = v if (k[0,1] == '_' and k != '_parent') or
|
58
|
+
(v.is_a?(Hash) and v['_context'] == 'procedure')
|
59
|
+
end if as_sfp
|
60
|
+
[mod, state]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return the state of an object
|
64
|
+
#
|
65
|
+
def get_object_state(value, root, as_sfp=false)
|
66
|
+
modules = {}
|
67
|
+
state = {}
|
68
|
+
if value['_context'] == 'object' and value['_isa'].to_s.isref
|
69
|
+
if value['_isa'] != '$.Object'
|
70
|
+
# if this value is an instance of a subclass of Object, then
|
71
|
+
# get the current state of this object
|
72
|
+
modules[:_self], state = get_module_state(value, root, as_sfp)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# get the state for each attributes which are not covered by this
|
77
|
+
# object's module
|
78
|
+
(value.keys - state.keys).each { |key|
|
79
|
+
next if key[0,1] == '_'
|
80
|
+
if value[key].is_a?(Hash)
|
81
|
+
modules[key], state[key] = get_object_state(value[key], root, as_sfp) if value[key]['_context'] == 'object'
|
82
|
+
else
|
83
|
+
state[key] = Sfp::Undefined.new
|
84
|
+
end
|
85
|
+
}
|
86
|
+
|
87
|
+
[modules, state]
|
88
|
+
end
|
89
|
+
|
90
|
+
root = Sfp::Helper.deep_clone(@root)
|
91
|
+
root.accept(Sfp::Visitor::ParentEliminator.new)
|
92
|
+
@modules, state = get_object_state(root, root, as_sfp)
|
93
|
+
|
94
|
+
state
|
95
|
+
end
|
96
|
+
|
97
|
+
def execute_plan(plan)
|
98
|
+
plan = JSON[plan]
|
99
|
+
if plan['type'] == 'sequential'
|
100
|
+
execute_sequential_plan(plan)
|
101
|
+
else
|
102
|
+
raise Exception, "Not implemented yet!"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
protected
|
107
|
+
class SfpState
|
108
|
+
def visit(name, value, parent)
|
109
|
+
parent.delete(name) if name[0,1] == '_' or
|
110
|
+
(value.is_a?(Hash) and value['_context'] != 'object')
|
111
|
+
true
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def execute_sequential_plan(plan)
|
116
|
+
puts 'Execute a sequential plan...'
|
117
|
+
|
118
|
+
plan['workflow'].each_index { |index|
|
119
|
+
action = plan['workflow'][index]
|
120
|
+
print "#{index+1}) #{action['name']} "
|
121
|
+
if not execute_action(action)
|
122
|
+
puts '[Failed]'
|
123
|
+
return false
|
124
|
+
end
|
125
|
+
puts '[OK]'
|
126
|
+
}
|
127
|
+
true
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
=begin
|
132
|
+
def execute(plan)
|
133
|
+
plan = JSON.parse(plan)
|
134
|
+
if plan['type'] == 'sequential'
|
135
|
+
execute_sequential(plan)
|
136
|
+
else
|
137
|
+
execute_parallel(plan)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
def execute_sequential(plan)
|
143
|
+
puts 'Execute a sequential plan...'
|
144
|
+
|
145
|
+
plan['workflow'].each_index { |index|
|
146
|
+
action = plan['workflow'][index]
|
147
|
+
print "#{index+1}) #{action['name']} "
|
148
|
+
|
149
|
+
module_path, method_name = action['name'].pop_ref
|
150
|
+
mod = @modules.at?(module_path)[:_self]
|
151
|
+
raise Exception, "Cannot execute #{action['name']}!" if not mod.respond_to?(method_name)
|
152
|
+
if not mod.send method_name.to_sym, normalise_parameters(action['parameters'])
|
153
|
+
puts '[Failed]'
|
154
|
+
return false
|
155
|
+
end
|
156
|
+
|
157
|
+
puts '[OK]'
|
158
|
+
}
|
159
|
+
true
|
160
|
+
end
|
161
|
+
|
162
|
+
def execute_parallel(plan)
|
163
|
+
# TODO
|
164
|
+
puts 'Execute a parallel plan...'
|
165
|
+
false
|
166
|
+
end
|
167
|
+
=end
|
168
|
+
|
169
|
+
=begin
|
170
|
+
def plan
|
171
|
+
# generate initial state
|
172
|
+
task = { 'initial' => Sfp::Helper.to_state('initial', self.get_state(true)) }
|
173
|
+
|
174
|
+
# add schemas
|
175
|
+
@root.each { |k,v|
|
176
|
+
next if !v.is_a?(Hash) or v['_context'] != 'class'
|
177
|
+
task[k] = v
|
178
|
+
}
|
179
|
+
|
180
|
+
# add goal constraint
|
181
|
+
model = @root.select { |k,v| v.is_a?(Hash) and v['_context'] == 'object' }
|
182
|
+
goalgen = Sfp::Helper::GoalGenerator.new
|
183
|
+
model.accept(goalgen)
|
184
|
+
task['goal'] = goalgen.results
|
185
|
+
|
186
|
+
# remove old parent links
|
187
|
+
task.accept(Sfp::Visitor::ParentEliminator.new)
|
188
|
+
|
189
|
+
# reconstruct Sfp parent links
|
190
|
+
task.accept(Sfp::Visitor::SfpGenerator.new(task))
|
191
|
+
|
192
|
+
# solve and return the plan solution
|
193
|
+
planner = Sfp::Planner.new
|
194
|
+
planner.solve({:sfp => task, :pretty_json => true})
|
195
|
+
end
|
196
|
+
=end
|
197
|
+
|
198
|
+
|
199
|
+
|
200
|
+
=begin
|
201
|
+
module Helper
|
202
|
+
def self.create_object(name)
|
203
|
+
{ '_self' => name, '_context' => 'object', '_isa' => '$.Object', '_classes' => ['$.Object'] }
|
204
|
+
end
|
205
|
+
|
206
|
+
def self.create_state(name)
|
207
|
+
{ '_self' => name, '_context' => 'state' }
|
208
|
+
end
|
209
|
+
|
210
|
+
def self.to_state(name, value)
|
211
|
+
raise Exception, 'Given value should be a Hash!' if not value.is_a?(Hash)
|
212
|
+
value['_self'] = name
|
213
|
+
value['_context'] = 'state'
|
214
|
+
value
|
215
|
+
end
|
216
|
+
|
217
|
+
module Constraint
|
218
|
+
def self.equals(value)
|
219
|
+
{ '_context' => 'constraint', '_type' => 'equals', '_value' => value }
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.and(name)
|
223
|
+
{ '_context' => 'constraint', '_type' => 'and', '_self' => name }
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
class GoalGenerator
|
228
|
+
attr_reader :results
|
229
|
+
|
230
|
+
def initialize
|
231
|
+
@results = Sfp::Helper::Constraint.and('goal')
|
232
|
+
end
|
233
|
+
|
234
|
+
def visit(name, value, parent)
|
235
|
+
return false if name[0,1] == '_'
|
236
|
+
if value.is_a?(Hash)
|
237
|
+
return true if value['_context'] == 'object'
|
238
|
+
return false
|
239
|
+
end
|
240
|
+
|
241
|
+
@results[ parent.ref.push(name) ] = Sfp::Helper::Constraint.equals(value)
|
242
|
+
false
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
=end
|
data/lib/sfpagent.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# external dependencies
|
2
|
+
require 'rubygems'
|
3
|
+
require 'json'
|
4
|
+
require 'sfp'
|
5
|
+
|
6
|
+
# internal dependencies
|
7
|
+
libdir = File.expand_path(File.dirname(__FILE__))
|
8
|
+
|
9
|
+
require libdir + '/sfpagent/executor.rb'
|
10
|
+
|
11
|
+
require libdir + '/sfpagent/runtime.rb'
|
12
|
+
require libdir + '/sfpagent/module.rb'
|
13
|
+
|
14
|
+
require libdir + '/sfpagent/agent.rb'
|
data/sfpagent.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'sfpagent'
|
3
|
+
s.version = '0.0.1'
|
4
|
+
s.date = '2013-07-03'
|
5
|
+
s.summary = 'SFP Agent'
|
6
|
+
s.description = 'A Ruby gem that provides a script of an SFP Agent.'
|
7
|
+
s.authors = ['Herry']
|
8
|
+
s.email = 'herry13@gmail.com'
|
9
|
+
|
10
|
+
s.executables << 'sfpagent'
|
11
|
+
s.files = `git ls-files`.split("\n").select { |n| !(n =~ /^(modules|test)\/.*/) }
|
12
|
+
|
13
|
+
s.require_paths = ['lib']
|
14
|
+
|
15
|
+
s.homepage = 'https://github.com/herry13/sfpagent'
|
16
|
+
s.rubyforge_project = 'sfpagent'
|
17
|
+
|
18
|
+
s.add_dependency 'sfp', '~> 0.3.0'
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sfpagent
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Herry
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-07-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: sfp
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.3.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.3.0
|
30
|
+
description: A Ruby gem that provides a script of an SFP Agent.
|
31
|
+
email: herry13@gmail.com
|
32
|
+
executables:
|
33
|
+
- sfpagent
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- README.md
|
39
|
+
- bin/cert.rb
|
40
|
+
- bin/sfpagent
|
41
|
+
- lib/sfpagent.rb
|
42
|
+
- lib/sfpagent/agent.rb
|
43
|
+
- lib/sfpagent/executor.rb
|
44
|
+
- lib/sfpagent/module.rb
|
45
|
+
- lib/sfpagent/runtime.rb
|
46
|
+
- sfpagent.gemspec
|
47
|
+
homepage: https://github.com/herry13/sfpagent
|
48
|
+
licenses: []
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ! '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubyforge_project: sfpagent
|
67
|
+
rubygems_version: 1.8.23
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: SFP Agent
|
71
|
+
test_files: []
|