#!/opt/opscode/embedded/bin/ruby

gem "omnibus-ctl"
require "omnibus-ctl"
require "veil"
require "chef_server_ctl/log"
require "chef_server_ctl/config"
require "license_acceptance/acceptor"
require "chef-utils/dist"

module Omnibus
  # This implements callbacks for handling commands related to the
  # external services supported by Chef Server. As additional services
  # are external-enabled, controls/configuration will need to be added here.
  class ChefServerCtl < Omnibus::Ctl

    def help(*args)
      puts ::ChefServerCtl::Config::DOC_PATENT_MSG
      super
    end

    def db_data
      d = [%w{oc_bifrost bifrost},
           %w{opscode-erchef opscode_chef},
           %w{oc_id oc_id}]
      if running_service_config("bookshelf")["storage_type"] == "sql"
        d << %w{bookshelf bookshelf}
      end
      d
    end

    CREDENTIAL_ENV = "#{ChefUtils::Dist::Infra::SHORT.upcase}_SECRETS_DATA".freeze

    def credentials
      if ENV.has_key?(CREDENTIAL_ENV)
        @credentials = Veil::CredentialCollection::ChefSecretsEnv.new(var_name: CREDENTIAL_ENV)
      else
        secrets_file = ENV["SECRETS_FILE"] || "/etc/#{ChefUtils::Dist::Org::LEGACY_CONF_DIR}/private-#{ChefUtils::Dist::Infra::SHORT}-secrets.json"
        @credentials ||= Veil::CredentialCollection::ChefSecretsFile.from_file(secrets_file)
      end
    end

    # Note that as we expand our external service support,
    # we may want to consider farming external_X functions to
    # service-specific classes
    def external_cleanse_postgresql(perform_delete)
      postgres = external_services["postgresql"]
      exec_list = []
      # NOTE a better way to handle this particular action may be through a chef_run of a 'cleanse' recipe,
      # since here we're explicitly reversing the actions we did in-recipe to set the DBs up....
      superuser = postgres["db_superuser"]
      db_data.each do |service|
        key, dbname = service
        service_config = running_service_config(key)
        exec_list << "DROP DATABASE #{dbname};"
        exec_list << "REVOKE \"#{service_config["sql_user"]}\" FROM \"#{superuser}\";"
        exec_list << "REVOKE \"#{service_config["sql_ro_user"]}\" FROM \"#{superuser}\";"
        exec_list << "DROP ROLE \"#{service_config["sql_user"]}\";"
        exec_list << "DROP ROLE \"#{service_config["sql_ro_user"]}\";"
      end
      if perform_delete
        delete_external_postgresql_data(exec_list, postgres)
      else
        path = File.join(backup_dir, "postgresql-manual-cleanup.sql")
        dump_exec_list_to_file(path, exec_list)
        log warn_CLEANSE002_no_postgres_delete(path, postgres)
      end
    end

    def delete_external_postgresql_data(exec_list, postgres)
      last = nil
      begin
        require "pg"
        connection = postgresql_connection(postgres)
        while exec_list.length > 0
          last = exec_list.shift
          connection.exec(last)
        end
        puts "#{ChefUtils::Dist::Server::PRODUCT} databases and roles have been successfully deleted from #{postgres['vip']}"
      rescue StandardError => e
        exec_list.insert(0, last) unless last.nil?
        if exec_list.empty?
          path = nil
        else
          path = Time.now.strftime("/root/%FT%R-#{ChefUtils::Dist::Infra::SHORT}-server-manual-postgresql-cleanup.sql")
          dump_exec_list_to_file(path, exec_list)
        end
        # Note - we're just showing the error and not exiting, so that
        # we don't block any other external cleanup that can happen
        # independent of postgresql.
        log err_CLEANSE001_postgres_failed(postgres, path, last, e.message)
      ensure
        connection.close if connection
      end
    end

    def dump_exec_list_to_file(path, list)
      File.open(path, "w") do |file|
        list.each { |line| file.puts line }
      end
    end

    def external_status_postgresql(detail_level)
      postgres = external_services["postgresql"]
      begin
        require "pg"
        connection = postgresql_connection(postgres)
        if detail_level == :sparse
          # We connected, that's all we care about for sparse status
          # We're going to keep the format similar to the existing output
          "run: postgresql: connected OK to #{postgres["vip"]}:#{postgres["port"]}"
          # to hopefully avoid breaking anyone who parses this.
        else
          postgres_verbose_status(postgres, connection)
        end
      rescue StandardError => e
        if detail_level == :sparse
          "down: postgresql: failed to connect to #{postgres["vip"]}:#{postgres["port"]}: #{e.message.split("\n")[0]}"
        else
          log err_STAT001_postgres_failed(postgres, e.message)
          Kernel.exit! 128
        end
      ensure
        connection.close if connection
      end
    end

    def postgresql_connection(postgres)
      PG::Connection.open("user"     => postgres["db_connection_superuser"] || postgres["db_superuser"],
                          "host"     => postgres["vip"],
                          "password" => credentials.get("postgresql", "db_superuser_password"),
                          "port"     => postgres["port"],
                          "sslmode"  => postgres["sslmode"],
                          "dbname"   => "template1")
    end

    def postgres_verbose_status(postgres, connection)
      max_conn = connection.exec("SELECT setting FROM pg_settings WHERE name = 'max_connections'")[0]["setting"]
      total_conn = connection.exec("SELECT sum(numbackends) num FROM pg_stat_database")[0]["num"]
      version = connection.exec("SHOW server_version")[0]["server_version"]
      lock_result =  connection.exec("SELECT pid FROM pg_locks WHERE NOT GRANTED")
      locks = lock_result.map { |r| r["pid"] }.join(",")
      locks = "none" if locks.nil? || locks.empty?
      <<EOM
PostgreSQL
  * Connected to PostgreSQL v#{version} on #{postgres["vip"]}:#{postgres["port"]}
  * Connections: #{total_conn} active out of #{max_conn} maximum.
  * Processes ids pending locks: #{locks}
EOM
    end

    def err_STAT001_postgres_failed(postgres, message)
      <<EOM
STAT001: An error occurred while attempting to get status from PostgreSQL
         running on #{postgres["vip"]}:#{postgres["port"]}

         The error report follows:

#{format_multiline_message(12, message)}

         See https://docs.chef.io/error_messages.html#stat001-postgres-failed
         for more information.
EOM
    end

    def err_CLEANSE001_postgres_failed(postgres, path, last, message)
      msg = <<EOM
CLEANSE001: While local cleanse of #{ChefUtils::Dist::Server::PRODUCT} succeeded, an error
            occurred while deleting #{ChefUtils::Dist::Server::PRODUCT} data from the external
            PostgreSQL server at #{postgres["vip"]}.

            The error reported was:

#{format_multiline_message(16, message)}

EOM
      msg << <<-EOM unless last.nil?
            This occurred when executing the following SQL statement:
              #{last}
      EOM

      msg << <<-EOM unless path.nil?
            To complete cleanup of PostgreSQL, please log into PostgreSQL
            on #{postgres["vip"]} as superuser and execute the statements
            that have been saved to the file below:

              #{path}
      EOM

      msg << <<EOM

            See https://docs.chef.io/error_messages.html#cleanse001-postgres-failed
            for more information.
EOM
      msg
    end

    def warn_CLEANSE002_no_postgres_delete(sql_path, postgres)
      <<EOM
CLEANSE002: Note that #{ChefUtils::Dist::Server::PRODUCT} data was not removed from your
            remote PostgreSQL server because you did not specify
            the '--with-external' option.

            If you do wish to purge #{ChefUtils::Dist::Server::PRODUCT} data from PostgreSQL,
            you can do by logging into PostgreSQL on
            #{postgres["vip"]}:#{postgres["port"]}
            and executing the appropriate SQL staements manually.

            For your convenience, these statements have been saved
            for you in:

            #{sql_path}

            See https://docs.chef.io/error_messages.html#cleanse002-postgres-not-purged
            for more information.
EOM
    end

    # External ElasticSearch Commands
    def external_status_elasticsearch(_detail_level)
      elasticsearch = external_services["elasticsearch"]["external_url"]
      begin
        Chef::HTTP.new(elasticsearch).get(elasticsearch_status_url)
        puts "run: elasticsearch: connected OK to #{elasticsearch}"
      rescue StandardError => e
        puts "down: elasticsearch: failed to connect to #{elasticsearch}: #{e.message.split("\n")[0]}"
      end
    end

    def external_cleanse_elasticsearch(perform_delete)
      log <<-EOM
Cleansing data in a remote Elasticsearch instance is not currently supported.
      EOM
    end

    def elasticsearch_status_url
      case running_service_config("opscode-erchef")["search_provider"]
      when "elasticsearch"
        "/chef"
      else
        "/admin/ping?wt=json"
      end
    end

    # External Solr Status callback needs to be implemented.
    # opscode-solr4['external'] is set to true un chef-server-running-json
    # to support backward compatibility with reporting.
    def external_status_opscode_solr4(_detail_level)
      # opscode-solr4 status is seen as elasticsearch status"
    end

    # Override reconfigure to skip license checking
    def reconfigure(*args)
      puts ::ChefServerCtl::Config::DOC_PATENT_MSG

      # No license needed for Cinc Server
      # LicenseAcceptance::Acceptor.check_and_persist!("infra-server", ChefServerCtl::VERSION.to_s)

      ENV["CHEF_LICENSE"] = "accept-no-persist"

      status = run_chef("#{base_path}/embedded/cookbooks/dna.json")

      # fetch trusted certs if mtls enabled
      ca           = running_service_config('nginx')['ssl_client_ca']
      cert         = running_service_config('nginx')['pivotal_ssl_client_cert']
      key          = running_service_config('nginx')['pivotal_ssl_client_key']
      command      = 'sudo --preserve-env=PATH /opt/opscode/embedded/bin/knife ssl fetch -c /etc/opscode/pivotal.rb'
      # enabled if all are non-empty strings
      mtls_enabled = [ca, cert, key].all? { |x| x && !x.empty? }
      mtls_enabled ? run_command(command) : :ok

      if status.success?
        log "#{display_name} Reconfigured!"
        exit! 0
      else
        exit! 1
      end
    end
  end
end

if Process.euid != 0
  puts "This command must be run as root"
  exit 1
end

# This replaces the default bin/omnibus-ctl command
require "pathname"
file_path = Pathname.new(__FILE__)
cmd_name = "opscode" # what does this do? was ARGV[0]
plugin_path = file_path.dirname.parent.join("plugins")
arguments = ARGV[0..-1] # Get the rest of the command line arguments

# Intialize logging
log_level = if ENV["CSC_LOG_LEVEL"]
              ENV["CSC_LOG_LEVEL"].to_sym
            else
              :info
            end

ChefServerCtl::Log.level = log_level

ctl = Omnibus::ChefServerCtl.new(cmd_name, true, "#{ChefUtils::Dist::Server::PRODUCT}")

# Initialize global configuration
ChefServerCtl::Config.init(ctl)

# Load plugins and run the command
ctl.load_files(plugin_path)
ctl.run(arguments)
