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 ADDED
@@ -0,0 +1,2 @@
1
+ *.gem
2
+ *.swp
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: []