ManageIQ/manageiq

View on GitHub
app/models/miq_server/log_management.rb

Summary

Maintainability
A
45 mins
Test Coverage
A
91%
module MiqServer::LogManagement
  extend ActiveSupport::Concern

  included do
    belongs_to :log_file_depot, :class_name => "FileDepot"
    has_many   :log_files, :dependent => :destroy, :as => :resource
  end

  def _post_my_logs(options)
    # Make the request to the MiqServer whose logs are needed
    MiqQueue.create_with(
      :miq_callback => options.delete(:callback),
      :msg_timeout  => options.delete(:timeout),
      :priority     => MiqQueue::HIGH_PRIORITY,
      :args         => [options]
    ).put_unless_exists(
      :class_name  => self.class.name,
      :instance_id => id,
      :method_name => "post_logs",
      :server_guid => guid,
      :zone        => my_zone
    ) do |msg|
      _log.info("Previous adhoc log collection is still running, skipping...Resource: [#{self.class.name}], id: [#{id}]") unless msg.nil?
      nil
    end
  end

  def synchronize_logs(*args)
    options = args.extract_options!
    args << self unless args.last.kind_of?(self.class)
    LogFile.logs_from_server(*args, options)
  end

  def last_log_sync_on
    log_files.maximum(:updated_on)
  end

  def last_log_sync_message
    last_log = log_files.order(:updated_on => :desc).first
    last_log.try(:miq_task)&.message
  end

  def include_automate_models_and_dialogs?(value)
    return value unless value.nil?

    Settings.log.collection.include_automate_models_and_dialogs
  end

  def post_logs(options)
    taskid = options[:taskid]
    task = MiqTask.find(taskid)
    context_log_depot = log_depot(options[:context])

    # the current queue item and task must be errored out on exceptions so re-raise any caught errors
    raise _("Log depot settings not configured") unless context_log_depot

    context_log_depot.update(:support_case => options[:support_case].presence)

    if include_automate_models_and_dialogs?(options[:include_automate_models_and_dialogs])
      post_automate_models(taskid, context_log_depot)
      post_automate_dialogs(taskid, context_log_depot)
    end

    post_historical_logs(taskid, context_log_depot) unless options[:only_current]
    post_current_logs(taskid, context_log_depot)
    task.update_status("Finished", "Ok", "Log files were successfully collected")
  end

  def current_log_patterns
    # use an array union to add pg log path patterns if not already there
    ::Settings.log.collection.current.pattern | pg_log_patterns
  end

  def pg_data_dir
    Pathname.new(ENV.fetch("APPLIANCE_PG_DATA"))
  end

  def pg_log_patterns
    pg_data = pg_data_dir
    return [] unless pg_data

    pg_data = Pathname.new(pg_data)
    [pg_data.join("*.conf"), pg_data.join("log/*"), Pathname.new("/etc/manageiq/postgresql.conf.d/*")]
  end

  def log_start_and_end_for_pattern(pattern)
    evm = VMDB::Util.get_evm_log_for_date(pattern)
    return if evm.nil?

    VMDB::Util.get_log_start_end_times(evm)
  end

  def format_log_time(time)
    time.respond_to?(:strftime) ? time.strftime("%Y%m%d_%H%M%S") : "unknown"
  end

  def log_patterns(log_type, base_pattern = nil)
    case log_type.to_s.downcase
    when "archive"
      Array(::Settings.log.collection.archive.pattern).unshift(base_pattern)
    when "current"
      current_log_patterns
    else
      [base_pattern]
    end
  end

  def post_one_log_pattern(pattern, logfile, log_type)
    task = logfile.miq_task
    log_prefix = "Task: [#{task.id}]"

    log_start, log_end = if logfile.logging_started_on
                           [logfile.logging_started_on, logfile.logging_ended_on]
                         else
                           log_start_and_end_for_pattern(pattern)
                         end

    date_string = "#{format_log_time(log_start)} #{format_log_time(log_end)}" unless log_start.nil? && log_end.nil?

    msg = "Zipping and posting #{log_type.downcase} logs for [#{who_am_i}] from: [#{log_start}] to [#{log_end}]"
    _log.info("#{log_prefix} #{msg}")
    task.update_status("Active", "Ok", msg)

    begin
      local_file = VMDB::Util.zip_logs(log_type.to_s.downcase.concat(".zip"), log_patterns(log_type, pattern), "system")
      log_files << logfile

      logfile.update(
        :local_file         => local_file,
        :logging_started_on => log_start,
        :logging_ended_on   => log_end,
        :name               => LogFile.logfile_name(self, log_type, date_string),
        :description        => "Logs for Zone #{zone.name rescue nil} Server #{name} #{date_string}"
      )

      logfile.upload
    rescue StandardError, Timeout::Error => err
      _log.error("#{log_prefix} Posting of #{log_type.downcase} logs failed for #{who_am_i} due to error: [#{err.class.name}] [#{err}]")
      task.update_status("Finished", "Error", "Posting of #{log_type.downcase} logs failed for #{who_am_i} due to error: [#{err.class.name}] [#{err}]")
      logfile.update(:state => "error")
      raise
    ensure
      FileUtils.rm_f(local_file) if local_file && File.exist?(local_file)
    end
    msg = "#{log_type} log files from #{who_am_i} are posted"
    _log.info("#{log_prefix} #{msg}")
    task.update_status("Active", "Ok", msg)
  end

  def post_automate_models(taskid, log_depot)
    domain_zip = Rails.root.join("log/domain.zip")
    backup_automate_models(domain_zip)
    now = Time.zone.now

    logfile = LogFile.historical_logfile
    logfile.update(:file_depot         => log_depot,
                   :miq_task           => MiqTask.find(taskid),
                   :logging_started_on => now,
                   :logging_ended_on   => now)
    post_one_log_pattern(domain_zip, logfile, "Models")
  ensure
    FileUtils.rm_rf(domain_zip)
  end

  def backup_automate_models(domain_zip)
    Dir.chdir(Rails.root) do
      MiqAeDatastore.backup('zip_file' => domain_zip, 'overwrite' => false)
    end
  end

  def post_automate_dialogs(taskid, log_depot)
    dialog_directory = Rails.root.join("log/service_dialogs")
    FileUtils.mkdir_p(dialog_directory)
    backup_automate_dialogs(dialog_directory)
    now = Time.zone.now

    logfile = LogFile.historical_logfile
    logfile.update(:file_depot         => log_depot,
                   :miq_task           => MiqTask.find(taskid),
                   :logging_started_on => now,
                   :logging_ended_on   => now)
    post_one_log_pattern(dialog_directory.join("*"), logfile, "Dialogs")
  ensure
    FileUtils.rm_rf(dialog_directory)
  end

  def backup_automate_dialogs(dialog_directory)
    Dir.chdir(Rails.root) do
      TaskHelpers::Exports::ServiceDialogs.new.export(:keep_spaces => false, :directory => dialog_directory)
    end
  end

  def post_historical_logs(taskid, log_depot)
    task = MiqTask.find(taskid)
    log_prefix = "Task: [#{task.id}]"
    log_type = "Archive"

    # Post all compressed logs for a specific date + configs, creating a new row per day
    VMDB::Util.compressed_log_patterns.each do |pattern|
      log_start, log_end = log_start_and_end_for_pattern(pattern)
      date_string = "#{format_log_time(log_start)}_#{format_log_time(log_end)}" unless log_start.nil? && log_end.nil?

      cond = {:historical => true, :name => LogFile.logfile_name(self, log_type, date_string), :state => 'available'}
      cond[:logging_started_on] = log_start unless log_start.nil?
      cond[:logging_ended_on] = log_end unless log_end.nil?
      logfile = log_files.find_by(cond)

      if logfile && logfile.log_uri.nil?
        _log.info("#{log_prefix} #{log_type} logfile already exists with id: [#{logfile.id}] for [#{who_am_i}] with contents from: [#{log_start}] to: [#{log_end}]")
        next
      else
        logfile = LogFile.historical_logfile
      end

      logfile.update(:file_depot => log_depot, :miq_task => task)
      post_one_log_pattern(pattern, logfile, log_type)
    end
  end

  def post_current_logs(taskid, log_depot)
    delete_old_requested_logs

    logfile = LogFile.current_logfile
    logfile.update(:file_depot => log_depot, :miq_task => MiqTask.find(taskid))
    post_one_log_pattern("log/*.log", logfile, "Current")
  end

  def delete_old_requested_logs
    log_files.where(:historical => false).destroy_all
  end

  def delete_active_log_collections_queue
    MiqQueue.create_with(:priority => MiqQueue::HIGH_PRIORITY).put_unless_exists(
      :class_name  => self.class.name,
      :instance_id => id,
      :method_name => "delete_active_log_collections",
      :server_guid => guid
    ) do |msg|
      _log.info("Previous cleanup is still running, skipping...") unless msg.nil?
    end
  end

  def delete_active_log_collections
    log_files.each do |lf|
      if lf.state == 'collecting'
        _log.info("Deleting #{lf.description}")
        lf.miq_task&.(:state => 'Finished', :status => 'Error', :message => 'Log Collection Incomplete during Server Startup')
        lf.destroy
      end
    end

    # Since a task is created before a logfile, there's a chance we have a task without a logfile
    MiqTask.where(:miq_server_id => id).where("name like ?", "Zipped log retrieval for %").where.not(state: "Finished").each do |task|
      task.update(:state => 'Finished', :status => 'Error', :message => 'Log Collection Incomplete during Server Startup')
    end
  end

  def log_collection_active_recently?(since = nil)
    since ||= 15.minutes.ago.utc
    return true if log_files.exists?(["created_on > ? AND state = ?", since, "collecting"])

    MiqTask.exists?(["miq_server_id = ? and name like ? and state != ? and created_on > ?", id, "Zipped log retrieval for %", "Finished", since])
  end

  def log_collection_active?
    return true if log_files.exists?(:state => "collecting")

    MiqTask.exists?(["miq_server_id = ? and name like ? and state != ?", id, "Zipped log retrieval for %", "Finished"])
  end

  def log_depot(context)
    context == "Zone" ? zone.log_file_depot : log_file_depot
  end

  def base_zip_log_name
    t = Time.now.utc.strftime('%FT%H_%M_%SZ'.freeze)
    # Name the file based on GUID and time.  GUID and Date/time of the request are as close to unique filename as we're going to get
    "App-#{guid}-#{t}"
  end
end