#!/usr/bin/env oo-ruby

#--
# Copyright 2012 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#++

require 'rubygems'
require 'getoptlong'
require 'time'

def usage
  puts <<USAGE
== Synopsis

#{$0}: Check all user applications

== Usage

#{$0} OPTIONS

Options:
-v|--verbose
    Print information about each check being performed
-l|--level [0,1]
    Level '0' is default, with level '1' extra checks are performed such as integrity of consumed_gears count
-h|--help
    Show Usage info
USAGE
  exit 255
end

args = {}
begin
  opts = GetoptLong.new(
    ["--verbose",          "-v", GetoptLong::NO_ARGUMENT],
    ["--level",            "-l", GetoptLong::REQUIRED_ARGUMENT],
    ["--help",             "-h", GetoptLong::NO_ARGUMENT]
  )
  opts.each{ |k,v| args[k]=v }
rescue GetoptLong::Error => e
  usage
end

level = args["--level"].to_i || 0
verbose = args["--verbose"]
usage if args["--help"]

require "/var/www/openshift/broker/config/environment"
# Disable analytics for admin scripts
Rails.configuration.analytics[:enabled] = false
Rails.configuration.msg_broker[:rpc_options][:disctimeout] = 20

def datastore_has_gear?(gear_uuid, app_uuid=nil)
  $false_positive_check_cnt += 1
  if $false_positive_check_cnt < FALSE_POSITIVE_CHECK_LIMIT
    query = { "group_instances.gears.uuid" => gear_uuid }
    query['uuid'] = app_uuid if app_uuid
    return Application.where(query).exists?
  else
    return true
  end
end

FALSE_POSITIVE_CHECK_LIMIT = 4000
$false_positive_check_cnt = 0
no_error = true
summary = []

datastore_hash = {}
puts "Started at: #{Time.now}"
start_time = (Time.now.to_f * 1000).to_i
query = {"group_instances.gears.0" => {"$exists" => true}}
selection = {:fields => ["name",
                       "created_at",
                       "domain_id",
                       "group_instances.gears.uuid",
                       "group_instances.gears.uid",
                       "group_instances.gears.server_identity",
                       "group_instances._id"], :timeout => false}
ret = []

user_hash = {}
domain_hash = {}
if level==1
  OpenShift::DataStore.find(:cloud_users, {}, {:fields => ["consumed_gears", "login"], :timeout => false}) do |user|
    user_hash[user["_id"]] = {"consumed_gears" => user["consumed_gears"],
                              "domains" => {},
                              "login" => user["login"] } 
  end

  OpenShift::DataStore.find(:domains, {}, {:fields => ["owner_id"], :timeout => false}) { |domain|
    owner_id = domain["owner_id"]
    domain_hash[domain["_id"]] = owner_id
    if !user_hash[owner_id]
      OpenShift::DataStore.find(:cloud_users, {"_id" => owner_id.to_s}, {:fields => ["consumed_gears", "login"], :timeout => false}) do |user|
        user_hash[user["_id"]] = {"consumed_gears" => user["consumed_gears"],
                                  "domains" => {},
                                  "login" => user["login"] } 
      end
    end

    user_hash[owner_id]["domains"][domain["_id"]] = 0 if user_hash[owner_id]
  } 
end

OpenShift::DataStore.find(:applications, query, selection) do |app|
    gear_count = 0
    owner_id = nil
    login = nil
    creation_time = app['created_at']
    domain_id = app['domain_id']
    if level==1
      owner_id = domain_hash[domain_id]
      login = user_hash[owner_id]["login"]
    end
    if app['group_instances']
      app['group_instances'].each { |gi|
        if gi['gears']
          gi['gears'].each { |gear|
            gear_count += 1
            datastore_hash[gear['uuid'].to_s] = [login, creation_time, gear['uid'], gear['server_identity'], app["uuid"] ]
          }
        else
          puts "ERROR: Group instance '#{gi['_id']}' for application: '#{app['name']}/#{app['uuid']}' doesn't have any gears"
          no_error = false
        end
      }
      user_hash[owner_id]["domains"][domain_id] += gear_count if level==1
    else
      puts "ERROR: Application: '#{app['name']}/#{app['uuid']}' doesn't have any group instances"
      no_error = false
    end
end

total_time = (Time.now.to_f * 1000).to_i - start_time
puts "Time to fetch mongo data: #{total_time.to_f/1000}s"
puts "Total gears found in mongo: #{datastore_hash.length}"

if level==1
  user_hash.each do |owner_id, owner_hash|
    total_gears = 0
    owner_hash["domains"].each { |dom_id, domain_gear_count| total_gears += domain_gear_count }
   
    if owner_hash['consumed_gears'] != total_gears
      msg = "FAIL - user #{owner_hash['login']} has a mismatch in consumed gears (#{owner_hash['consumed_gears']}) and actual gears (#{total_gears})!"
      puts msg if verbose
      summary << msg
      no_error = false
    else
      puts "OK - user #{owner_hash['login']} has consumed_gears equal to actual gears (#{total_gears})!" if verbose
    end
  end
end

get_all_gears_start_time = (Time.now.to_f * 1000).to_i
node_hash, sender_hash = OpenShift::ApplicationContainerProxy.get_all_gears
total_time = (Time.now.to_f * 1000).to_i - get_all_gears_start_time
puts "Time to get all gears from nodes: #{total_time.to_f/1000}s"
puts "Total gears found on the nodes: #{node_hash.length}"
puts "Total nodes that responded : #{sender_hash.length}"

# now check
puts "Checking application gears on corresponding nodes:" if verbose
datastore_hash.each { |gear_uuid, gear_info|
  login = gear_info[0]
  creation_time = gear_info[1]
  server_identity = gear_info[3]
  app_uuid = gear_info[4]
  print "#{gear_uuid} : #{gear_uuid.class}...\t" if verbose
  if (Time.now - creation_time) > 600
    if not node_hash.has_key? gear_uuid
      if sender_hash.has_key? server_identity
        if datastore_has_gear?(gear_uuid, app_uuid)
          no_error = false
          puts "FAIL" if verbose
          summary << "Gear #{gear_uuid} does not exist on any node"
        elsif verbose
          puts "OK"
        end
      else
        no_error = false
        puts "FAIL" if verbose
        summary << "The node #{server_identity} with gear #{gear_uuid} wasn't returned from mcollective"
      end
    elsif verbose
      puts "OK"
    end
  elsif verbose
    puts "OK"
  end
}

# now check reverse
puts "Checking node gears in application database:" if verbose
node_hash.each { |gear_uuid, gear_info|
  print "#{gear_uuid}...\t" if verbose
  datastore_gear_info = datastore_hash[gear_uuid]
  if !datastore_gear_info
    if !datastore_has_gear?(gear_uuid)
      no_error = false
      puts "FAIL" if verbose
      summary << "Gear #{gear_uuid} exists on node #{gear_info[0]} (uid: #{gear_info[1]}) but does not exist in mongo database"
    elsif verbose
      puts "OK"
    end
  else
    puts "OK" if verbose
    if !datastore_gear_info[2].nil?
      begin
        uid = gear_info[1]
        if uid != datastore_gear_info[2].to_i
          summary << "Gear #{gear_uuid} is using uid: '#{uid}' but has reserved uid: '#{datastore_gear_info[2].to_i}'"
          no_error = false
        end
      rescue Exception => e
        summary << "Failed to check gear: '#{gear_uuid}'s uid because of exception: #{e.message}"
        no_error = false
      end
    end
  end
}

puts no_error ? "Success" : "Check failed.\n #{summary.join("\n")}"
if $false_positive_check_cnt >= FALSE_POSITIVE_CHECK_LIMIT
  puts "WARNING: Only checked the first #{FALSE_POSITIVE_CHECK_LIMIT} errors for false positives."
end
total_time = (Time.now.to_f * 1000).to_i - start_time
puts "Total time: #{total_time.to_f/1000}s"
puts "Finished at: #{Time.now}"
exit (no_error ? 0 : 1)
