require 'net/ssh' require 'pathname' require 'json' require 'base64' namespace :exec_commands do desc "Exec commands Script" task :exec_commands,[:base64_args,:site_construct_id,:commands,:type,:server_name,:rails_env,:commands_i18n] => :environment do |task,args| if args.base64_args.present? tmp = JSON.parse(Base64.decode64(args.base64_args)) rescue {} args = Rake::TaskArguments.new(tmp.keys, tmp.values) end @type = args.type if args.site_construct_id.present? @site_construct = SiteConstruct.find(args.site_construct_id) site_server = SiteServer.where(:server_name=>@site_construct.server_type).first site_servers = [site_server] @site_construct.update(:status=>"execing",:infos=>[""]) else @site_construct = nil site_servers = SiteServer.where(:server_name.in=>args.server_name.split("////")).to_a end if args.type == "exec_all" Multithread.where(:key=>'execing_commands').each do |thread| if thread.status["status"] == "error" || thread.status["status"] == "finish" thread.destroy elsif thread.updated_at < (Time.now - 5.minute) thread.destroy end end @thread = Multithread.where(:key=>'execing_commands').first else @thread = nil end if @thread.nil? @command_trans = {} @command_relations = {"upgrade_site"=>'git fetch origin && rails runner "a=Admin::SitesController.new;a.git_reset(%w(origin)[0],%w(update)[0]);while(1) do (Multithread.where(key: %w(update_manager)[0],status: %w(finish)[0]).count!=0 ? break : nil) end;sleep(5)"', "bundle_update"=>"bundle update && kill -s USR2 `cat tmp/pids/unicorn.pid`", "restart_site"=>"kill -s TERM `fuser tmp/pids/unicorn.sock tmp/sockets/unicorn.sock tmp/unicorn.sock 2>/dev/null`; rvm use `cat .ruby-version`; bundle exec unicorn_rails -c config/unicorn.rb -D -E {{rails_env}}", "get_template"=>"mongo --eval 'db.sites.find().pretty()[0][\"template\"]' {{db_name}} --quiet"} @preserve_output = ["get_template"] @command_relations.each do |k,v| @command_trans[k] = I18n.t("client_management.#{k}") end rails_envs = ["production", "development"] rails_envs.each do |env| @command_relations["start_site_in_env,#{env}"] = "kill -s TERM `fuser tmp/pids/unicorn.sock tmp/sockets/unicorn.sock tmp/unicorn.sock 2>/dev/null`; rvm use `cat .ruby-version`; bundle exec unicorn_rails -c config/unicorn.rb -D -E #{env}" @command_relations["start_site_in_env\\,#{env}"] = @command_relations["start_site_in_env,#{env}"] @command_trans["start_site_in_env,#{env}"] = I18n.t("client_management.start_site_in_env",{:env=>env}) @command_trans["start_site_in_env\\,#{env}"] = @command_trans["start_site_in_env,#{env}"] end begin if args.type == "exec_all" if @thread.nil? @thread = Multithread.create(:key=>'execing_commands',:status=>{"infos"=>[],"status"=>"execing"}) else @thread.update(:status=>{"infos"=>[],"status"=>"execing"}) end end site_servers.each do |site_server| ip = site_server.ip server_port = site_server.port user = site_server.account password = site_server.password @password = password begin Net::SSH.start(ip , user , { password: password, port: server_port}) do |ssh| end rescue Net::SSH::HostKeyMismatch system("ssh-keygen -f \"$HOME/.ssh/known_hosts\" -R #{ip}") rescue Errno::ENOTTY system("ssh-add \"$HOME/.ssh/id_rsa\"") end Net::SSH.start(ip , user , { password: password, port: server_port}) do |ssh| @site_construct.update!(:infos=>[]) rescue nil if args.type == 'close_site' exec_ssh_command_by_sudo_and_see_output(ssh,"chmod 777 #{@site_construct.path}/#{@site_construct.site_name} -R",false) exec_ssh_command_by_sudo_and_see_output(ssh,"bash -l -c 'kill -s TERM `fuser #{@site_construct.path}/#{@site_construct.site_name}/tmp/unicorn.sock 2>/dev/null`'",false) update_infos_for_exec("Finish closing #{@site_construct.site_name}") @site_construct.update(:status =>"closed") elsif args.type == 'open_site' default_rails_env = 'development' enable_rails_env = ['development','production'] rails_env = @site_construct.rails_env.blank? ? default_rails_env : @site_construct.rails_env cmd_output = exec_ssh_command_by_sudo_and_see_output(ssh,"ps -o args= -p `cat #{@site_construct.path}/#{@site_construct.site_name}/tmp/pids/unicorn.pid`",false) rescue [] enable_rails_env.each do |env| if( cmd_output[0].include?(env) rescue false) rails_env = env break elsif( cmd_output[0].include?("No such file or directory") rescue false) @site_construct.update(:status => "closed") break end end if args.rails_env.present? rails_env = args.rails_env end @site_construct.update(:rails_env=>rails_env) update_infos_for_exec("Starting site to #{rails_env}") outputs = exec_ssh_command_by_sudo_and_see_output(ssh,"sudo -p 'sudo password:' chmod 777 #{@site_construct.path}/#{@site_construct.site_name} -R",false) output = exec_ssh_command_by_sudo_and_see_output(ssh,"bash -l -c 'cd #{@site_construct.path}/#{@site_construct.site_name}\nkill -s TERM `fuser tmp/unicorn.sock 2>/dev/null`\nsudo -p \"sudo password:\" kill -s TERM `sudo -p \"sudo password:\" fuser tmp/unicorn.sock 2>/dev/null`\nsudo -p \"sudo password:\" rm -f tmp/pids/unicorn.pid\n rvm use `cat .ruby-version`\nbundle exec unicorn_rails -c config/unicorn.rb -D -E #{rails_env}\n'",false) if output.include? "bundle install" update_infos_for_exec("Not yet bundle install") outputs = exec_ssh_command_by_sudo_and_see_output(ssh,"bash -l -c 'cd #{@site_construct.path}/#{@site_construct.site_name}; rvm use `cat .ruby-version`; bundle install'") if outputs.join("\n").include? "Username for" update_infos_for_exec("Cannot finish bundle install.") update_infos_for_exec("This site include an private git project.") @site_construct.update(:status =>"error") break else exec_ssh_command_by_sudo_and_see_output(ssh,"bash -l -c 'cd #{@site_construct.path}/#{@site_construct.site_name}; rvm use `cat .ruby-version`; bundle exec unicorn_rails -c config/unicorn.rb -D -E #{rails_env}'",false) end end update_infos_for_exec("Finish starting #{@site_construct.site_name}") @site_construct.update(:status =>"finish") else if args.type == 'exec_all' sites = SiteConstruct.where(:server_type=>site_server.server_name).to_a else sites = [@site_construct] end commands = args.commands if commands.class == String commands = commands.split("////").select{|c| c != ""} elsif commands.nil? commands = [] end if commands.include?("{{create_users}}") commands = ['sudo -p "sudo password:" chmod 777 {{full_site_path}} -R', 'sudo -p "sudo password:" chmod 777 {{full_site_path}}/public -R', 'sudo -p "sudo password:" chmod 777 {{full_site_path}}/app/templates -R', 'sudo -p "sudo password:" useradd --home-dir {{full_site_path}}/app/templates {{site_name}} --no-create-home --badnames', 'sudo -p "sudo password:" bash -l -c \'echo -e "{{site_name}}\n{{site_name}}"|passwd {{site_name}}\''] end @commands_i18n = args.commands_i18n # command 客製化翻譯 sites.each do |site_construct| @site_construct = site_construct @site_construct.update!(:infos=>[]) exec_ssh_command_by_sudo(ssh,"chmod 777 #{@site_construct.path}/#{@site_construct.site_name} -R") rails_env = @site_construct.rails_env.blank? ? "development" : @site_construct.rails_env commands.each_with_index do |command, idx| @command_i18n = command update_flag = 1 @command_relations.each do |k,v| if command.include?("{{#{k}}}") if @preserve_output.include?(k) update_flag = 1 else update_flag = 2 #Not logging commands result end end command = command.gsub("{{#{k}}}",v) @command_i18n = @command_i18n.gsub("{{#{k}}}",@command_trans[k]) end if @commands_i18n tmp = @commands_i18n[idx] @command_i18n = tmp if tmp end command = command.gsub("{{rails_env}}",rails_env) command = command.gsub("{{full_site_path}}",@site_construct.full_site_path) command = command.gsub("{{site_name}}",@site_construct.site_name) command = command.gsub("{{db_name}}",@site_construct.db_name) @command_i18n = @command_i18n.gsub("{{rails_env}}",rails_env) @command_i18n = @command_i18n.gsub("{{full_site_path}}",@site_construct.full_site_path) @command_i18n = @command_i18n.gsub("{{site_name}}",@site_construct.site_name) command = command.gsub("'","'\"'\"'") exec_ssh_command_by_sudo_and_see_output(ssh,"bash -l -c 'cd #{@site_construct.path}/#{@site_construct.site_name};#{command}'", update_flag) @command_i18n = nil end end end end end if !@thread.nil? @thread.update(:status=>@thread.status.merge({"status"=>"finish"})) elsif @site_construct @site_construct.update(:status =>"finish") end rescue => e if !@thread.nil? @thread.update(:status=>{"infos"=>@thread.status["infos"].push(e.message),"status"=>"error"}) @thread.update(:status=>{"infos"=>@thread.status["infos"].push(e.backtrace.join("\n")),"status"=>"error"}) end if !@site_construct.nil? @site_construct.update(:status =>"error",:infos=>@site_construct.infos.push("#{e.message}")) @site_construct.update(:status =>"error",:infos=>@site_construct.infos.push("#{e.backtrace.join("\n")}")) end end end end def exec_ssh_command_by_sudo(session,command) output = session.exec!("echo '#{@password}' | sudo -S #{command}") # output = session.exec!("echo '#{@password}' | sudo -S -s #{command}") if output.include?("sudo:") && output.include?("command not found") output = session.exec!(command) end return output.encode!("UTF-8", :invalid => :replace, :undef => :replace, :replace => '') end def update_infos_for_exec(info,update_last=false,update_array=false) return if @site_construct.nil? if update_last && !@site_construct.infos.empty? if @remove_last_line @site_construct.infos[-1] = info.to_s else @site_construct.infos[-1] += info.to_s end else if @remove_last_line @site_construct.infos = @site_construct.infos[0...-1] end if update_array @site_construct.infos += info else @site_construct.infos.push(info.to_s) end end @site_construct.save! return @site_construct.infos end def update_thread_infos_for_exec(info,update_last=false,update_array=false) if update_last && !@thread.status["infos"].empty? if @remove_last_line @thread.status["infos"][-1] = info.to_s else @thread.status["infos"][-1] += info.to_s end else if @remove_last_line @thread.status["infos"] = @thread.status["infos"][0...-1] end if update_array @thread.status["infos"] += info else @thread.status["infos"].push(info.to_s) end end @thread.save! return @thread.status["infos"] end def exec_ssh_command_and_see_output(session,command,update_flag=1,output_string=false) outputs = [] @flag = (@type == "exec_all") command_i18n = command update = (update_flag == true || update_flag == 1 || update_flag == 2) update_outputs = (update_flag == true || update_flag == 1) if update command_i18n = @command_i18n if @command_i18n if @site_construct update_thread_infos_for_exec(["Execing #{command_i18n} on on #{@site_construct.domain_name}",""],false,true) if @flag update_infos_for_exec(["Execing #{command_i18n}",""],false,true) elsif @thread update_thread_infos_for_exec(["Execing #{command_i18n}...",""],false,true) end end session.open_channel do |channel| channel.request_pty do |channel, success| channel.exec("LANG=en.UTF8 #{command}") do |ch, success| abort "could not execute command: #{command}" unless success channel.on_data do |ch, data| data_str = data.to_s data_str.encode!("UTF-8", :invalid => :replace, :undef => :replace, :replace => '') if data_str.include?("sudo password:") || data_str.include?("Password:") channel.send_data "#{@password}\n" else print data_str unless @no_stdout data_str.gsub!(/\r\n/, "\n") next if data_str.length == 0 output_lines = data_str.split("\n", -1) first_output = output_lines[0] if first_output if outputs.count != 0 outputs[-1] += first_output @remove_last_line = false outputs[-1] = outputs[-1].sub(/(.+)\r(.+)/) do |s| @remove_last_line = true first_output = $2 end else outputs << first_output end if update_outputs update_thread_infos_for_exec(first_output,true) if @flag update_infos_for_exec(first_output,true) end @remove_last_line = false new_arr = output_lines[1..-1] new_arr = new_arr.map do |output_line| output_line = output_line.sub(/(.+)\r(.+)/, '\2') end outputs += new_arr if update_outputs update_thread_infos_for_exec(new_arr,false,true) if @flag update_infos_for_exec(new_arr,false,true) end end end end channel.on_close do |ch| if update if @site_construct update_thread_infos_for_exec("Finish execing #{command_i18n} on #{@site_construct.domain_name}") if @flag update_infos_for_exec("Finish execing #{command_i18n}") elsif @thread update_thread_infos_for_exec("Finish execing #{command_i18n}") end end end end end end session.loop if output_string return outputs.join("\n") else return outputs end end def exec_ssh_command_by_sudo_and_see_output(session,command,update=1,output_string=false) outputs = exec_ssh_command_and_see_output(session,command,update) tmp = outputs.join("\n") if tmp.include?("Permission denied") || tmp.include?("Operation not permitted") outputs = exec_ssh_command_and_see_output(session,"sudo -p 'sudo password:' #{command}",update) tmp = outputs.join("\n") end if output_string return tmp else return outputs end end end