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

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

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 = 'CHEF_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/opscode/private-chef-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 "#{Chef::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-#{ChefConfig::Dist::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 #{Chef::Dist::SERVER_PRODUCT} succeeded, an error
            occurred while deleting #{Chef::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 #{Chef::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 #{Chef::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")
      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, "#{Chef::Dist::SERVER_PRODUCT}")

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

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