#!/usr/bin/env ruby

require 'rubygems'
require 'uri'
require 'net/https'
require 'json'
require 'pp'

###############################################################################
# The CloudClient module contains general functionality to implement a
# Cloud Client
###############################################################################
module CloudClient

    # OpenNebula version
    VERSION = '4.14.0'

    # #########################################################################
    # Default location for the authentication file
    # #########################################################################
    DEFAULT_AUTH_FILE = ENV["HOME"]+"/.one/one_auth"

    # #########################################################################
    # Gets authorization credentials from ONE_AUTH or default
    # auth file.
    #
    # Raises an error if authorization is not found
    # #########################################################################
    def self.get_one_auth
        if ENV["ONE_AUTH"] and !ENV["ONE_AUTH"].empty? and
            File.file?(ENV["ONE_AUTH"])
            one_auth=File.read(ENV["ONE_AUTH"]).strip.split(':')
        elsif File.file?(DEFAULT_AUTH_FILE)
            one_auth=File.read(DEFAULT_AUTH_FILE).strip.split(':')
        else
            raise "No authorization data present"
        end

        raise "Authorization data malformed" if one_auth.length < 2

        one_auth
    end

    # #########################################################################
    # Starts an http connection and calls the block provided. SSL flag
    # is set if needed.
    # #########################################################################
    def self.http_start(url, timeout, &block)
        host = nil
        port = nil

        if ENV['http_proxy']
            uri_proxy  = URI.parse(ENV['http_proxy'])
            host = uri_proxy.host
            port = uri_proxy.port
        end

        http = Net::HTTP::Proxy(host, port).new(url.host, url.port)

        if timeout
            http.read_timeout = timeout.to_i
        end

        if url.scheme=='https'
            http.use_ssl = true
            http.verify_mode=OpenSSL::SSL::VERIFY_NONE
        end

        begin
            res = http.start do |connection|
                block.call(connection)
            end
        rescue Errno::ECONNREFUSED => e
            str =  "Error connecting to server (#{e.to_s}).\n"
            str << "Server: #{url.host}:#{url.port}"

            return CloudClient::Error.new(str,"503")
        rescue Errno::ETIMEDOUT => e
            str =  "Error timeout connecting to server (#{e.to_s}).\n"
            str << "Server: #{url.host}:#{url.port}"

            return CloudClient::Error.new(str,"504")
        rescue Timeout::Error => e
            str =  "Error timeout while connected to server (#{e.to_s}).\n"
            str << "Server: #{url.host}:#{url.port}"

            return CloudClient::Error.new(str,"504")
        rescue SocketError => e
            str =  "Error timeout while connected to server (#{e.to_s}).\n"

            return CloudClient::Error.new(str,"503")
        rescue
            return CloudClient::Error.new($!.to_s,"503")
        end

        if res.is_a?(Net::HTTPSuccess)
            res
        else
            CloudClient::Error.new(res.body, res.code)
        end
    end

    # #########################################################################
    # The Error Class represents a generic error in the Cloud Client
    # library. It contains a readable representation of the error.
    # #########################################################################
    class Error
        attr_reader :message
        attr_reader :code

        # +message+ a description of the error
        def initialize(message=nil, code="500")
            @message=message
            @code=code
        end

        def to_s()
            @message
        end
    end

    # #########################################################################
    # Returns true if the object returned by a method of the OpenNebula
    # library is an Error
    # #########################################################################
    def self.is_error?(value)
        value.class==CloudClient::Error
    end
end

module OneGate
    module VirtualMachine
        VM_STATE=%w{INIT PENDING HOLD ACTIVE STOPPED SUSPENDED DONE FAILED
            POWEROFF UNDEPLOYED}

        LCM_STATE=%w{LCM_INIT PROLOG BOOT RUNNING MIGRATE SAVE_STOP SAVE_SUSPEND
            SAVE_MIGRATE PROLOG_MIGRATE PROLOG_RESUME EPILOG_STOP EPILOG
            SHUTDOWN CANCEL FAILURE CLEANUP_RESUBMIT UNKNOWN HOTPLUG SHUTDOWN_POWEROFF
            BOOT_UNKNOWN BOOT_POWEROFF BOOT_SUSPENDED BOOT_STOPPED CLEANUP_DELETE
            HOTPLUG_SNAPSHOT HOTPLUG_NIC HOTPLUG_SAVEAS HOTPLUG_SAVEAS_POWEROFF
            HOTPLUG_SAVEAS_SUSPENDED SHUTDOWN_UNDEPLOY EPILOG_UNDEPLOY
            PROLOG_UNDEPLOY BOOT_UNDEPLOY}

        SHORT_VM_STATES={
            "INIT"      => "init",
            "PENDING"   => "pend",
            "HOLD"      => "hold",
            "ACTIVE"    => "actv",
            "STOPPED"   => "stop",
            "SUSPENDED" => "susp",
            "DONE"      => "done",
            "FAILED"    => "fail",
            "POWEROFF"  => "poff",
            "UNDEPLOYED"=> "unde"
        }

        SHORT_LCM_STATES={
            "PROLOG"            => "prol",
            "BOOT"              => "boot",
            "RUNNING"           => "runn",
            "MIGRATE"           => "migr",
            "SAVE_STOP"         => "save",
            "SAVE_SUSPEND"      => "save",
            "SAVE_MIGRATE"      => "save",
            "PROLOG_MIGRATE"    => "migr",
            "PROLOG_RESUME"     => "prol",
            "EPILOG_STOP"       => "epil",
            "EPILOG"            => "epil",
            "SHUTDOWN"          => "shut",
            "CANCEL"            => "shut",
            "FAILURE"           => "fail",
            "CLEANUP_RESUBMIT"  => "clea",
            "UNKNOWN"           => "unkn",
            "HOTPLUG"           => "hotp",
            "SHUTDOWN_POWEROFF" => "shut",
            "BOOT_UNKNOWN"      => "boot",
            "BOOT_POWEROFF"     => "boot",
            "BOOT_SUSPENDED"    => "boot",
            "BOOT_STOPPED"      => "boot",
            "CLEANUP_DELETE"    => "clea",
            "HOTPLUG_SNAPSHOT"  => "snap",
            "HOTPLUG_NIC"       => "hotp",
            "HOTPLUG_SAVEAS"           => "hotp",
            "HOTPLUG_SAVEAS_POWEROFF"  => "hotp",
            "HOTPLUG_SAVEAS_SUSPENDED" => "hotp",
            "SHUTDOWN_UNDEPLOY" => "shut",
            "EPILOG_UNDEPLOY"   => "epil",
            "PROLOG_UNDEPLOY"   => "prol",
            "BOOT_UNDEPLOY"     => "boot"
        }

        def self.state_to_str(id, lcm_id)
            id = id.to_i
            state_str = VM_STATE[id]

            if state_str=="ACTIVE"
                lcm_id = lcm_id.to_i
                return LCM_STATE[lcm_id]
            end

            return state_str
        end

        def self.print(json_hash)
            OneGate.print_header("VM " + json_hash["VM"]["ID"])
            OneGate.print_key_value("NAME", json_hash["VM"]["NAME"])
            OneGate.print_key_value(
                "STATE",
                self.state_to_str(
                    json_hash["VM"]["STATE"],
                    json_hash["VM"]["LCM_STATE"]))

            vm_nics = [json_hash['VM']['TEMPLATE']['NIC']].flatten
            vm_nics.each { |nic|
                # TODO: IPv6
                OneGate.print_key_value("IP", nic["IP"])
            }
        end
    end

    module Service
        STATE = {
            'PENDING'            => 0,
            'DEPLOYING'          => 1,
            'RUNNING'            => 2,
            'UNDEPLOYING'        => 3,
            'WARNING'            => 4,
            'DONE'               => 5,
            'FAILED_UNDEPLOYING' => 6,
            'FAILED_DEPLOYING'   => 7,
            'SCALING'            => 8,
            'FAILED_SCALING'     => 9,
            'COOLDOWN'           => 10
        }

        STATE_STR = [
            'PENDING',
            'DEPLOYING',
            'RUNNING',
            'UNDEPLOYING',
            'WARNING',
            'DONE',
            'FAILED_UNDEPLOYING',
            'FAILED_DEPLOYING',
            'SCALING',
            'FAILED_SCALING',
            'COOLDOWN'
        ]

        # Returns the string representation of the service state
        # @param [String] state String number representing the state
        # @return the state string
        def self.state_str(state_number)
            return STATE_STR[state_number.to_i]
        end

        def self.print(json_hash)
            OneGate.print_header("SERVICE " + json_hash["SERVICE"]["id"])
            OneGate.print_key_value("NAME", json_hash["SERVICE"]["name"])
            OneGate.print_key_value("STATE", Service.state_str(json_hash["SERVICE"]['state']))
            puts

            roles = [json_hash['SERVICE']['roles']].flatten
            roles.each { |role|
                OneGate.print_header("ROLE " + role["name"], false)

                if role["nodes"]
                    role["nodes"].each{ |node|
                        OneGate::VirtualMachine.print(node["vm_info"])
                    }
                end

                puts
            }
        end
    end

    class Client
        def initialize(opts={})
            @vmid = ENV["VMID"]
            @token = ENV["TOKENTXT"]

            url = opts[:url] || ENV['ONEGATE_ENDPOINT']
            @uri = URI.parse(url)

            @user_agent = "OpenNebula #{CloudClient::VERSION} " <<
                "(#{opts[:user_agent]||"Ruby"})"

            @host = nil
            @port = nil

            if ENV['http_proxy']
                uri_proxy  = URI.parse(ENV['http_proxy'])
                @host = uri_proxy.host
                @port = uri_proxy.port
            end
        end

        def get(path)
            req = Net::HTTP::Proxy(@host, @port)::Get.new(path)

            do_request(req)
        end

        def delete(path)
            req =Net::HTTP::Proxy(@host, @port)::Delete.new(path)

            do_request(req)
        end

        def post(path, body)
            req = Net::HTTP::Proxy(@host, @port)::Post.new(path)
            req.body = body

            do_request(req)
        end

        def put(path, body)
            req = Net::HTTP::Proxy(@host, @port)::Put.new(path)
            req.body = body

            do_request(req)
        end

        def login
            req = Net::HTTP::Proxy(@host, @port)::Post.new('/login')

            do_request(req)
        end

        def logout
            req = Net::HTTP::Proxy(@host, @port)::Post.new('/logout')

            do_request(req)
        end

        private

        def do_request(req)
            req.basic_auth @username, @password

            req['User-Agent'] = @user_agent
            req['X-ONEGATE-TOKEN'] = @token
            req['X-ONEGATE-VMID'] = @vmid

            res = CloudClient::http_start(@uri, @timeout) do |http|
                http.request(req)
            end

            res
        end
    end

    def self.parse_json(response)
        if CloudClient::is_error?(response)
            puts "ERROR: "
            puts response.message
            exit -1
        else
            return JSON.parse(response.body)
        end
    end

    # Sets bold font
    def self.scr_bold
        print "\33[1m"
    end

    # Sets underline
    def self.scr_underline
        print "\33[4m"
    end

    # Restore normal font
    def self.scr_restore
        print "\33[0m"
    end

    # Print header
    def self.print_header(str, underline=true)
        if $stdout.tty?
            scr_bold
            scr_underline if underline
            print "%-80s" % str
            scr_restore
        else
            print str
        end
        puts
    end

    def self.print_key_value(key, value)
        puts "%-20s: %-20s" % [key, value]
    end

    def self.help_str
        return <<-EOT
Available commands
    $ onegate vm show [VMID] [--json]

    $ onegate vm update [VMID] --data KEY=VALUE[\\nKEY2=VALUE2]

    $ onegate vm ACTION VMID
        $ onegate resume [VMID]
        $ onegate stop [VMID]
        $ onegate suspend [VMID]
        $ onegate delete [VMID] [--hard]
        $ onegate shutdown [VMID] [--hard]
        $ onegate reboot [VMID] [--hard]
        $ onegate poweroff [VMID] [--hard]
        $ onegate resubmit [VMID]
        $ onegate resched [VMID]
        $ onegate unresched [VMID]
        $ onegate hold [VMID]
        $ onegate release [VMID]

    $ onegate service show [--json]

    $ onegate service scale --role ROLE --cardinality CARDINALITY
EOT
    end
end


require 'optparse'

options = {}
OptionParser.new do |opts|
  opts.on("-d", "--data DATA", "Data to be included in the VM") do |data|
    options[:data] = data
  end

  opts.on("-r", "--role ROLE", "Service role") do |role|
    options[:role] = role
  end

  opts.on("-c", "--cardinality CARD", "Service cardinality") do |cardinality|
    options[:cardinality] = cardinality
  end

  opts.on("-j", "--json", "Print resource information in JSON") do |json|
    options[:json] = json
  end

  opts.on("-f", "--hard", "Hard option for power off operations") do |hard|
    options[:hard] = hard
  end

  opts.on("-h", "--help", "Show this message") do
    puts OneGate.help_str
    exit
  end
end.parse!

client = OneGate::Client.new()

case ARGV[0]
when "vm"
    case ARGV[1]
    when "show"
        if ARGV[2]
            response = client.get("/vms/"+ARGV[2])
        else
            response = client.get("/vm")
        end

        json_hash = OneGate.parse_json(response)
        if options[:json]
            puts JSON.pretty_generate(json_hash)
        else
            OneGate::VirtualMachine.print(json_hash)
        end
    when "update"
        if !options[:data]
            puts "You have to provide the data as a param (--data)"
            exit -1
        end

        if ARGV[2]
            response = client.put("/vms/"+ARGV[2], options[:data])
        else
            response = client.put("/vm", options[:data])
        end

        if CloudClient::is_error?(response)
            puts "ERROR: "
            puts response.message
            exit -1
        end
    when "resume",
         "stop",
         "suspend",
         "delete",
         "shutdown",
         "reboot",
         "poweroff",
         "resubmit",
         "resched",
         "unresched",
         "hold",
         "release"
        if ARGV[2]
            action_hash = {
                "action" => {
                    "perform" => ARGV[1]
                }
            }

            if options[:hard]
                action_hash["action"]["params"] = true
            end

            response = client.post("/vms/"+ARGV[2]+"/action", action_hash.to_json)

            if CloudClient::is_error?(response)
                puts "ERROR: "
                puts response.message
                exit -1
            end
        else
            puts "You have to provide a VM ID"
            exit -1
        end
    else
        puts OneGate.help_str
        puts
        puts "Action #{ARGV[1]} not supported"
        exit -1
    end
when "service"
    case ARGV[1]
    when "show"
        response = client.get("/service")
        json_hash = OneGate.parse_json(response)
        #pp json_hash
        if options[:json]
            puts JSON.pretty_generate(json_hash)
        else
            OneGate::Service.print(json_hash)
        end
    when "scale"
        response = client.put(
            "/service/role/" + options[:role],
            {
                :cardinality => options[:cardinality]
            }.to_json)

        if CloudClient::is_error?(response)
            puts "ERROR: "
            puts response.message
            exit -1
        end
    else
        puts OneGate.help_str
        puts
        puts "Action #{ARGV[1]} not supported"
        exit -1
    end
else
    puts OneGate.help_str
    exit -1
end

