# coding: utf-8
# -------------------------------------------------------------------------- #
# Copyright 2002-2016, OpenNebula Project, OpenNebula Systems                #
#                                                                            #
# 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 'nokogiri'

module OneDBPatch
  VERSION = "4.90.0"
  LOCAL_VERSION = "4.90.0"

  IMAGE_TYPE = %w[hd cdrom]

  class NoImageError < StandardError
  end

  def is_hot_patch(ops)
    return false
  end

  def check_db_version(ops)
    db_version = read_db_version()

    if ( db_version[:version] != VERSION ||
         db_version[:local_version] != LOCAL_VERSION )

      raise <<-EOT
Version mismatch: patch file is for version
Shared: #{VERSION}, Local: #{LOCAL_VERSION}

Current database is version
Shared: #{db_version[:version]}, Local: #{db_version[:local_version]}
EOT
    end
  end

  # Modify template boot order to match disk# and nic# instead of HD, CDROM and NETWORK
  # Params:
  # +ops+:: Options
  def patch(ops)
    init_log_time()

    #

    @db.transaction do
      @db.fetch("SELECT oid,body FROM template_pool") do |row|
        doc = Nokogiri::XML(row[:body],nil,NOKOGIRI_ENCODING){|c| c.default_xml.noblanks}
        template_name = doc.root.at_css("NAME").content

        # Rename VMMMAD -> VM_MAD and TMMAD -> TM_MAD
        boot = doc.root.at_xpath("TEMPLATE/OS/BOOT")
        if boot.nil?
          # No boot entry for template
          next
        end

        # Convert each boot device
        # Skip template if:
        # * the image referenced by a DISK is not found
        # * a boot device in BOOT does not match any DISK
        devs=[]
        nic_id=0

        begin
          boot.content.split(",").each do |dev|
            case dev.downcase

              when "hd", "cdrom"
                id = get_first_disk_id(dev, doc)
                if id.nil?
                  puts "Warning template #{row[:oid]} #{template_name}: skip nonexistent boot device '#{dev}'"
                  next
                end
                devs.push("disk#{id}")

              when "network"
                devs.push("nic#{nic_id}")
                nic_id += 1

              when /^(?:nic|disk)[0-9]+/
                devs.push(dev)

            end
          end
        rescue NoImageError => error
          puts "Skipping template #{row[:oid]} #{template_name}: #{error.message}"
          next
        end

        if boot.content != devs.join(",")
          # Avoid updating unchanged lines
          new_boot = devs.join(",")

          if ops[:verbose]
            puts "Template #{row[:oid]} #{template_name}: '#{boot.content}' -> '#{new_boot}'"
          end

          boot.content = new_boot

          @db[:template_pool].where(:oid => row[:oid]).update(
            :body => doc.root.to_s)

        end
      end

    end

    log_time()

    return true
  end

  # Returns the ID of the first disk of a type
  # Params:
  # +type+:: type name of the disk, can be “hd” or “cdrom”
  # +doc+:: Nokogiri::XML::Node describing the VM template
  def get_first_disk_id(type, doc)
    doc.root.xpath("TEMPLATE/DISK").each_with_index do |disk, index|
      id = disk.at_css("IMAGE_ID")
      if ! id.nil?
        image = get_image_from_id(id.content)
      else
        image = get_image_from_name(disk)
      end

      if is_image_type_matching?(image.at_css("TYPE").content, type)
        return index
      end
    end
    return nil # Skip this type of device
  end

  # Returns a Nokogiri::XML::Node describing an image
  # Params:
  # +id+:: ID of the image
  def get_image_from_id(id)
    @db.transaction do
      row = @db.fetch("SELECT body from image_pool where oid=#{id}").first
      # No image found, so unable to get image TYPE
      raise NoImageError, "No image with ID '#{id}'" if row.nil?
      image = Nokogiri::XML(row[:body], nil,NOKOGIRI_ENCODING){|c| c.default_xml.noblanks}
      return image
    end
  end

  # Returns a Nokogiri::XML::Node describing an image
  # Params:
  # +disk+:: Nokogiri::XML::Node describing a disk used by a template
  def get_image_from_name(disk)
    name = disk.at_css("IMAGE").content # always defined
    uid = disk.at_css("IMAGE_UID")
    uname = disk.at_css("IMAGE_UNAME")

    if ! name.nil? and (! uid.nil? or ! uname.nil?)
      if uid.nil?
        uid = get_user_id(uname.content)
      else
        uid = uid.content
      end

      @db.transaction do
        row = @db.fetch("SELECT body from image_pool where name=\"#{name}\" and uid=#{uid}").first
        # No image found, so unable to get image TYPE
        raise NoImageError, "No image named '#{name}' for uid '#{uid}'" if row.nil?
        image = Nokogiri::XML(row[:body], nil,NOKOGIRI_ENCODING){|c| c.default_xml.noblanks}
        return image
      end
    end
  end

  # Returns the ID of a user name
  # Params:
  # +name+:: name of a user
  def get_user_id(name)
    @db.transaction do
      row = @db.fetch("SELECT uid from user_pool where name=\"#{name}\"").first
      return row[:uid]
    end
  end

  # Check if an image type match the type used in template BOOT
  # Params:
  # +image_type+:: numerical type of an image
  # +wanted_type+:: string type extracted from VM template BOOT
  def is_image_type_matching?(image_type, wanted_type)
    Integer(image_type) == IMAGE_TYPE.index(wanted_type.downcase)
  end

end
