module Deplomat class Node attr_accessor :log_to, :raise_exceptions, :logfile, :wrap_in attr_reader :current_path attr_writer :stdout_lines_to_ignore def stdout_lines_to_ignore if @stdout_lines_to_ignore.kind_of?(Array) @stdout_lines_to_ignore else [@stdout_lines_to_ignore] end.compact end def initialize(logfile: "#{Dir.pwd}/deplomat.log", log_to: [:stdout], path: nil, raise_exceptions: true) @log_to = log_to @logfile = logfile @raise_exceptions = raise_exceptions @current_path = path if path_exist?(path) end def execute(command, path=@current_path, message: [], stdout_source: :stdout, log_command: true, _raise_exceptions: @raise_exceptions) original_command = command if @wrap_in command = @wrap_in.sub("_command_", command) end message = [message] if message.kind_of?(String) log(message[0] + "\n", color: 'white') unless message.empty? || message.nil? # Respect current_path command_to_log = original_command if path command_to_log = "#{original_command}\n(in #{path})" command = "cd #{path} && #{command}" end out = "" status = nil Open3.popen3(command) do |stdin, stdout, stderr, thread| # Sometimes, programs write in stderr, although there are no errors. # rake assets:precompile does that, for example. stdout_source_object = (stdout_source == :stderr ? stderr : stdout) log("--> " + command_to_log + "\n", color: "white") if log_command stdout_source_object.readlines.each_with_index do |line, i| self.stdout_lines_to_ignore.each do |line_to_ignore| line_to_ignore = line_to_ignore.to_a if line_to_ignore.kind_of?(Range) line_to_ignore = [line_to_ignore] if line_to_ignore.kind_of?(Integer) line = nil if line_to_ignore.include?(i+1) end if line out << line log(" #{line}") if log_command end end error_out = "" status = thread.value.exitstatus.to_i if status > 0 while o = stderr.gets error_out += o end log(error_out + "\n", color: 'red') if _raise_exceptions self.close if self.respond_to?(:close) raise Deplomat::ExecutionError end end yield if block_given? end log(message[1] + "\n", color: 'white') unless message.empty? || message.nil? return { status: status, out: out } end def copy(what, where) execute("rsync -ar #{what} #{where}") end alias :cp :copy def move(what, where) execute("mv #{what} #{where}") end alias :mv :move def remove(what) execute("rm -rf #{what}") end alias :rm :remove def create_file(filename) execute("touch #{filename}") end alias :touch :create_file def create_dir(dirname) execute("mkdir -p #{dirname}") end alias :mkdir :create_dir def create_symlink(source, target) execute("ln -s #{source} #{target}") end alias :ln :create_symlink def path_exist?(path) execute("[ -e #{path} ]", @current_path, log_command: false, _raise_exceptions: true) && true rescue Deplomat::ExecutionError # returned 1, file doesn't exist false end alias :path_exists? :path_exist? alias :file_exists? :path_exist? def cd(path) raise Deplomat::NoSuchPathError, path if !path.nil? && !path_exist?(path) @current_path = if path =~ /\A[\/~]/ || path.nil? path else File.expand_path("#{@current_path}#{path}") end # Making sure we don't end up with double // at the end of the path @current_path = @current_path.chomp("/") + "/" end def git_push(remote="origin", branch="master") execute("git push #{remote} #{branch}") end def git_pull(remote="origin", branch="master") execute("git pull #{remote} #{branch}") end def git_merge(source="origin", target="master") execute("git merge #{source} #{target}") end def git_checkout(target) execute("git checkout #{target}") end def update_requisite_number!(n, counter_file_path: "#{@current_path}/.deployment_requisites_counter") current_number = current_requisite_number(counter_file_path) if n <= current_number log "New requisite number (#{n}) is below or equals the current one (#{current_number}). " + "Something must have gone wrong.", color: "red" self.close if self.respond_to?(:close) exit 1 else execute("echo '#{n}' > #{counter_file_path}") end end def current_requisite_number(fn="#{@current_path}/.deployment_requisites_counter") if file_exists?(fn) contents = execute("cat #{fn}")[:out].chomp("\n") if contents =~ /\A\d+\Z/ return contents.to_i else 0 end else log "Requisite counter file `#{fn}` doesn't exist. " + "Please create it manually and symlink it in the deployment script if necessary.", color: "red" self.close if self.respond_to?(:close) exit 1 end end def clean(path: @current_path, except: [], leave: [0, :last]) # Gets us all entries sorted by date, most recent ones first entries_by_date = execute("ls -t", path, log_command: false)[:out].split("\n") if entries_by_date # Don't do anything with entries listed in :except entries_by_date = entries_by_date - except if leave entries_by_date.reverse! if leave[1] == :first entries_by_date = entries_by_date[leave[0]..entries_by_date.length] end entries_by_date.each { |entry| remove("#{path}#{entry}") } if entries_by_date end end def log(line, color: 'light_black') @message_color = color # Only calls log methods mentioned in the @log_to property @log_to.each { |logger| self.send("log_to_#{logger}", line) } end private def log_to_file(line) if @logfile open(@logfile, 'a') { |f| f.puts line } end end def log_to_stdout(line) print_to_terminal(line, color: @message_color, newline: false) end end end