#!/usr/bin/env ruby
#
#A Ruby library to perform low-level Linode API functions.
#
#Copyright (c) 2008 David S Bell <dave@geordish.org>
#
#Permission is hereby granted, free of charge, to any person
#obtaining a copy of this software and associated documentation
#files (the "Software"), to deal in the Software without
#restriction, including without limitation the rights to use,
#copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the
#Software is furnished to do so, subject to the following
#conditions:
#
#The above copyright notice and this permission notice shall be
#included in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
#OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
#HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
#WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
#OTHER DEALINGS IN THE SOFTWARE.
#
#A link to the library can be found here[http://geordish.org/linodeapi/linodeapi.tar.gz]


require 'net/https'
require 'openssl' 

require 'rubygems'
require 'json'

LINODE_API_URL = "api.linode.com"

class ApiCore
	
  #  Initizlize the Ruby Linode API
  #  
  #  Parameters:
  #    APIKEY         - Unique key generated by Linode to access the commands
  #    DEBUG 		      - Set to true for debugging to be turned on
  #    BATCH          - Set to true to send requests in batch
  #
  #  Returns nothing
  
  def initialize(key, debug=false, batching=false)
    @key = key
    @debug = debug
    @batching = batching
    @waiting = Array.new if @batching
  end

  #  Retrieve the list of domains visible to the user.
  #
  #  Parameters:
  #    None
  #
  #  Returns an array of hashes, one per domain, each with the following fields:
  #    DOMAINID       - Unique identifier for the domain
  #    DOMAIN         - Domain's name e.g. 'linode.com'
  #    TYPE           - Domain's type: 'master' or 'slave'
  #    STATUS         - Domain's current status
  #      * Possible values are listed below
  #    SOA_EMAIL      - SOA email address for the domain
  #    REFRESH_SEC *  - 'refresh' value for the domain
  #      * A value of zero indicates default time (2 hours)
  #    RETRY_SEC *    - 'retry' value for the domain
  #      * A value of zero indicates default time (2 hours)
  #    TTL_SEC *      - 'ttl' value for the domain
  #      * A value of zero indicates default time (1 day)
  #
  #  Possible values for STATUS
  #    0 - Disabled   - Domain is not being served
  #    1 - Active     - Domain is being served
  #    2 - Edit Mode  - Domain is being served but changes are not rendered
  #    3 - Has Errors - There are errors in the rendered zonefile
  
  def domainList
    make_request this_method 
  end

  #  Retrieve the details for a specific domain.
  #
  #  Parameters:
  #    DomainID       - Unique identifier for the domain to be retrieved
  #      * Always required
  #
  #   Returns a hash for the requested domain
  #   See domainList documentation for the fields in the hash
  
  def domainGet args
    if not args.has_key?(:DomainID)
      raise "DomainID argument missing from argument list"
    end

    make_request this_method, args
  end

  #  Create or update a specific domain.
  #
  #  Parameters:
  #    DomainID       - Unique identifier for the domain
  #      * Always required - use 0 to insert a new domain
  #    Domain         - Domain's name e.g. 'linode.com'
  #      * Always required
  #    Type           - Domain's type: 'master' or 'slave'
  #      * Always required
  #    Status         - Domain's new status
  #      * Always required - see domainList documentation for possible values
  #    SOA_Email      - SOA email address for the domain
  #      * Reqyured if Type = 'master'
  #    Master_IPs     - Semicolon separated list of IP(s) for master servers
  #      * Required if Type = 'slave', ignored otherwise
  #    Refresh_Sec    - 'refresh' value for the domain
  #      * Excluding or setting to zero indicates default time (2 hours)
  #    Retry_Sec      - 'retry' value for the domain
  #      * Excluding or setting to zero indicates default time (2 hours)
  #    TTK_Sec        - 'ttl' value for the domain
  #      * Excluding or setting to zero indicates default time (1 day)
  #
  #  Returned fields:
  #  DOMAINID       - Unique identifier for the new or updated domain
  #
  #  *** Parameters not passed to update a domain will be reset to defaults ***
  
  def domainSave args
    
    # Input validation
    if not args.has_key?(:DomainID) 
      raise "DomainID argument missing from argument list" 
    end

    if not args.has_key?(:Type)
      raise "Type argument missing from argument list"
    end

    if not args.has_key?(:Domain)
      raise "Domain argument missing from argument list"
    end

    if not args.has_key?(:Status)
      raise "Status argument missing from argument list"
    end

    # If it is a master record, there must be an soa email
    if args[:Type].downcase == "master"
      if not args[:SOA_Email] =~/^[\w_\-+]+@[\w\-]+\.[\w]{2,3}$/
          raise "SOA_EMAIL must be a valid email address"
      end
    # If it is a slave record, there must be valid master ips
    elsif args[:Type].downcase == "slave"
      if not args[:Master_IPs]=~/^(\d{1,3}\.\d{1,3}.\d{1,3}\.\d{1,3};?)+$/
        raise "Valid MASTER_IPS must be supplied for type slave"
      end
    # Otherwise its not a valid record
    else
      raise "Zone type is not master or slave"
    end

    make_request this_method, args
  end
  
  #  Delete a specific domain/zone
  #
  #  Parameters:
  #    DomainID      - The unique identifier for this Domain/Zone
  #
  #  Returns DOMAINID
  
  def domainDelete args
    if not args.has_key?(:DomainID)
      raise "DomainID argument missing from argument list"
    end

    make_request this_method, args
  end
  
  #  Retrieve the list of resource records (RRs) for a specific domain.
  #    
  #  Parameters:
  #    DomainID       - Unique identifier for the domain
  #      * Always required
  #
  #  Returns an array of hashes, one per RR, each with the following fields:
  #    RESOURCEID     - Unique identifier for the RR
  #    DOMAINID       - Unique identifier for the domain
  #    NAME           - Name of the RR
  #      * May be empty
  #    TYPE           - Type of the RR
  #      * Possible values are listed below
  #    TARGET         - IP, name or string this RR resolves to
  #    PRIORITY       - Priority for MX type RRs
  #    TTL_SEC        - 'ttl' value for the RR
  #      * A value of zero indicates the domain default
  #    WEIGHT         - Weight for SRV type RRs
  #    PORT           - Port for SRV type RRs
  #  
  #  Possible values for RR TYPE
  #    NS             - Name server
  #    MX             - Mail exchanger
  #    A              - IPv4 address
  #    AAAA           - IPv6 address
  #    CNAME          - Canonical name
  #    TXT            - Text
  #    SRV            - Service location

  def domainResourceList args
    if not args.has_key?(:DomainID)
      raise "DomainID argument missing from argument list"
    end
  
    make_request this_method, args
  end
  
  #  Retrieve the details for a specific resource record (RR).
  #
  #  Parameters:
  #    ResourceID     - Unique identifier for the domain to be retrieved
  #      * Always required
  #
  #  Returns a hash for the requested RR
  #    * See domainResourceList documentation for the fields in the hash
      
  def domainResourceGet args
  	if not args.has_key?(:ResourceID)
  		raise "ResourceID argument missing from argument list"
  	end
  	
  	make_request this_method, args
  end
  
  #  Create or update a specific resource record (RR).
  # 
  #  Parameters:
  #    ResourceID     - Unique identifier for the RR
  #      * Always required - use 0 to insert a new RR
  #    DomainID       - Unique identifier for the domain
  #      * Always required.
  #    Name           - Name of the RR
  #      * May be empty
  #    Type           - Type of the RR
  #      * Always required - see domainResourceList documentation for possible values
  #    Target         - IP, name or string this RR resolves to
  #    Priority       - Priority for MX type RRs
  #    TTL_Sec        - 'ttl' value for the RR
  #      * A value of zero indicates the domain default
  #    Weight         - Weight for SRV type RRs
  #    Port           - Port for SRV type RRs
  #   
  #  Returned fields:
  #    RESOURCEID     - Unique identifier for the new or updated RR
  #
  #  *** Parameters not passed to update an RR will be reset to defaults ***
  
  def domainResourceSave args
  	if not args.has_key?(:ResourceID)
  		raise "ResourceID argument missing from argument list"
  	end
  	
  	if not args.has_key?(:DomainID)
  		raise "DomainID argument missing from argument list"
  	end
  	
  	if not args.has_key?(:Target)
  		raise "Target argument missing from argument list"
  	end
  	
  	if not args.has_key?(:Type)
  		raise "ResourceID argument missing from argument list"
  	end
  	if args[:Type].downcase == "mx"
  		if not args.has_key?(:Priority)
  		  raise "Valid Priority must be included for type MX"
  		end
  	elsif args[:Type].downcase == "srv"
  		if not args.has_key?(:Weight)
  		  raise "Valid Weight must be included for type SRV"
  		end
  		if not args.has_key?(:Port)
  		  raise "Valid Port must be included for type SRV"
  		end
  	elsif not (args[:Type].downcase == "ns" or  args[:Type].downcase == "a" or  args[:Type].downcase = "aaaa" or  args[:Type].downcase = "cname"  or  args[:Type].downcase = "txt")
  		  raise "#{args[:Type]} is not a valid RR type"
  	end
  	
  	make_request this_method, args
  end
  
  #  Delete a specific resource record (RR).
  #
  #  Parameters:
  #    ResourceID    - The unique identifier for this Resource Record
  #
  #  Returns RESOURCEID
  
  def domainResourceDelete args
  	if not args.has_key(:ResourceID)
  		raise "ResourceID argument missing from argument list"
  	end
  	
  	make_request this_method, args
  end
  
  #  Retrieve the list of Linodes visible to the user.
  #  
  #  Parameters:
  #    None
  #    
  #  Returns an array of hashes, one per Linode, each with the following fields:
  #    LINODEID       - The unique identifier for this Linode
  #    STATUS         - The Linode's status (see below)
  #    HOSTHOSTNAME   - The DNS name for the host the Linode is on
  #    LISHUSERNAME   - The username to connect to a Lish session
  #    LABEL          - The label for the Linode, as seen on the Linode Manager
  #    TOTALRAM       - Total RAM assigned to this Linode (MiB)
  #    TOTALHD        - Total hard drive space assigned to this Linode (MiB)
  #    TOTALXFER      - Total transfer assigned to this Linode (GiB)
  #    
  #  Possible values for STATUS
  #    * This is still an undocumented API call
      
  def linodeList
  	make_request this_method
  end

  #  Flush the cache of waiting results
  #
  #  Parameters: 
  #    None
  #
  #  Returns:
  #    Array of hashes containing results of queries    

  def flush
    if not @batching
      throw "Batching is not enabled"
    end
    send_request "batch&requestArray=#{JSON.generate @waiting}"
  end

  		
  # Private helper methods
  private
  def make_request request, args = {}
    
    # Check for batching
    if not @batching
      # Init variables
      query = ""

      # Loop through the argument array, and construct the query string
      args.each do |key, arg|
        query = "#{query}&#{key}=#{arg}"
      end
      send_request("#{request}#{query}")
    else
      # If batching, simply add the args into a batch array
      args["action"] = request
      @waiting << args
      return 
    end
  end
  
  def send_request query
    # Init variables
    resp = ""

    # Open up https connection, and check certificate
    http = Net::HTTP.new(LINODE_API_URL, 443)
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_PEER
    store = OpenSSL::X509::Store.new
    store.set_default_paths
    http.cert_store = store
    
    # Send the request to the server, and receive the response
    http.start do |http|
      req = Net::HTTP::Get.new("/api/?api_key=#{@key}&action=#{query}")
      response = http.request(req)
      resp = response.body
    end 
    
    puts "/api/?api_key=#{@key}&action=#{query}"
    puts resp
    
    # Parse the JSON response into a Ruby Hash
    result = JSON.parse resp
    
    # If batching just return the results
    return result if @batching

    # If the error array is set, raise an exception, with the error array as its argument
    raise "#{result['ERRORARRAY']}" if result['ERRORARRAY'].length > 0
    
    
    # Return the data
    result['DATA']
  end
  
  # Overide the puts function, and only print if debug is set
  def puts(arg)
    super arg if @debug
  end
  
  # Return the name of the method being called
  def this_method
    caller[0]=~/`(.*?)'/
    $1
  end

end


