#!/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" class Nuri::Console include Nuri::Helper 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] [path] 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 :color, 'enable colorized output' 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 args.length > 0 state = state.at?("$.#{args[0]}") puts (!opts[:color] ? "Path: #{args[0]}" : "Path: " + "#{args[0]}".yellow) end if opts[:json] puts (!opts[:color] ? JSON.generate(state) : CodeRay.encode(JSON.pretty_generate(state), :json, :terminal)) else puts (!opts[:color] ? 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 :json, 'print the plan in JSON' opt :color, 'enable colorized output' opt :no_interactive, 'disable interactive input' opt :no_push_module, 'disable automatic push module' opt :image, 'image graph of the generated plan', :default => '' #Dir.home + '/.nuri/plan.png' 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 plan_file = Dir.home + '/.nuri/plan.json' File.open(plan_file, 'w') { |f| f.write(JSON.generate(plan)) } if opts[:image].strip.length > 0 system "#{File.dirname(__FILE__)}/nuri-sfwgraph #{plan_file} #{opts[:image]}" end if opts[:json] json = JSON.pretty_generate(plan) puts (opts[:color] ? CodeRay.encode(json, :json, :terminal) : json) else actions = plan['workflow'] if plan['type'] == 'sequential' p = 'Sequential Plan:' actions.each_index { |i| p += "\n#{i+1}. #{actions[i]['name']} #{JSON.generate(actions[i]['parameters'])}" } puts p else p = 'Partial-Order Plan:' 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:' opts[:plan] = plan if master.execute_plan(opts) puts (opts[:color] ? "Execution success.".green : "Execution success.") else puts (opts[:color] ? "Execution failed.".red : "Execution failed.") end end elsif plan['workflow'].length == 0 puts (opts[:color] ? "Goal state has been achieved.".green : "Goal state has been achieved.") end else $stderr.puts (opts[:color] ? "No solution.".red : "No solution.") end else $stderr.puts "Model file '#{opts[:model_file]}' is not exist! Use \"-h\" option for more details." 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 :apply, 'generate and then deploy a BSig model' opt :status, 'get BSig status of all agents' opt :purge, 'purge existing BSig model', :short => '-g' opt :color, 'enable colorized output' 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 opts[:status] opts[:push_modules] = true if !opts[:no_push_module] master = Nuri::Master.new master.set_model opts master.get_state(opts).each do |name,state| print "- #{name}: " if not state.is_a?(Hash) or not state.has_key?('sfpAddress') puts (opts[:color] ? "unknown address".yellow : "unknown address") else address = state['sfpAddress'] port = (state['sfpPort'] ? state['sfpPort'] : 1314) if address.is_a?(String) and port.is_a?(Fixnum) status = Net::HTTP.get(URI("http://#{address}:#{port}/bsig/flaws")) if status == '{}' puts (opts[:color] ? "no-flaws".green : "no-flaws") else puts (opts[:color] ? status.yellow : status) end elsif address.is_a?(Sfp::Undefined) puts (opts[:color] ? "not-exist".yellow : "not-exist") elsif address.is_a?(Sfp::Unknown) puts (opts[:color] ? "unknown".yellow : "unknown") end end end elsif File.exist?(opts[:model_file]) opts[:bsig_deploy] = true opts[:parallel] = true opts[:push_modules] = true if !opts[:no_push_module] master = Nuri::Master.new master.set_model(opts) if opts[:purge] print "Purging Behavioural Signature model " puts (opts[:color] ? "[Wait]".yellow : "[Wait]") status = master.purge_bsig(opts) print "Purging Behavioural Signature model " if status puts (opts[:color] ? "[OK]".green : "[OK]") else puts (opts[:color] ? "[Failed]".red : "[Failed]") 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 json = JSON.pretty_generate(bsig) puts (opts[:color] ? CodeRay.encode(json, :json, :terminal) : json) if not opts[:no_interactive] and not opts[:apply] print "Deploy the BSig model [y/N]? " opts[:apply] = true if STDIN.gets.chomp.upcase == 'Y' end if opts[:apply] print 'Deploying the BSig model ' puts (opts[:color] ? "[Wait]".yellow : "[Wait]") opts[:bsig] = bsig if master.deploy_bsig(opts) print 'Deploying the BSig model ' puts (opts[:color] ? "[OK]".green : "[OK]") else print 'Deploying the BSig model ' puts (opts[:color] ? "[Failed]".red : "[Failed]") end end else puts (opts[:color] ? "Goal state has been achieved.".green : "Goal state has been achieved.") end else $stderr.puts (opts[:color] ? "No solution.".red : "No solution.") end end else $stderr.puts "Model file '#{opts[:model_file]}' is not exist! Use \"-h\" option for more details." 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) reset delete all modules, model (desired state), bsig-model, and agents list 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 agent's logs file 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' opt :raw, "print a RAW output" opt :color, "enable colorized output" 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 or ssh_opt.length > 0 case subcommand when 'i', 'install' system("#{ssh_opt} sudo gem install sfpagent --no-ri --no-rdoc") when 's', 'start' system "#{ssh_opt} sfpagent -s && sleep 5" system "#{ssh_opt} sfpagent -a" when 'r', 'restart' system "#{ssh_opt} sfpagent -r" when 't', 'stop' system("#{ssh_opt} sfpagent -t") when 'a', 'status' system("#{ssh_opt} sfpagent -a") when 'upgrade' system("#{ssh_opt} sudo gem update sfpagent --no-ri --no-rdoc") when 'e', 'state' code, state = get_data(opts[:address], 1314, "/state") if code == '200' and state =~ /{.*}/ state = JSON[state]['state'] if not opts[:raw] state.keys.each { |key| state.delete(key) if state[key]['_context'] != 'object' } state.accept(Sfp::Helper::Sfp2Ruby) end puts (opts[:color] ? CodeRay.encode(YAML.dump(state), :yaml, :terminal) : YAML.dump(state)) else puts state end when 'm', 'model' code, model = get_data(opts[:address], 1314, "/model") if code == '200' and model =~ /{.+}/ model = JSON[model] if not opts[:raw] model.select! { |k,v| k[0,1] != '_' and (not v.is_a?(Hash) or v['_context'] == 'object') } model.accept(Sfp::Helper::Sfp2Ruby) end puts (opts[:color] ? CodeRay.encode(YAML.dump(model), :yaml, :terminal) : YAML.dump(model)) elsif code == '404' or model == '{}' puts "Model of desired state is not exist." else $stderr.puts "Error: #{code}" end when 'b', 'bsig' code, bsig = get_data(opts[:address], 1314, "/bsig") if code == '200' and bsig =~ /{.+}/ json = JSON.pretty_generate(JSON[json]) puts (opts[:color] ? CodeRay.encode(json, :json, :terminal) : json) elsif code == '404' or bsig == '{}' puts 'Behavioural Signature model is not exist.' else $stderr.puts "Error: #{code}" end when 'mod', 'module', 'modules' code, modules = get_data(opts[:address], 1314, "/modules") if code == '200' and modules =~ /{.+}/ output = YAML.dump(JSON[modules]) puts (opts[:color] ? CodeRay.encode(output, :yaml, :terminal) : output) elsif code == '404' or modules == '{}' puts 'No module is available.' else $stderr.puts "Error: #{code}" end when 'x', '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, _ = post_data(opts[:address], 1314, "/execute", data) if opts[:color] puts (code == '200' ? "Executing #{name} [OK]".green : "Executing #{name} [Failed]".red) else puts (code == '200' ? "Executing #{name} [OK]" : "Executing #{name} [Failed]") end else $stderr.puts 'Invalid parameters (usage: agent exec [action-arguments]).' end when 'g', 'log', 'logs' puts Net::HTTP.get "#{opts[:address]}", "/log", 1314 when 'l', 'list' get_agents(opts).each { |name,model| puts "#{name} address=#{model['sfpAddress']} port=#{model['sfpPort']}" } when 'reset' codes = [] codes[0], _ = delete_data(opts[:address], 1314, "/bsig") codes[1], _ = delete_data(opts[:address], 1314, "/model/cache") codes[2], _ = delete_data(opts[:address], 1314, "/model") codes[3], _ = delete_data(opts[:address], 1314, "/modules") codes[4], _ = delete_data(opts[:address], 1314, "/agents") failed = codes.index { |x| x != '200' } if opts[:color] puts (failed ? "Reset agent [Failed]".red : "Reset agent [OK]".green) else puts (failed ? "Reset agent [Failed]" : "Reset agent [OK]") end when 'h', 'help' parser.educate(STDOUT) else $stderr.puts 'Unrecognized command! Use -h to print available commands.' end else $stderr.puts 'sfpagent gem is not installed!' 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.is_a?(Hash) and model['sfpAddress'].is_a?(String) ? model['sfpAddress'] : ''), 'sfpPort' => (model.is_a?(Hash) and model['sfpPort'].is_a?(Fixnum) ? model['sfpPort'] : 0) } end agents end def do_console parser = Trollop::Parser.new do 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 end username = `whoami`.strip puts About loop do begin print "nuri@#{username}> ".cyan 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 #{args.join(" ")}" when 'help' parser.educate(STDOUT) when 'version', '-v' puts About else $stderr.puts 'Unrecognized command! Use -h option to print available commands.' 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 :color, 'enable colorized output' 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[:color] ? JSON.generate(model) : CodeRay.encode(JSON.pretty_generate(model), :json, :terminal)) else puts (!opts[:color] ? 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." 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 's', 'state' do_state when 'p', 'plan' do_plan when 'b', 'bsig' do_bsig when 'a', 'agent' do_agent when 'c', 'console' do_console when 'e', 'edit' do_edit when 'm', 'model' do_model else puts banner end else puts banner end end end if $0 == __FILE__ Nuri.init Nuri::Console.new.run end