#!/usr/bin/ruby

# Copyright RightScale, Inc. All rights reserved.  All access and use subject to the
# RightScale Terms of Service available at http://www.rightscale.com/terms.php and,
# if applicable, other agreements such as a RightScale Master Subscription Agreement.

#
# report_tool: Parses system calls and hint files to return a JSON summary of the running system.
#

require 'rubygems'
# JSON not in core-ruby.
require 'json'
# Platform checking.
require 'rbconfig'

# Monkeypatch to recursively clean empty hashes.
class Hash
  def rec_delete_empty
    delete_if{|k, v| v.nil? or v.empty? or v.instance_of?(Hash) && v.rec_delete_empty.empty?}
  end
end


# Borrowed from ohai and modifies to detect hypervisor in rebundles.
def hyperChecker
  #
  # Modifed by Drew Waranis.
  # RightScale, Inc. 2012
  #

  #
  # Author:: Thom May (<thom@clearairturbulence.org>)
  # Copyright:: Copyright (c) 2009 Opscode, Inc.
  # License:: Apache License, Version 2.0
  #
  # 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.
  #

  virtualization = Hash.new

  # Try virt-what first.
  if File.exists?("/usr/sbin/virt-what")
    return `/usr/sbin/virt-what`.split("\n").first
  end

  # Xen
  # /proc/xen is an empty dir for EL6 + Linode Guests
  if File.exists?("/proc/xen"); return "xen"; end

  # Xen Notes:
  # - cpuid of guests, if we could get it, would also be a clue
  # - may be able to determine if under paravirt from /dev/xen/evtchn (See OHAI-253)
  # - EL6 guests carry a 'hypervisor' cpu flag
  # - Additional edge cases likely should not change the above assumptions
  #   but rather be additive - btm

  # Detect from kernel module
  if File.exists?("/proc/modules")
    if File.read("/proc/modules") =~ /^kvm/; return "vbox"; end
  end

  # Detect KVM/QEMU from cpuinfo, report as KVM
  # We could pick KVM from 'Booting paravirtualized kernel on KVM' in dmesg
  # 2.6.27-9-server (intrepid) has this / 2.6.18-6-amd64 (etch) does not
  # It would be great if we could read pv_info in the kernel
  # Wait for reply to: http://article.gmane.org/gmane.comp.emulators.kvm.devel/27885
  if File.exists?("/proc/cpuinfo")
    if File.read("/proc/cpuinfo") =~ /QEMU Virtual CPU/; return "kvm"; end
  end

  # Detect OpenVZ / Virtuozzo.
  # http://wiki.openvz.org/BC_proc_entries
  if File.exists?("/proc/vz"); return "openvz"; end

  # http://www.dmo.ca/blog/detecting-virtualization-on-linux
  if File.exists?("/usr/sbin/dmidecode")
    dmi_info = `/usr/sbin/dmidecode`
    case dmi_info
    when /Manufacturer: Microsoft/ then return "virtualpc"
    when /Manufacturer: VMware/ then return "vmware"
    when /Manufacturer: Xen/ then return "xen"
    end
  end

  # Detect Linux-VServer
  if File.exists?("/proc/self/status")
    vxid = File.read("/proc/self/status").match(/^(s_context|VxID): (\d+)$/)
    if vxid and vxid[2] then return "linux-vserver"; end
  end

  return nil;
end
# End hypervisor checker.


# JSON class infrastructure.

# Inform parser what platform file is from.
class OS
  def initialize() 
    # If it contains linux then Linux, otherwise Windows (future dev).
    if Config::CONFIG["host_os"] =~ /linux/i
      @os = "linux"
    else  
      @os = "windows"
    end 
  end

  def to_hash(*a) {"os" => @os}.rec_delete_empty end 
                                # Strip empty values
end


# Linux Standard Base.
# lsb_release: id (i), description (d), release (r), codename (c) .
class LSB
  def initialize()
    # Retrieve and split command output.
    lsb = `lsb_release -ircs`.split

    @id = lsb[0]
    # Called separately to get full description with spaces.
    # Sanitize newline and quotes.
    @description = `lsb_release -ds`.sub("\n",'').gsub("\"",'')
    @release = lsb[1]
    @codename = lsb[2]
  end

  def to_hash(*a)
    { "lsb" =>
      {"id" => @id,
       "description" => @description,
       "release" => @release,
       "codename" => @codename} 
    }.rec_delete_empty
  end
end

# Kernel release name.
# Retrieved from /boot/grub/grub.conf if it is available,
  # else from menu.lst if is available,
  # else from the vmlinuz filename if it is available,
  # else nil.
# rec_empty_delete strips empty and nil values.
class Kern
  def initialize()
    # kernel-release is on the first line beginning with "(optional whitespace)initrd".
    # It is located after the first "-" and should not include an ending ".img", if present.
    if File.exists? "/boot/grub/grub.conf"
      @release = IO.read("/boot/grub/grub.conf").match(/^\s*initrd[^-]*-(.*?)(\.img)?$/)[1]
    elsif File.exists? "/boot/grub/menu.lst"
      @release = IO.read("/boot/grub/menu.lst").match(/^\s*initrd[^-]*-(.*?)(\.img)?$/)[1]
    # As a fallback, get kernel-release from the newest vmlinux filename.
    elsif Dir.entries("/boot/grub/").grep(/vmlinuz*/)
      @release = (Dir.glob("/boot/vmlinuz*").max_by {|f| File.mtime(f)}).match(/vmlinuz[^-]*-(.*)/)[1]
    else
      @release = nil
    end
  end

  def to_hash(*a)
    {"kernel" => 
      {"release" => @release}
    }.rec_delete_empty
  end
end

# Report the presence (and features they enable) of selected modules.
class Mods
  def initialize(release)

    # Cancel if kernel version wasn't determined.
    if release.nil?
      @mods = nil
    end

    # Manual ist of which modules to check.
    # Also update the case-statment below.
    modules_list = %w"dm-mod xfs"

    # Set locations based on kernel version.
    kernel_config = "/boot/config-#{release}"
    modules_dir = "/lib/modules/#{release}"

    # Returned by the function to merge into the Kern hash.
    @mods = Hash.new

    modules_list.each do |mod|
      case mod
      when "dm-mod"
        # Pretty print name.
        feature = "LVM2 (dm-mod)"
        # Flag name in the kerne's config file.
        flag = "CONFIG_BLK_DEV_DM"
      when "xfs"
        feature = "XFS (xfs)"
        flag = "CONFIG_XFS_FS"
      else
        # If module is not defined here, then skip.
        next
      end

      # Check the kernel's config file, if it exists.
      if File.exists? "#{kernel_config}"
        # Check what the flag is set to.
        case `cat #{kernel_config} | grep #{flag}=`.chomp.split("=")[1]
        when "y"
          status = "Built-In"
        when "m"
          status = "Module"
        when "n"
          status = "Not Selected"
        end
      # As a backup, check if it is dynamically compiled in /lib/modules .
      elsif `find #{modules_dir} -name "#{mod}.ko"`
        status = "Module"
      else
        status = "Not Available"
      end
      @mods[feature] = status
    end
  end
  # Strips value if nil.
  def to_hash(*a)
    {"modules" => @mods }.rec_delete_empty
  end
end

# List packages on Linux system.
# Takes the LSB's id as an argument.
class Packages
  def initialize(id)
    # Prep packages hash
    packs = Hash.new

    # Linux distro family specific options:
      # Ubuntu = dpkg,
      # CentOS/RHEL = rpm,
        # or exit.
 
    case id
      when "Ubuntu"
        # Read packages into a hash.
        `dpkg-query -W`.each_line{ |line|
          col = line.split 
          packs[col[0]] = col[1]
          }

      when "CentOS", /RedHat/
        # Read packages into a hash.
        `rpm -qa --qf "%{NAME}\t%{VERSION}\n"`.each_line{ |line|
              col = line.split
              packs[col[0]] = col[1]
              }
      else
        packs["This distro"] = "is not supported."
        exit
      end

    # Store in instance variable to extract rightlink version.
    @packages = packs
  end

  def to_hash(*a)
    { "packages" => @packages }.rec_delete_empty
  end
end


# Holds RS specific info.
# Takes hint hash as arg.
class RightScaleMirror
  def initialize(hint)
    @repo_freezedate = hint["freeze-date"]
    @rubygems_freezedate = hint["freeze-date"]
    @rightlink_version = hint["rightlink-version"]
  end

  def to_hash(*a)
    {"rightscale-mirror" =>
      {"repo-freezedate" => @repo_freezedate, 
      "rubygems-freezedate" => @rubygems_freezedate,
      "rightlink-version" => @rightlink_version
      }
    }.rec_delete_empty
  end
end

# Holds info about the image.
# MD5 sums added to report_hash in later step.
# Takes hint hash as arg.
class Image
  def initialize(hint)
    @build_date = hint["build-date"]
  end

  def to_hash(*a)
    {"image" => 
      {"build-date" => @build_date} 
    }.rec_delete_empty
  end
end

# Holds the type and version of hypervisor.
# Takes hint hash as arg.
class Virtualization
  def initialize(hint)
    # If the entry is in the hint file, use it.
    if not hint["hypervisor"].nil?
      @hypervisor = hint["hypervisor"]
    # Checks if report_tool is being chrooted.
    # If not, check for hypervisor.
    # Chomp return character.
    elsif `if [ "$(stat -c %d:%i /)" == "$(stat -c %d:%i /proc/1/root/. 2>/dev/null)" ];
           then echo "true";
           else echo "false"; 
           fi`.chomp == "true"
      @hypervisor = hyperChecker
    # report_tool is running in a chroot and and the hypervisor cannot be properly determined.
    else
      @hypervisor = nil;
    end
  end

  def to_hash(*a)
    {"virtualization" => 
      {"hypervisor" => @hypervisor,
       "version" => @version}
    }.rec_delete_empty
  end
end

# Name of the cloud the image is meant for.
class Cloud
  def initialize()
    # Safely ignores hint if not available.
    if File.exists? "/etc/rightscale.d/cloud"
      @provider = File.open("/etc/rightscale.d/cloud", &:readline)
    else
      @provider = nil
    end 
  end
  # Strips value if nil.
  def to_hash(*a)
    {"cloud" => 
      {"provider" => @provider}
    }.rec_delete_empty
  end
end

# End JSON class infrastructure.

# Merge JSON of classes into report_hash.
report_hash = Hash.new
report_hash.merge!(OS.new)
# Switch on OS.
if report_hash["os"] != "linux"
  puts "Windows support is coming... soon."
  exit
end

# And the rest.
report_hash.merge!(LSB.new)
report_hash.merge!(Kern.new)
# Take kernel version as argument.
report_hash.merge!(Mods.new(report_hash["kernel"]["release"]))
report_hash.merge!(Cloud.new)

# Take platform as argument.
report_hash.merge!(Packages.new(report_hash["lsb"]["id"]))

# Give hint hash.
if File.exists? "/etc/rightscale.d/rightimage-release.js"
  hint = JSON.parse(File.read("/etc/rightscale.d/rightimage-release.js"))
# Otherwise give empty hint hash.
else
  hint = Hash.new
end  
  
# Receive hint.
report_hash.merge!(RightScaleMirror.new(hint))
report_hash.merge!(Image.new(hint))
report_hash.merge!(Virtualization.new(hint))

# Print results if flag is set.
if(ARGV[0] == "print" )
  puts JSON.pretty_generate(report_hash)
end

# Save JSON to /tmp.
File.open("/tmp/report.js","w") do |f|
  f.write(JSON.pretty_generate(report_hash))
end