#!/usr/bin/env ruby dir = File.expand_path('../../', __FILE__) require "#{dir}/lib/nuri" Version = File.read(File.dirname(__FILE__) + "/../VERSION").strip About = "Nuri #{Version} (c) 2013" HTTP = Object.new.extend(Nuri::Net::Helper) Nuri.init def console(*args) print Time.now.strftime('%Y-%m-%d %H:%M:%S '.yellow) $stdout.puts(args) end class Sfp::Console PrettyStateGenerator = Sfp::Visitor::PrettyStateGenerator.new ParentEliminator = Sfp::Visitor::ParentEliminator.new def process_args(args, parser) Trollop::with_standard_exception_handling parser do parser.parse args end end def check_help(args) help = args.count { |x| x == '-h' or x == '--help' } [(help > 0), args.select { |x| x != '-h' and x != '--help' }] end def do_state(args=ARGV, cmd="nuri ") parser = Trollop::Parser.new do banner <<-EOS Usage: #{cmd}state [options] where [options] are: EOS opt :model_file, 'file contains a model of desired state', :default => Nuri.main, :short => '-m' opt :json, 'print output in JSON' opt :plain, 'print output in plain text' opt :no_push_module, 'disable automatic push module' end help, args = check_help(args) opts = process_args args, parser if help parser.educate(STDOUT) elsif File.exist?(opts[:model_file]) opts[:push_modules] = true if !opts[:no_push_module] master = Nuri::Master.new master.set_model opts state = master.get_state opts state.accept(Sfp::Helper::Sfp2Ruby) if opts[:json] puts (opts[:plain] ? JSON.generate(state) : CodeRay.encode(JSON.pretty_generate(state), :json, :terminal)) else puts (opts[:plain] ? YAML.dump(state) : CodeRay.encode(YAML.dump(state), :yaml, :terminal)) end else $stderr.puts "Model file '#{opts[:model_file]}' is not exist! Use \"-h\" option for more details.".red end end def do_plan(args=ARGV, cmd="nuri ") parser = Trollop::Parser.new do banner <<-EOS Usage: #{cmd}plan [options] where [options] are: EOS opt :model_file, 'file contains a model of desired state', :default => Nuri.main, :short => '-m' opt :parallel, 'generate a parallel plan', :short => '-l' opt :apply, 'generate and execute a plan', :short => '-a' opt :plain, 'print output in plain JSON' opt :no_interactive, 'disable interactive input' opt :no_push_module, 'disable automatic push module' end help, args = check_help(args) opts = process_args args, parser if help parser.educate(STDOUT) elsif File.exist?(opts[:model_file]) opts[:plan] = true opts[:push_modules] = true if !opts[:no_push_module] master = Nuri::Master.new master.set_model(opts) plan = master.get_plan(opts) if plan.is_a?(Hash) and !plan['workflow'].nil? if plan['workflow'].length > 0 if opts[:plain] puts JSON.generate(plan) else actions = plan['workflow'] if plan['type'] == 'sequential' p = 'Sequential Plan:'.yellow actions.each_index { |i| p += "\n#{i+1}. #{actions[i]['name']} #{JSON.generate(actions[i]['parameters'])}" } puts p else p = 'Partial-Order Plan:'.yellow actions.each { |op| p += "\n#{op['id']+1}. #{op['name']} #{JSON.generate(op['parameters'])} (#{op['predecessors'].map{|i|i+1}}, #{op['successors'].map{|i|i+1}})" } puts p end end if not opts[:apply] and not opts[:no_interactive] print "Execute the plan [y/N]? " opts[:apply] = true if STDIN.gets.chomp.upcase == 'Y' end if opts[:apply] puts 'Executing the plan: '.yellow opts[:plan] = plan if master.execute_plan(opts) puts "Execution success!".green else puts "Execution failed!".red end end elsif plan['workflow'].length == 0 puts (opts[:plain] ? 'Goal state has been achieved.' : 'Goal state has been achieved.').green end else $stderr.puts (opts[:plain] ? "No solution!".yellow : "No solution!".red) end else $stderr.puts "Model file '#{opts[:model_file]}' is not exist! Use \"-h\" option for more details.".red end end def do_bsig(args=ARGV, cmd="nuri ") parser = Trollop::Parser.new do banner <<-EOS Usage: #{cmd}bsig [options] where [options] are: EOS opt :model_file, 'file contains a model of desired state', :default => Nuri.main, :short => '-m' opt :purge, 'purge existing BSig model', :short => '-g' opt :deploy, 'generate and then deploy a BSig model' opt :plain, 'print output in plain JSON' opt :no_interactive, 'disable interactive input' end help, args = check_help(args) opts = process_args args, parser if help parser.educate(STDOUT) elsif File.exist?(opts[:model_file]) opts[:bsig_deploy] = true opts[:parallel] = true master = Nuri::Master.new master.set_model(opts) if opts[:purge] if master.purge_bsig(opts) puts "Purging Behavioural Signature model [OK]".green else puts "Purging Behavioural Signature model [Failed]".red end else bsig = master.get_bsig(opts) if bsig.is_a?(Hash) and bsig.length > 0 empty_local_bsig = bsig.select { |name,local_bsig| local_bsig['operators'].length <= 0 } if empty_local_bsig.length != bsig.length if opts[:plain] puts JSON.generate(bsig) else puts CodeRay.encode(JSON.pretty_generate(bsig), :json, :terminal) end if not opts[:no_interactive] and not opts[:deploy] print "Deploy the BSig model [y/N]? " opts[:deploy] = true if STDIN.gets.chomp.upcase == 'Y' end if opts[:deploy] puts 'Deploying the BSig model: '.yellow opts[:bsig] = bsig if master.deploy_bsig(opts) puts "Deployment success!".green else puts "Deployment failed!".red end end else puts (opts[:plain] ? 'Goal state has been achieved.' : 'Goal state has been achieved.').green end else $stderr.puts (opts[:plain] ? "No solution!".yellow : "No solution!".red) end end else $stderr.puts "Model file '#{opts[:model_file]}' is not exist! Use \"-h\" option for more details.".red end end def do_agent(args=ARGV, cmd="nuri ") parser = Trollop::Parser.new do banner <<-EOS Usage: #{cmd}agent [node] [options] where is: install install agent (use SSH for remote node) upgrade upgrade agent (use SSH for remote node) start start the agent (use SSH for remote node) restart restart the agent (use SSH for remode node) stop stop the agent (use SSH for remote node) status get status (use SSH for remote node) state get current state model get current model bsig get current Behavioural Signature model exec execute given action description = [param1=value1 param2=value2 ...] module get modules list log get last 100 lines of logs list list of available agents from model file where [options] are: EOS opt :model_file, 'file contains a model of desired state', :default => Nuri.main, :short => '-m' opt :address, "address", :default => 'localhost' opt :port, "port", :default => 1314 opt :ssh_user, "SSH username", :short => '-u', :default => 'root' opt :ssh_port, "SSH port", :short => '-p', :default => '22' end help, args = check_help(args) subcommand = (args.length > 0 ? args.shift : '') opts = process_args args, parser ssh_opt = '' if opts[:address] != 'localhost' ssh_opt = "ssh " ssh_opt += (opts[:ssh_user] ? "#{opts[:ssh_user]}@#{opts[:address]}" : opts[:address]) ssh_opt += (opts[:ssh_port] ? " -p #{opts[:ssh_port]}" : '') end if help parser.educate(STDOUT) elsif `which sfpagent`.strip.length > 0 case subcommand when 'install' system("#{ssh_opt} sudo gem install sfpagent --no-ri --no-rdoc") when 'start' system "#{ssh_opt} sfpagent -s && sleep 5" system "#{ssh_opt} sfpagent -a" when 'restart' system "#{ssh_opt} sfpagent -r" when 'stop' system("#{ssh_opt} sfpagent -t") when 'status' system("#{ssh_opt} sfpagent -a") when 'upgrade' system("#{ssh_opt} sudo gem update sfpagent --no-ri --no-rdoc") when 'state' code, state = HTTP.get_data(opts[:address], 1314, "/state") if code == '200' and state =~ /{.*}/ state = JSON[state]['state'] state.keys.each { |key| state.delete(key) if state[key]['_context'] != 'object' } puts CodeRay.encode(JSON.pretty_generate(state), :json, :terminal) else puts state end when 'model' code, model = HTTP.get_data(opts[:address], 1314, "/model") if code == '200' and model =~ /{.*}/ model = JSON[model] model.keys.each { |key| model.delete(key) if model[key]['_context'] != 'object' } puts CodeRay.encode(JSON.pretty_generate(model), :json, :terminal) else puts model end when 'bsig' code, json = HTTP.get_data(opts[:address], 1314, "/bsig") puts (code == '200' and json.length >= 2 ? CodeRay.encode(JSON.pretty_generate(JSON[json]), :json, :terminal) : json) when 'module' code, json = HTTP.get_data(opts[:address], 1314, "/bsig") puts (code == '200' and json.length >= 2 ? CodeRay.encode(JSON.pretty_generate(JSON[json]), :json, :terminal) : json) when 'exec', 'execute' if args.length > 0 name = args.shift name = '$.' + name if !name.isref parameters = {} args.each do |arg| if arg =~ /=/ arg = arg.split('=', 2) parameters[arg[0]] = arg[1] end end data = { 'action' => JSON.generate({ 'name' => name, 'parameters' => parameters }) } code, _ = HTTP.post_data(opts[:address], 1314, "/execute", data) puts (code == '200' ? "Executing #{name} [OK]".green : "Executing #{name} [Failed]".red) else $stderr.puts 'Invalid parameters (usage: agent exec [action-arguments]).' end when 'log' puts Net::HTTP.get "#{opts[:address]}", "/log", 1314 when 'list' get_agents(opts).each { |name,model| puts "#{name} address=#{model['sfpAddress']} port=#{model['sfpPort']}" } when 'help' parser.educate(STDOUT) else $stderr.puts 'Unrecognized command! Use \"-h\" to print available commands.'.red end else $stderr.puts 'sfpagent gem is not installed!'.red end end def do_edit(args=ARGV) file = (args[0].nil? ? Nuri.main : args[0].to_s) if file == '-h' or file == '--help' puts <<-EOS Usage: edit EOS elsif File.exist?(file) system "#{Nuri.config['editor']} #{file}" else $stderr.puts 'Target file is not specified!' end end def get_agents(opts={}) opts[:push_modules] = true if !opts[:no_push_module] master = Nuri::Master.new master.set_model opts state = master.get_state opts agents = {} state.each do |name,model| agents[name] = { 'sfpAddress' => (model['sfpAddress'].is_a?(String) ? model['sfpAddress'] : ''), 'sfpPort' => (model['sfpPort'].is_a?(Fixnum) ? model['sfpPort'] : 0) } end agents end def do_console banner = <<-EOS Usage: [options] when is: state get current state plan generate a plan bsig deployment with distributed mechanism agent manage Nuri agent edit edit file help print this help exit exit this console EOS username = `whoami`.strip puts About loop do begin print "nuri@#{username}> ".green begin input = STDIN.gets raise Exception if input.nil? rescue Exception puts '' break end input = input.chomp.strip if input.length <= 0 elsif input[0] == '!' system(input[1..input.length]) else command, args = input.split(' ', 2) break if command == 'exit' or command == 'quit' or command == 'q' args = args.to_s.split(' ') command = args[0] and args = ['-h'] if command == 'help' and args.length > 0 case command when 'state' do_state args, '' when 'plan' do_plan args, '' when 'bsig' do_bsig args, '' when 'agent' do_agent args, '' when 'edit' do_edit args when 'model' do_model args when 'cd' Dir.chdir(args[0].to_s) when 'pwd' puts Dir.pwd when 'ls' system "ls --color #{args.join(" ")}" when 'help' puts banner when 'version', '-v' puts About else $stderr.puts 'Unrecognized command! Use \"-h\" option to print available commands.'.red end end rescue Exception => e $stderr.puts "#{e}\n#{e.backtrace.join("\n")}" end end puts "Bye!\n" end def do_model(args=ARGV, cmd='nuri') parser = Trollop::Parser.new do banner <<-EOS Usage: #{cmd}state [options] where [options] are: EOS opt :model_file, 'file contains a model of desired state', :default => Nuri.main, :short => '-m' opt :json, 'print output in JSON' opt :plain, 'print output in plain format' end help, args = check_help(args) opts = process_args args, parser if help parser.educate(STDOUT) elsif File.exist?(opts[:model_file]) @parser = Sfp::Parser.new({:home_dir => File.dirname(opts[:model_file])}) @parser.parse File.read(opts[:model_file]) model = @parser.root model.accept(ParentEliminator) model.select! { |k,v| k[0,1] != '_' and (not v.is_a?(Hash) or v['_context'] == 'object') } model.accept(Sfp::Helper::Sfp2Ruby) if opts[:json] puts (opts[:plain] ? JSON.generate(model) : CodeRay.encode(JSON.pretty_generate(model), :json, :terminal)) else puts (opts[:plain] ? YAML.dump(model) : CodeRay.encode(YAML.dump(model), :yaml, :terminal)) end else $stderr.puts "Model file '#{opts[:model_file]}' is not exist! Use \"-h\" option for more details.".red end end def run banner = <<-EOS Usage: nuri [command] where [command] is: model get compilation result of the model state get the current state plan generate the plan bsig deployment with distributed mechanism agent manage Nuri agent console enter console EOS if ARGV.length > 0 success = false case ARGV.shift when 'state' do_state when 'plan' do_plan when 'bsig' do_bsig when 'agent' do_agent when 'console' do_console when 'edit' do_edit when 'model' do_model else puts banner end else puts banner end end end module Sfp::Helper Sfp2Ruby = Object.new def Sfp2Ruby.visit(name, value, parent) if name[0] == '_' parent.delete(name) elsif value.is_a?(Hash) case value['_context'] when 'null' parent[name] = nil when 'any_value', 'constraint', 'procedure' parent.delete(name) when 'set' parent[name] = value['_values'] end end true end end Sfp::Console.new.run if $0 == __FILE__