require 'sinatra'
require 'rexml/document'
require 'openssl'
require 'logger'
require 'thread'
require 'fileutils'

require 'bootstrap.rb'
require 'resource.rb'
require 'remote.rb'
require 'cluster.rb'
require 'config.rb'
require 'pcs.rb'
require 'auth.rb'
require 'permissions.rb'

use Rack::CommonLogger

set :app_file, __FILE__
set :logging, false
set :static, false

def __msg_cluster_name_already_used(cluster_name)
  return "The cluster name '#{cluster_name}' has already been added. You may not add two clusters with the same name."
end

def __msg_node_name_already_used(node_name, cluster_name)
  return "The node '#{node_name}' is already a part of the '#{cluster_name}' cluster. You may not add a node to two different clusters."
end

def getAuthUser()
  return {
    :username => Thread.current[:tornado_username],
    :usergroups => Thread.current[:tornado_groups],
  }
end

before do
  @auth_user = getAuthUser()
  begin
    $cluster_name, $cluster_uuid = get_cluster_name_and_uuid()
  rescue SystemCallError => e
    $logger.error("Unable to read corosync.conf: #{e.message}")
    $logger.warn("Continuing request processing as if this node is not in a cluster")
    $cluster_name = ''
    $cluster_uuid = ''
  rescue CorosyncConf::ParseErrorException => e
    $logger.error("Unable to parse corosync.conf: #{e.message}")
    $logger.warn("Continuing request processing as if this node is not in a cluster")
    $cluster_name = ''
    $cluster_uuid = ''
  end
  if PCSD_RESTART_AFTER_REQUESTS > 0
    $request_counter += 1
    # Even though Puma is multi-threaded, we don't need to lock here since GVL
    # is only released during blocking I/O operations
    if $request_counter >= PCSD_RESTART_AFTER_REQUESTS
      $request_counter = 0
      # Start Puma hot restart
      $logger.debug('Requested a restart of Ruby server')
      Process.kill("USR2", Process.pid)
    end
  end
end

configure do
  PCS = get_pcs_path()
  PCS_INTERNAL = get_pcs_internal_path()
  $logger = configure_logger()
  early_log($logger)
  capabilities, capabilities_pcsd = get_capabilities($logger)
  CAPABILITIES = capabilities.freeze
  CAPABILITIES_PCSD = capabilities_pcsd.freeze
end

if Rack.const_defined?(:QueryParser) and Rack::QueryParser.const_defined?(:QueryLimitError)
  error Rack::QueryParser::QueryLimitError do
    $logger.warn(env['sinatra.error'].message)
    return 400, env['sinatra.error'].message
  end
end

get '/remote/?:command?' do
  return remote(params, request, @auth_user)
end

post '/remote/?:command?' do
  return remote(params, request, @auth_user)
end

post '/run_pcs' do
  std_in = params['stdin'] || nil
  begin
    command = JSON.parse(params['command'] || '[]')
    options = JSON.parse(params['options'] || '[]')
  rescue JSON::ParserError
    result = {
      'status' => 'error',
      'data' => {},
    }
    return JSON.pretty_generate(result)
  end
  # Do not reveal potentially sensitive information: remove --debug and all its
  # prefixes since getopt parser in pcs considers them equal to --debug.
  debug_items = ["--de", "--deb", "--debu", "--debug"]
  options_sanitized = []
  options.each { |item|
    options_sanitized << item unless debug_items.include?(item)
  }

  allowed_commands = {
    ['cluster', 'auth', '...'] => {
      'only_superuser' => false,
      'permissions' => nil,
    },
    # runs on the local node, check permissions
    ['cluster', 'corosync'] => {
      'only_superuser' => false,
      'permissions' => Permissions::READ,
    },
    # runs on a remote node which checks permissions by itself
    ['cluster', 'corosync', '...'] => {
      'only_superuser' => false,
      'permissions' => nil,
    },
    ['cluster', 'destroy', '...'] => {
      'only_superuser' => false,
      'permissions' => Permissions::FULL,
    },
    # runs on the local node, check permissions
    ['cluster', 'disable'] => {
      'only_superuser' => false,
      'permissions' => Permissions::WRITE,
    },
    # runs on a remote node which checks permissions by itself
    ['cluster', 'disable', '...'] => {
      'only_superuser' => false,
      'permissions' => nil,
    },
    # runs on the local node, check permissions
    ['cluster', 'enable'] => {
      'only_superuser' => false,
      'permissions' => Permissions::WRITE,
    },
    # runs on a remote node which checks permissions by itself
    ['cluster', 'enable', '...'] => {
      'only_superuser' => false,
      'permissions' => nil,
    },
    ['cluster', 'node', '...'] => {
      'only_superuser' => false,
      'permissions' => Permissions::FULL,
    },
    # runs on the local node, check permissions
    ['cluster', 'start'] => {
      'only_superuser' => false,
      'permissions' => Permissions::WRITE,
    },
    # runs on a remote node which checks permissions by itself
    ['cluster', 'start', '...'] => {
      'only_superuser' => false,
      'permissions' => nil,
    },
    # runs on the local node, check permissions
    ['cluster', 'stop'] => {
      'only_superuser' => false,
      'permissions' => Permissions::WRITE,
    },
    # runs on a remote node which checks permissions by itself
    ['cluster', 'stop', '...'] => {
      'only_superuser' => false,
      'permissions' => nil,
    },
    ['cluster', 'sync', '...'] => {
      'only_superuser' => false,
      'permissions' => Permissions::FULL,
    },
    ['config', 'restore', '...'] => {
      'only_superuser' => false,
      'permissions' => Permissions::FULL,
    },
    ['host', 'auth', '...'] => {
      'only_superuser' => false,
      'permissions' => nil,
    },
    ['host', 'deauth', '...'] => {
      'only_superuser' => false,
      'permissions' => nil,
    },
    ['pcsd', 'deauth', '...'] => {
      'only_superuser' => false,
      'permissions' => nil,
    },
    ['pcsd', 'status', '...'] => {
      'only_superuser' => false,
      'permissions' => nil,
    },
    ['pcsd', 'sync-certificates', '...'] => {
      'only_superuser' => false,
      'permissions' => Permissions::FULL,
    },
    ['quorum', 'device', 'status', '...'] => {
      'only_superuser' => false,
      'permissions' => Permissions::READ,
    },
    ['quorum', 'status', '...'] => {
      'only_superuser' => false,
      'permissions' => Permissions::READ,
    },
    ['status'] => {
      'only_superuser' => false,
      'permissions' => Permissions::READ,
    },
    ['status', 'corosync', '...'] => {
      'only_superuser' => false,
      'permissions' => Permissions::READ,
    },
    ['status', 'pcsd', '...'] => {
      'only_superuser' => false,
      'permissions' => nil,
    },
    ['status', 'quorum', '...'] => {
      'only_superuser' => false,
      'permissions' => Permissions::READ,
    },
    ['status', 'status', '...'] => {
      'only_superuser' => false,
      'permissions' => Permissions::READ,
    },
  }
  allowed = false
  command_settings = {}
  allowed_commands.each { |allowed_cmd, cmd_settings|
    if command == allowed_cmd \
      or \
      (allowed_cmd[-1] == '...' and allowed_cmd[0..-2] == command[0..(allowed_cmd.length - 2)])
      then
        allowed = true
        command_settings = cmd_settings
        break
    end
  }
  if !allowed
    result = {
      'status' => 'bad_command',
      'data' => {},
    }
    return JSON.pretty_generate(result)
  end

  if command_settings['only_superuser']
    if not allowed_for_superuser(@auth_user)
      return 403, 'Permission denied'
    end
  end
  if command_settings['permissions']
    if not allowed_for_local_cluster(@auth_user, command_settings['permissions'])
      return 403, 'Permission denied'
    end
  end

  runner_options = {}
  runner_options['stdin'] = std_in if std_in
  std_out, std_err, retval = run_cmd_options(
    @auth_user, runner_options, PCS, *options_sanitized, '--', *command
  )
  result = {
    'status' => 'ok',
    'data' => {
      'stdout' => std_out.join(""),
      'stderr' => std_err.join(""),
      'code' => retval,
    },
  }
  return JSON.pretty_generate(result)
end

### urls related to creating a new cluster - begin
#
# Creating a new cluster consists of several steps which are directed from js.
# Each url provides an action. The actions together orchestrated by js achieve
# creating a new cluster including various checks, the cluster setup itself and
# adding the new cluster into the gui.

# use case:
# - js is already authenticated to all future cluster nodes
# - js calls this url to send its tokens to a future cluster node
# - later, js will instruct that node to setup the cluster
# - the node will distribute the tokens in the cluster
post '/manage/send-known-hosts-to-node' do
  auth_user = getAuthUser()
  if not allowed_for_superuser(auth_user)
    return 403, 'Permission denied.'
  end
  return pcs_compatibility_layer_known_hosts_add(
    auth_user, false, params[:target_node], params[:node_names]
  )
end

# use case:
# - js asks us if specified cluster name and/or node names are available
get '/manage/can-add-cluster-or-nodes' do
  # This is currently used form cluster setup and node add, both of which
  # require hacluster user anyway. If needed to be used anywhere else, consider
  # if hacluster should be required.
  auth_user = getAuthUser()
  if not allowed_for_superuser(auth_user)
    return 403, 'Permission denied.'
  end

  pcs_config = PCSConfig.new(get_pcs_settings_conf())
  errors = []

  if params.include?(:cluster)
    if pcs_config.is_cluster_name_in_use(params[:cluster])
      errors << __msg_cluster_name_already_used(params[:cluster])
    end
  end

  if params.include?(:node_names)
    params[:node_names].each { |node_name|
      cluster_name = pcs_config.get_nodes_cluster(node_name)
      if cluster_name
        errors << __msg_node_name_already_used(node_name, cluster_name)
      end
    }
  end

  unless errors.empty?
    return 400, errors.join("\n")
  end
  return 200, ""
end

post '/manage/api/v1/cluster-setup' do
  auth_user = getAuthUser()
  if not allowed_for_superuser(auth_user)
    return 403, 'Permission denied.'
  end
  if params[:target_node] and params[:setup_data]
    return send_request_with_token(
      auth_user,
      params[:target_node],
      '/api/v1/cluster-setup/v1',
      true, # post
      {}, # data - useless when there are raw_data
      false, # remote
      params[:setup_data] # raw_data
    )
  end
end

# use case:
# - js instructs us to add the just created cluster to our list of clusters
#
# post /manage/remember-cluster - moved into Python

### urls related to creating a new cluster - end

get '/manage/check_auth_against_nodes' do
  auth_user = getAuthUser()
  node_list = []
  if params[:node_list].is_a?(Array)
    node_list = params[:node_list]
  end
  node_results = {}
  online, offline, notauthorized = is_auth_against_nodes(auth_user, node_list)
  online.each { |node|
    node_results[node] = 'Online'
  }
  offline.each { |node|
    node_results[node] = 'Offline'
  }
  notauthorized.each { |node|
    node_results[node] = 'Unable to authenticate'
  }
  return JSON.generate(node_results)
end

get '/imported-cluster-list' do
  imported_cluster_list(params, request, getAuthUser())
end

post '/managec/:cluster/permissions_save/?' do
  auth_user = getAuthUser()
  new_params = {
    'json_data' => JSON.generate(params)
  }
  return send_cluster_request_with_token(
    auth_user, params[:cluster], "set_permissions", true, new_params
  )
end

get '/managec/:cluster/cluster_status' do
  auth_user = getAuthUser()
  cluster_status_gui(auth_user, params[:cluster])
end

post '/managec/:cluster/fix_auth_of_cluster' do
  clustername = params[:cluster]
  unless clustername
    return [400, "cluster name not defined"]
  end

  retval = pcs_compatibility_layer_known_hosts_add(
    PCSAuth.getSuperuserAuth(), true, clustername, get_cluster_nodes(clustername)
  )
  if retval == 'not_supported'
    return [400, "Old version of PCS/PCSD is running on cluster nodes. Fixing authentication is not supported. Use 'pcs host auth' command to authenticate the nodes."]
  elsif retval == 'error'
    return [400, "Authentication failed."]
  end
  return [200, "Auhentication of nodes in cluster should be fixed."]
end

post '/managec/:cluster/send-known-hosts' do
  # send
  #   data (token, dest_list) of nodes in params[:node_names]
  #   to nodes that belongs to a cluster with name params[:cluster]
  auth_user = getAuthUser()
  if not allowed_for_superuser(auth_user)
    return 403, 'Permission denied.'
  end
  return pcs_compatibility_layer_known_hosts_add(
    auth_user, true, params[:cluster], params[:node_names]
  )
end

def pcs_compatibility_layer_known_hosts_add(
  auth_user, is_cluster_request, target, host_list
)
  known_hosts = get_known_hosts().select { |name, obj|
    host_list.include?(name)
  }
  known_hosts_request_data = {}
  known_hosts.each { |host_name, host_obj|
    known_hosts_request_data[host_name] = {
      'dest_list' => host_obj.dest_list,
      'token' => host_obj.token,
    }
  }
  request_data = {
    'data_json' => JSON.generate(
      {
        'known_hosts_add' => known_hosts_request_data,
        'known_hosts_remove' => [],
      }
    ),
  }
  if is_cluster_request
    retval, _out = send_cluster_request_with_token(
      auth_user, target, '/known_hosts_change', true, request_data
    )
  else
    retval, _out = send_request_with_token(
      auth_user, target, '/known_hosts_change', true, request_data
    )
  end

  if retval == 200
    return 'success'
  end

  if retval == 404
    return 'not_supported'
  end
  return 'error'
end

post '/managec/:cluster/api/v1/:command' do
  auth_user = getAuthUser()
  if params[:cluster] and params[:command]
    request.body.rewind
    return send_cluster_request_with_token(
      auth_user,
      params[:cluster],
      '/api/v1/' + params[:command] + '/v1',
      true, # post
      {}, # data - useless when there are raw_data
      false, # remote
      request.body.read # raw_data
    )
  end
end

get '/managec/:cluster/api/v1/:command' do
  auth_user = getAuthUser()
  if params[:cluster] and params[:command]
    request.body.rewind
    return send_cluster_request_with_token(
      auth_user,
      params[:cluster],
      '/api/v1/' + params[:command] + '/v1',
      false, # post
      {}, # data - useless when there are raw_data
      false, # remote
      request.body.read # raw_data
    )
  end
end

post '/managec/:cluster/?*' do
  auth_user = getAuthUser()
  request.body.rewind
  raw_data = request.body.read
  if params[:cluster]
    request = "/" + params[:splat].join("/")

    return send_cluster_request_with_token(
      auth_user, params[:cluster], request, true, params, true, raw_data
    )
  end
end

get '/managec/:cluster/?*' do
  auth_user = getAuthUser()
  request.body.rewind
  raw_data = request.body.read
  if params[:cluster]
    request = "/" + params[:splat].join("/")
    code, out = send_cluster_request_with_token(
      auth_user, params[:cluster], request, false, params, true, raw_data
    )
    return code, out
  end
end

get '*' do
  $logger.debug "Bad URL"
  $logger.debug params[:splat]
  $logger.info "Redirecting '*'...\n"
  redirect '/manage'
  redirect "Bad URL"
  call(env.merge("PATH_INFO" => '/nodes'))
end
