# Helper class for manipulating VM storage *volumes*, i.e. it deals
# only with creation of images and keeps a name => volume path lookup
# table (plugging drives or getting info of plugged devices is done in
# the VM class). We'd like better coupling, but given the ridiculous
# disconnect between Libvirt::StoragePool and Libvirt::Domain (hint:
# they have nothing with each other to do whatsoever) it's what makes
# sense.

require 'libvirt'
require 'guestfs'
require 'rexml/document'
require 'etc'

class VMStorage

  def initialize(virt, xml_path)
    @virt = virt
    @xml_path = xml_path
    pool_xml = REXML::Document.new(File.read("#{@xml_path}/storage_pool.xml"))
    pool_name = LIBVIRT_DOMAIN_NAME
    pool_xml.elements['pool/name'].text = pool_name
    @pool_path = "/srv/workspace/vm-pools/#{pool_name}"
    begin
      @pool = @virt.lookup_storage_pool_by_name(pool_name)
    rescue Libvirt::RetrieveError
      @pool = nil
    end
    if @pool and not(KEEP_SNAPSHOTS)
      VMStorage.clear_storage_pool(@pool)
      @pool = nil
    end
    unless @pool
      pool_xml.elements['pool/target/path'].text = @pool_path
      @pool = @virt.define_storage_pool_xml(pool_xml.to_s)
      if not(Dir.exists?(@pool_path))
        # We'd like to use @pool.build, which will just create the
        # @pool_path directory, but it does so with root:root as owner
        # (at least with libvirt 1.2.21-2). libvirt itself can handle
        # that situation, but guestfs (at least with <=
        # 1:1.28.12-1+b3) cannot when invoked by a non-root user,
        # which we want to support.
        FileUtils.mkdir(@pool_path)
        FileUtils.chown(nil, 'libvirt-qemu', @pool_path)
        FileUtils.chmod("ug+wrx", @pool_path)
      end
    end
    @pool.create unless @pool.active?
    @pool.refresh
  end

  def VMStorage.clear_storage_pool_volumes(pool)
    was_not_active = !pool.active?
    if was_not_active
      pool.create
    end
    pool.list_volumes.each do |vol_name|
      vol = pool.lookup_volume_by_name(vol_name)
      vol.delete
    end
    if was_not_active
      pool.destroy
    end
  rescue
    # Some of the above operations can fail if the pool's path was
    # deleted by external means; let's ignore that.
  end

  def VMStorage.clear_storage_pool(pool)
    VMStorage.clear_storage_pool_volumes(pool)
    pool.destroy if pool.active?
    pool.undefine
  end

  def clear_pool
    VMStorage.clear_storage_pool(@pool)
  end

  def clear_volumes
    VMStorage.clear_storage_pool_volumes(@pool)
  end

  def delete_volume(name)
    @pool.lookup_volume_by_name(name).delete
  end

  def create_new_disk(name, options = {})
    options[:size] ||= 2
    options[:unit] ||= "GiB"
    options[:type] ||= "qcow2"
    # Require 'slightly' more space to be available to give a bit more leeway
    # with rounding, temp file creation, etc.
    reserved = 500
    needed = convert_to_MiB(options[:size].to_i, options[:unit])
    avail = convert_to_MiB(get_free_space('host', @pool_path), "KiB")
    assert(avail - reserved >= needed,
           "Error creating disk \"#{name}\" in \"#{@pool_path}\". " \
           "Need #{needed} MiB but only #{avail} MiB is available of " \
           "which #{reserved} MiB is reserved for other temporary files.")
    begin
      old_vol = @pool.lookup_volume_by_name(name)
    rescue Libvirt::RetrieveError
      # noop
    else
      old_vol.delete
    end
    uid = Etc::getpwnam("libvirt-qemu").uid
    gid = Etc::getgrnam("libvirt-qemu").gid
    vol_xml = REXML::Document.new(File.read("#{@xml_path}/volume.xml"))
    vol_xml.elements['volume/name'].text = name
    size_b = convert_to_bytes(options[:size].to_f, options[:unit])
    vol_xml.elements['volume/capacity'].text = size_b.to_s
    vol_xml.elements['volume/target/format'].attributes["type"] = options[:type]
    vol_xml.elements['volume/target/path'].text = "#{@pool_path}/#{name}"
    vol_xml.elements['volume/target/permissions/owner'].text = uid.to_s
    vol_xml.elements['volume/target/permissions/group'].text = gid.to_s
    vol = @pool.create_volume_xml(vol_xml.to_s)
    @pool.refresh
  end

  def clone_to_new_disk(from, to)
    begin
      old_to_vol = @pool.lookup_volume_by_name(to)
    rescue Libvirt::RetrieveError
      # noop
    else
      old_to_vol.delete
    end
    from_vol = @pool.lookup_volume_by_name(from)
    xml = REXML::Document.new(from_vol.xml_desc)
    pool_path = REXML::Document.new(@pool.xml_desc).elements['pool/target/path'].text
    xml.elements['volume/name'].text = to
    xml.elements['volume/target/path'].text = "#{pool_path}/#{to}"
    @pool.create_volume_xml_from(xml.to_s, from_vol)
  end

  def disk_format(name)
    vol = @pool.lookup_volume_by_name(name)
    vol_xml = REXML::Document.new(vol.xml_desc)
    return vol_xml.elements['volume/target/format'].attributes["type"]
  end

  def disk_path(name)
    @pool.lookup_volume_by_name(name).path
  end

  def disk_mklabel(name, parttype)
    disk = {
      :path => disk_path(name),
      :opts => {
        :format => disk_format(name)
      }
    }
    guestfs_disk_helper(disk) do |g, disk_handle|
      g.part_init(disk_handle, parttype)
    end
  end

  def disk_mkpartfs(name, parttype, fstype, opts = {})
    opts[:label] ||= nil
    opts[:luks_password] ||= nil
    disk = {
      :path => disk_path(name),
      :opts => {
        :format => disk_format(name)
      }
    }
    guestfs_disk_helper(disk) do |g, disk_handle|
      g.part_disk(disk_handle, parttype)
      g.part_set_name(disk_handle, 1, opts[:label]) if opts[:label]
      primary_partition = g.list_partitions()[0]
      if opts[:luks_password]
        g.luks_format(primary_partition, opts[:luks_password], 0)
        luks_mapping = File.basename(primary_partition) + "_unlocked"
        g.luks_open(primary_partition, opts[:luks_password], luks_mapping)
        luks_dev = "/dev/mapper/#{luks_mapping}"
        g.mkfs(fstype, luks_dev)
        g.luks_close(luks_dev)
      else
        g.mkfs(fstype, primary_partition)
      end
    end
  end

  def disk_mkswap(name, parttype)
    disk = {
      :path => disk_path(name),
      :opts => {
        :format => disk_format(name)
      }
    }
    guestfs_disk_helper(disk) do |g, disk_handle|
      g.part_disk(disk_handle, parttype)
      primary_partition = g.list_partitions()[0]
      g.mkswap(primary_partition)
    end
  end

  def guestfs_disk_helper(*disks)
    assert(block_given?)
    g = Guestfs::Guestfs.new()
    g.set_trace(1)
    message_callback = Proc.new do |event, _, message, _|
      debug_log("libguestfs: #{Guestfs.event_to_string(event)}: #{message}")
    end
    g.set_event_callback(message_callback,
                         Guestfs::EVENT_TRACE)
    g.set_autosync(1)
    disks.each do |disk|
      g.add_drive_opts(disk[:path], disk[:opts])
    end
    g.launch()
    yield(g, *g.list_devices())
  ensure
    g.close
  end

end