ManageIQ/manageiq-smartstate

View on GitHub
lib/metadata/VmConfig/VmConfig.rb

Summary

Maintainability
F
1 wk
Test Coverage
F
18%
require 'pathname'
require 'metadata/VMMount/VMMount'
require 'miq_unicode'
require 'manageiq/gems/pending'
require 'util/miq-xml'
require 'VMwareWebService/MiqVimInventory'
require 'timeout'
require 'util/miq-extensions'

class VmConfig
  using ManageIQ::UnicodeString

  attr_reader :configFile

  def initialize(filename)
    @configFile = nil
    @configPath = nil
    @direct_file_access = true

    if filename.kind_of? Hash
      @direct_file_access = false
      @cfgHash = filename
    else
      @cfgHash = {}
      # If filename contains embedded new-line chars treat it as data.  Otherwise, its a filename.
      if filename.index("\n")
        f = filename
      else
        set_vmconfig_path(filename)

        begin
          configType = File.extname(filename).delete(".").downcase
          require "metadata/VmConfig/#{configType}Config"
        rescue LoadError => e
          raise e, "Filetype unrecognized for file #{filename}"
        end
        extend Kernel.const_get(configType.capitalize + "Config")
        f = convert(filename)
      end

      process_file(f)

      f.close if f.class == File

      postProcessDisks
    end

    # Handle oddities
    configuration_fixup
  end

  def process_file(data_lines)
    data_lines.each_line do |line|
      line.AsciiToUtf8!.strip!
      next if line.length == 0
      next if line =~ /^#.*$/
      next unless line.include?("=")
      k, v = line.split(/\s*=\s*/)

      # Note: All key names are lower-cased for easy lookup
      k = k.downcase
      v = v.gsub(/^"/, "").gsub(/"$/, "")

      @cfgHash[k] = v
    end
  end

  def postProcessDisks
    # Convert absolute paths to relative paths for the disks
    if @direct_file_access
      getAllDiskKeys.each do |dk|
        next if @cfgHash[dk + ".devicetype"] && @cfgHash[dk + ".devicetype"].include?("cdrom")
        next if @cfgHash[dk + ".filename"] && @cfgHash[dk + ".filename"].downcase == "auto detect"

        begin
          dskPath = Pathname.new(@cfgHash[dk + ".filename"])
          begin
            @cfgHash[dk + ".filename"] = dskPath.relative_path_from(Pathname.new(@configPath)).to_s.tr("\\", "/") if dskPath.absolute?
          rescue
            @cfgHash[dk + ".filename"] = dskPath.to_s.tr("\\", "/")
          end
          if self.respond_to?(:diskCreateType)
            createType = diskCreateType(@cfgHash[dk + ".filename"])
            @cfgHash[dk + ".createType"] = createType if createType
          end
          if self.respond_to?(:diskControllerType)
            adapterType = diskControllerType(@cfgHash[dk + ".filename"])
            @cfgHash[dk + ".adapterType"] = adapterType if adapterType
          end
        rescue => err
          $log.warn "VmConfig: Failed to convert path: #{@cfgHash[dk + ".filename"]}, #{err}"
        end
      end
    end
  end

  def getHash
    @cfgHash
  end

  def getDiskFileHash
    return @diskFileHash if @diskFileHash

    @diskFileHash = {}
    getAllDiskKeys.each do |dk|
      next if @cfgHash[dk + ".devicetype"] && @cfgHash[dk + ".devicetype"].include?("cdrom")
      next if @cfgHash[dk + ".filename"] && @cfgHash[dk + ".filename"].downcase == "auto detect"
      filename = @cfgHash[dk + ".filename"]
      @diskFileHash[dk] = filename
      if @direct_file_access
        ds, _dir, _name = split_filename(filename)
        if ds.nil? && !Pathname.new(filename).absolute?
          @diskFileHash[dk] = File.expand_path(File.join(@configPath, filename))
        end
      end
    end
    @diskFileHash
  end # getDiskFileHash

  def getAllDiskKeys
    dskKeys = @cfgHash.keys.delete_if { |e| !diskKey?(e) }.collect! { |e| e.gsub(".filename", "") }
    dskKeys.delete_if { |dk| @cfgHash[dk + ".present"].to_s.downcase == "false" }
    dskKeys.sort
  end

  def getSnapshotDiskFileHash
    return @snapshotdiskFileHash if @snapshotdiskFileHash

    # For snapshots we need to keep the disks order by snapshot uid, then by device
    @snapshotdiskFileHash = Hash.new { |h, k| h[k] = {} }
    getSnapshotDiskKeys.each do |dk|
      next if @cfgHash[dk + ".devicetype"].to_s.include?("cdrom")
      next if @cfgHash[dk + ".filename"].to_s.downcase == "auto detect"
      dk =~ /^snapshot\d+/
      sn_uid = @cfgHash["#{$&}.uid"]
      key = @cfgHash[dk + ".node"]

      # Independent disks do not par-take in snapshots.  Check the mode of the parent disk.
      next if @cfgHash["#{key}.mode"].to_s.include?('independent')

      @snapshotdiskFileHash[sn_uid]['disks'] ||= {}
      filename = @cfgHash[dk + ".filename"]
      disk_path = filename

      if @direct_file_access
        ds, _dir, _name = split_filename(filename)
        if ds.nil? && !Pathname.new(@cfgHash[dk + ".filename"]).absolute?
          disk_path = File.join(@configPath, filename)
        end
      end
      @snapshotdiskFileHash[sn_uid]['disks'][key] = disk_path
    end
    @snapshotdiskFileHash
  end # getSnapshotDiskFileHash

  def getSnapshotDiskKeys
    dskKeys = @cfgHash.keys.delete_if { |e| !snapshotdiskKey?(e) }.collect! { |e| e.gsub(".filename", "") }
    dskKeys.delete_if { |dk| @cfgHash[dk + ".present"].to_s.downcase == "false" }
    dskKeys.sort
  end

  def set_vmconfig_path(config_path)
    @configFile = normalize_vmconfig_file(config_path)
    @configPath = File.dirname(@configFile)
  end

  def to_xml(diskStats = true, miqvm = nil)
    # If loaded directly from a hash object, try to add snapshot metadata before processing
    load_vim_snapshots(miqvm)

    set_vmconfig_path(miqvm.vmConfigFile) unless miqvm.nil?

    normalize_file_paths

    xml = MiqXml.createDoc("<vm_configuration><hardware></hardware><vm/></vm_configuration>", "vendor" => vendor)
    # Sort the Hash into an array to group like keys and add each array to the XML doc
    @cfgHash.sort { |a, b| a <=> b }.each do |(k, v)|
      next if k[0..0] == '.'
      insert_XML(k, v, xml)
    end

    # Pre-load data
    unless miqvm.nil?
      # Make sure we have the volume manager and loaded
      begin
        miqvm.rootTrees[0]
        @vol_mgr_loaded = true
      rescue LoadError
        $log.warn "add_disk_stats [#{$!.class}]-[#{$!}]"
      end

      files(miqvm)
    end

    # Add virtual disk sizes
    add_disk_stats(xml, miqvm) if diskStats

    # Here we list all files in the directory and get their size on disk.
    add_file_sizes(xml, miqvm)

    # Add partition and volume information
    add_volumes(xml, miqvm)

    # Add date and size info for snapshots
    add_snapshot_size(xml, miqvm)

    # TODO: Re-enable
    # normalize_path_names(xml)

    xml
  end

  alias_method :toXML, :to_xml

  def getDriveImageList
    vmDisks = []
    # Convert config file to XML
    xml = toXML(false)
    # Find the disk section of the XML
    xml.root.each_recursive do |e|
      # Only process drives that are present and have a filename
      if (!e.attributes['present'].nil? && e.attributes['present'].downcase == 'true') && !e.attributes['filename'].nil? && (!e.attributes['type'].nil? && e.attributes['type'].downcase == 'disk') && (!e.attributes['id'].nil? && e.attributes['id'].include?(":"))
        # Make sure the disk we are looking at is not a CD-ROM
        if e.attributes['devicetype'].nil? || (!e.attributes['devicetype'].nil? && e.attributes['devicetype'].downcase.index("cd").nil?)
          diskName = e.attributes['filename'].tr("\"", "").strip.tr("\\", "/")
          diskName = File.join(@configPath, diskName) unless diskName[0..0] == "/"
          $log.debug "Adding Disk name to list: [#{diskName}]"
          begin
            vmDisks.push(File.expand_path(diskName))
          rescue
            vmDisks.push(diskName)
          end
          $log.debug "Adding Disk name to list: [#{diskName}]"
          # $log.info "vmDisk Array = #{vmDisks.to_s()}"
        end
      end
    end
    vmDisks
  end

  def dump_config_to_log(log_level = :warn)
    $log.send(log_level, "VmConfig: Start of configuration info for: #{@configFile}")
    @cfgHash.each { |k, v| $log.send(log_level, "#{k} = #{v}") }
    $log.send(log_level, "VmConfig: End of configuration info for: #{@configFile}")
  end

  def vendor
    "unknown"
  end

  def find_file(filename, file_list = files)
    #    basename = File.basename(filename)
    #    return file_list.detect {|f| f[:name] == basename}
    file_list.detect { |f| f[:path] == filename }
  end

  private

  def normalize_vmconfig_file(config_file)
    ds, _dir, _name = split_filename(config_file)
    return config_file unless ds.nil?

    File.expand_path(config_file.tr('\\', '/'))
  end

  def diskKey?(key)
    key =~ /^ide\d+:\d+\.filename$/ || key =~ /^scsi\d+:\d+\.filename$/
  end

  def snapshotdiskKey?(key)
    key =~ /^snapshot\d+.disk\d+.filename$/
  end

  def insert_XML(key, value, xml)
    maj, min, other = key.split(".")

    # Any elements that cannot be split are put into a misc class
    min, maj = maj, maj if min.nil?

    if maj.index(/\d/)
      maj_name = $`.chomp(':')
    else
      maj_name = maj
    end
    if min[0...3] == "mem" || min[0...5] == "nvram"
      path = [["hardware"], ["memory"]]
    elsif maj_name == "uuid"
      path = [["hardware"], ["bios"]]
      # Drives
    elsif maj_name == "scsi" || maj_name == "ide" || maj_name == "floppy"
      path = getDevicePath(maj, "disk")
      # Ports
    elsif maj_name == "usb" || maj_name == "parallel" || maj_name == "serial" || maj_name == "sound" || maj_name == "ethernet"
      path = getDevicePath(maj, maj_name)
    elsif maj_name == "snapshot"
      other, min = min, other if other
      path = getSnapShotPath(maj, maj_name, other)
    else
      path = [["vm"], [maj]]
    end

    parent = xml.root
    path.each do |p|
      if p[1].nil?
        ele = MIQRexml.findElement(p[0], parent) if parent.has_elements?
      else
        ele = find_with_attributes(p[0], p[1], parent) if parent.has_elements?
      end

      # Create new node if needed.
      if ele.nil?
        # Remove any nil keys from the hash
        p[1].delete_if { |_k, v| v.nil? } if p[1].kind_of?(Hash)
        parent = parent.add_element(p[0], p[1])
      else
        parent, ele = ele, nil
      end
    end
    parent.add_attribute(min.tr(':', '_'), value) if parent
  end

  def add_disk_stats(xml, miqvm)
    # Now loop over the xml and find disk files
    xml.find_first("/*/hardware").each_recursive do |e|
      # Find elements that have a filename attribute
      # Loop through the "whole disks" and get the size for this disk
      if e.attributes['filename']
        getDiskFileHash.each_pair do |device_id, filename|
          if is_same_disk?(device_id, e)
            if (fstat = find_file(e.attributes['filename'], disk_files(miqvm))).nil?
              $log.warn "add_disk_stats - Disk file not found - Details: device_id: [#{device_id}] [#{filename}]"
              next
            end

            $log.info "add_disk_stats - device_id: [#{device_id}] [#{filename}]"

            whole_disk = nil
            whole_disk = miqvm.wholeDisks.detect { |wd| wd.hwId.include?(device_id) } if @vol_mgr_loaded
            size = whole_disk.nil? ? fstat[:provision_size] : whole_disk.size

            # Report a disk's size_on_disk as the size of all the files that make up the disk.
            # This includes the base disk and snapshots.
            collective_disk_size = collective_size_on_disk(device_id, disk_files(miqvm))
            size_on_disk = collective_disk_size.blank? ? fstat[:size] : collective_disk_size
            e.add_attributes({'size' => size, 'size_on_disk' => size_on_disk, 'disk_type' => fstat[:disk_type]})
          end
        end
      end
    end
  rescue Exception
    $log.error "VmConfig.add_disk_stats [#{$!.class}]-[#{$!}]\n#{$!.backtrace.join("\n")}"
  end

  # Match filenames to loaded disks using the hardware id.  IE scsi0:0 or ide0:0
  def is_same_disk?(hardware_id, xml_element)
    diskId = xml_element.parent.attributes["type"] + xml_element.attributes["id"].to_s
    hardware_id.include?(diskId)
  end

  def normalize_path_names(xml)
    # Now loop over the xml and find disk files
    xml.root.each_recursive { |e| e.add_attribute('filename', resolve_ds_path(e.attributes['filename'])) if e.attributes['filename'] }
    xml.find_first("/*/files").each_recursive { |e| e.add_attribute('name', resolve_ds_path(e.attributes['name'])) if e.attributes['name'] }
    xml.find_first("/*/volumes").each_recursive { |e| e.add_attribute('virtual_disk_file', resolve_ds_path(e.attributes['virtual_disk_file'])) if e.attributes['virtual_disk_file'] }
  end

  def resolve_ds_path(filename)
    # TODO: Determine if we need to do any work here.
    ds, _dir, _name = split_filename(filename)
    return filename unless ds.nil?

    @ds_replace ||= {}
    @ds_replace.each_pair do |path, ds|
      return filename.sub(path, ds) if filename.include?(path)
    end

    ds_filename = resolve_ds_path_with_vim(filename)
    ds, dir, _name = split_filename(ds_filename)
    unless ds.nil?
      idx = filename.index(dir)
      replaced_str = filename[0, idx]
      @ds_replace[replaced_str] = "[#{ds}] "
    end

    ds_filename
  end

  def host_vim_credentials
    creds = nil
    if $miqHostCfg
      if $miqHostCfg.emsLocal
        creds = $miqHostCfg.ems[$miqHostCfg.emsLocal]
      elsif $miqHostCfg.vimHost
        c = $miqHostCfg.vimHost
        if c[:username].nil?
          $log.warn "Host credentials are missing: skipping snapshot information."
          return nil
        end
        creds = {'host' => (c[:hostname] || c[:ipaddress]), 'user' => c[:username], 'password' => c[:password], 'use_vim_broker' => c[:use_vim_broker]}
      end
    end
    creds
  end

  def connect_to_host_vim(conn_reason, vmCfgFile)
    ems_host = host_vim_credentials
    return nil if ems_host.nil?

    begin
      st = Time.now
      ems_display_text = "host(#{ems_host['use_vim_broker'] ? 'via broker' : 'directly'}):#{ems_host['host']}"
      $log.info "#{conn_reason}: Connecting to [#{ems_display_text}] for VM:[#{vmCfgFile}]"

      require 'VMwareWebService/MiqVim'

      password_decrypt = ManageIQ::Password.decrypt(ems_host['password'])
      hostVim = MiqVim.new(:server => ems_host['host'], :username => ems_host['user'], :password => password_decrypt)
      $log.info "#{conn_reason}: Connection to [#{ems_display_text}] completed for VM:[#{vmCfgFile}] in [#{Time.now - st}] seconds"
      return hostVim
    rescue Timeout::Error => err
      $log.error "#{conn_reason}: Connection to [#{ems_display_text}] timed out for VM:[#{vmCfgFile}] with error [#{err}] after [#{Time.now - st}] seconds"
    rescue Exception => err
      $log.error "#{conn_reason}: Connection to [#{ems_display_text}] failed for VM:[#{vmCfgFile}] with error [#{err}] after [#{Time.now - st}] seconds"
    end
    nil
  end

  def resolve_ds_path_with_vim(filename)
    vi = nil
    ems = host_vim_credentials
    return (filename) if ems.nil?

    password_decrypt = ManageIQ::Password.decrypt(ems['password'])
    $log.debug "resolve_path_names: emsHost = #{ems['host']}, emsUser = #{ems['user']}" if $log
    vi = MiqVimInventory.new(:server => ems['host'], :username => ems['user'], :password => password_decrypt)
    return getDsName(filename, vi)

  rescue
    $log.error "VmConfig.resolve_ds_path_with_vim #{$!}\n#{$!.backtrace.join("\n")}"
  ensure
    vi.disconnect if vi
  end

  def getDsName(filename, vi)
    filename = File.join(File.dirname(@configFile), filename) if File.dirname(filename) == "."
    dsName = vi.datastorePath(filename)
    dsName.sub!("] /", "] ") if dsName.index("] /")
    return dsName
  rescue
    return filename
  end

  def files(miqvm = nil)
    return @files if @files
    log_header = "VmConfig.files"

    @files = []
    if @direct_file_access
      Dir.glob(File.join(@configPath, "/*.*")) do |f|
        s = File.size(f) rescue 0
        @files << {:path => f, :name => File.basename(f), :size => s, :mtime => File.mtime(f)}
      end
    else
      begin
        if miqvm.vim
          filePattern = nil
          pathOnly = false
          vimDs = nil
          each_datastore(miqvm) do |ds, dirs|
            begin
              vimDs = miqvm.vim.getVimDataStore(ds)
              dirs.each do |path|
                vimDs.dsFileSearch(filePattern, path, pathOnly).each do |f|
                  @files << {:path => f['fullPath'], :name => f['path'], :size => f['fileSize'].to_i, :mtime => Time.parse(f['modification'])}
                end
              end
            ensure
              vimDs.release if vimDs rescue nil
            end
          end
        elsif miqvm.rhevm
          # First add the VM's active disk files.
          disks = miqvm.rhevm.collect_vm_disks(miqvm.rhevmVm)
          disks.each { |disk| @files << rhevm_disk_file_entry(disk) }
          # Then add the files associtaed with inactive snapshots.
          miqvm.rhevm.collect_snapshots(miqvm.rhevmVm).each do |snap|
            next if snap.snapshot_type == 'active'

            miqvm.rhevm.collect_disks_of_snapshot(snap).each do |disk|
              @files << rhevm_disk_file_entry(disk)
            end
          end
        end
      rescue => err
        $log.error "#{log_header} #{err}\n#{err.backtrace.join("\n")}"
      end
    end
    disk_files(miqvm)
    @files
  end

  def rhevm_disk_file_entry(disk)
    storage_id = disk.storage_domains&.first&.id
    disk_id = disk.image_id || disk.id
    full_path = storage_id && File.join('/dev', storage_id, disk_id)
    {:path => full_path, :name => disk_id, :size => disk.actual_size.to_i}
  end
  private :rhevm_disk_file_entry

  def disk_files(miqvm = nil)
    return @disk_files if @disk_files
    @disk_files = []
    if @direct_file_access
      @disk_files = files(miqvm)
    else
      begin
        vim_disks = {}
        if miqvm.vimVm
          miqvm.vimVm.devices.each do |d|
            next if d['capacityInKB'].nil?
            next if (filename = d.fetch_path('backing', 'fileName')).nil?
            disk = vim_disks[filename.to_s] = {}

            # check if it's raw disk type, using same logic as in vmware refresher_parser.
            mode = d.fetch_path('backing', 'compatibilityMode')
            disk_type = "rdm-#{mode[0...-4]}" if mode

            disk[:disk_type] = mode ? disk_type : ((d.fetch_path('backing', 'thinProvisioned') == 'true') ? 'thin' : 'thick')
            disk[:provision_size] = d['capacityInKB'].to_i * 1024
            disk[:display_name] = d.fetch_path('deviceInfo', 'label').to_s
            disk[:vim_index] = d['key'].to_i
            disk[:disk_mode] = d['diskMode']
          end

          filePattern = nil
          pathOnly = false
          each_datastore(miqvm) do |ds, dirs|
            begin
              vimDs = miqvm.vim.getVimDataStore(ds)
              dirs.each do |path|
                vimDs.dsVmDiskFileSearch(filePattern, path, pathOnly).each do |f|
                  d = {:path => f['fullPath'], :name => f['path'].to_s, :size => f['fileSize'].to_i, :mtime => Time.parse(f['modification'])}
                  dh = vim_disks[d[:path]]
                  d.merge!(dh) unless dh.nil?
                  @disk_files << d
                end
              end
            ensure
              vimDs.release if vimDs rescue nil
            end
          end
        elsif miqvm.rhevmVm
          disks = miqvm.rhevm.collect_vm_disks(miqvm.rhevmVm)
          disks.each do |disk|
            storage_id = disk.storage_domains&.first&.id
            disk_id = disk.image_id || disk.id
            full_path = storage_id && File.join('/dev', storage_id, disk_id)
            d = {:path => full_path, :name => disk.name.to_s, :size => disk.actual_size.to_i}
            @disk_files << d
          end
        end
      rescue Exception
        $log.error "VmConfig.disk_files #{$!}\n#{$!.backtrace.join("\n")}"
      end
    end
    @disk_files
  end

  def add_file_sizes(xml, miqvm)
    return if miqvm.nil?
    begin
      node = xml.root.add_element("files")
      total_size = 0
      files(miqvm).each do |fh|
        total_size += fh[:size]
        node.add_element('file', {"name" => fh[:name], "size_on_disk" => fh[:size]})
      end
      # Add the total size to the "files" node
      node.add_attribute("size_on_disk", total_size)

      free_space = 0; disk_capacity = 0
      if miqvm && @vol_mgr_loaded
        # Make sure we have the volume manager loaded
        vmRoot = miqvm.rootTrees[0]
        if vmRoot
          miqvm.wholeDisks.each { |d| disk_capacity += d.size }
          rootAdded = false
          vmRoot.fileSystems.each do |fsd|
            $log.info "FileSystem: #{fsd.fsSpec}, Mounted on: #{fsd.mountPoint}, Type: #{fsd.fs.fsType}, Free bytes: #{fsd.fs.freeBytes}"
            # The root volume can appear more than once, so only add it one time.
            if fsd.mountPoint == "/"
              if rootAdded == false
                free_space += fsd.fs.freeBytes
                rootAdded = true
              end
            else
              free_space += fsd.fs.freeBytes
            end
          end
        end

        # Now calculate free/used percent against size on disk
        if !free_space.zero? && !disk_capacity.zero?
          # Calculate the percentage of free space
          percent_free = free_space.to_f / disk_capacity.to_f * 100

          # Populate formated text fields
          node.add_attributes("pct_free_wrt_size_on_disk" => percent_free, "pct_used_wrt_size_on_disk" => (100 - percent_free))
          node.add_attributes("disk_free_space" => free_space, "disk_capacity" => disk_capacity)
        end
      end
      node.add_attributes("disk_free_space" => free_space, "disk_capacity" => disk_capacity)
    rescue
    end
  end

  def load_vim_snapshots(miqvm)
    if @direct_file_access == false && miqvm && miqvm.vim
      # If the VM does not have snapshots we can stop here
      return if miqvm.snapshots.nil?
      ds, dir, name = split_filename(miqvm.vmConfigFile)

      # We need to connect to the host to get the snapshot file content
      hostVim = nil
      if miqvm.vim.isVirtualCenter?
        hostVim = connect_to_host_vim('snapshot_metadata', miqvm.vmConfigFile)

        if hostVim.nil?
          $log.warn "Snapshots information will be skipped due to EMS host missing credentials."
          return
        end

        vimDs = hostVim.getVimDataStore(ds)
      else
        vimDs = miqvm.vim.getVimDataStore(ds)
      end

      require "metadata/VmConfig/vmxConfig"
      extend Kernel.const_get("VmxConfig")
      snapshot_file = File.join(dir, File.basename(name, ".*") + ".vmsd")
      Timeout.timeout(60) do
        process_file(convert_vmsd(vimDs.get_file_content(snapshot_file)))
      end
      @cfgHash
    end
  rescue Timeout::Error => err
    $log.warn "Timeout reached transferring snapshot metadata for config file [#{miqvm.vmConfigFile}].  Message:[#{err}]"
  rescue => err
    $log.warn "Unable to process snapshot metadata for config file [#{miqvm.vmConfigFile}].  Message:[#{err}]"
  ensure
    vimDs.release   if vimDs   rescue nil
    hostVim.release if hostVim rescue nil
  end

  def each_datastore(_miqvm)
    files = [@configFile]
    each_disks { |_device_id, filename| files << filename }

    ds = Hash.new { |h, k| h[k] = {} }
    files.each do |f|
      dsName, dir, _name = split_filename(f)
      ds[dsName][dir] = true # unless dsName.nil?
    end

    ds.each_pair { |ds, h| yield(ds, h.to_a.transpose.first) }
  end

  def getDevicePath(name, endType)
    # Initialize variables
    full_pos, maj_pos, device = nil, "0", name
    # If the name contains a number, use it as the id

    if name.index(/\d/)
      device   = $`.chomp(':')
      maj_pos  = $&
      full_pos = "#{$&}#{$'}"
    end

    # These devices only have 1 position number, and should be grouped under a controller
    maj_pos = nil if ["ethernet", "floppy", "parallel", "serial"].include?(device)

    ret = [["hardware"], ["bus", {"type" => "pci"}], ["controller", {"type" => device, "id" => maj_pos}]]
    # Add a device type if the major position and full position differ, otherwise
    # we are processing a controller element.
    ret << ["device", {"type" => endType, "id" => full_pos}] if maj_pos != full_pos
    ret
  end

  def getSnapShotPath(name, endType, disk)
    # Initialize variables
    full_pos, _maj_pos, _device, pos_idx = split_data(name)

    return [["vm"], ["snapshots"]] if pos_idx.nil?
    ret = [["vm"], ["snapshots"], [endType, {"id" => full_pos}]]

    if disk
      subDevice = split_data(disk)[2]
      diskPath = getSnapShotPath(disk, subDevice, nil)
      ret << diskPath.pop
    end

    ret
  end

  def add_snapshot_size(xml, _miqvm)
    adjust_snapshot_aligment(xml)

    # First we need to loop through the snapshots and add flat files for any
    # disks that are just descriptor files
    xml.find_each("//*/snapshots/snapshot/disk") do |e|
      unless e.attributes['filename'].nil?
        find_additional_disk_files(e.attributes['filename']) do |f|
          e.parent.add_element("disk", "filename" => f[:path])
        end
      end
    end

    # Now loop through the filename and collect stats  (date/time/size)
    node = xml.find_first("//*/snapshots")
    node.each_recursive do |e|
      if e.attributes.include?("filename")
        begin
          filename = normalize_file_path(e.attributes['filename'])
          next if (fstat = find_file(filename)).nil?
          e.add_attribute("size_on_disk",  fstat[:size])
          e.add_attribute("mdate_on_disk", fstat[:mtime].getutc.iso8601(6))
        rescue => err
          # Ignore errors here, we will try to load almost anything.
          $log.error "VmConfig.add_snapshot_size [#{err.class}]-[#{err}]\n#{err.backtrace.join("\n")}"
        end
      end
    end unless node.nil?
  end

  def adjust_snapshot_aligment(xml)
    sn = {}
    sn_root = xml.find_first("//*/snapshots")

    unless sn_root.nil?
      disks_by_snap = disks_by_snapshot_and_node
      xml.find_each("//*/snapshots/snapshot") { |s| sn[s.attributes[:uid]] = s }

      sn_list = sn.sort { |a, b| a[1].attributes[:uid].to_s <=> b[1].attributes[:uid].to_s }
      clear_snapshot_disks(sn_list)

      sn_list.each do |(id, snapshot)|
        parent = snapshot.attributes[:parent]
        unless parent.nil? || sn[parent].nil?
          disks_by_snap[id.to_s]['disks'].each do |device_id, filename|
            sn[parent].add_element(:disk, :node => device_id, :filename => filename)
          end
        end

        filename = normalize_file_path(snapshot.attributes[:filename])
        snapshot.add_element("vmem", "filename" => filename) if find_file(filename)
      end

      current = sn[sn_root.attributes[:current]]
      unless current.nil?
        disks_by_snap[:current]['disks'].each do |device_id, filename|
          current.add_element(:disk, :node => device_id, :filename => filename)
        end
      end
    end
  end

  def clear_snapshot_disks(sn_list)
    sn_list.each do |(_id, snapshot)|
      delete_items = []
      snapshot.each { |d| delete_items << d if d.name == 'disk' }
      delete_items.each(&:remove!)
    end
  end

  def split_data(name)
    # Initialize variables
    full_pos, maj_pos, device = nil, "0", name
    # If the name contains a number, use it as the id
    pos_idx = name.index(/\d/)
    if pos_idx
      full_pos = name[pos_idx..-1]
      maj_pos, device = full_pos[0..0]
      device = name[0...pos_idx]
    end
    return full_pos, maj_pos, device, pos_idx
  end

  def find_with_attributes(findKey, findAttr, xmlNode)
    xmlNode.each_element do |e|
      if e.name == findKey
        found = true
        findAttr.each_pair do |k, v|
          found = false if e.attributes[k] != v
        end
        return e if found
      end
    end
    nil
  end

  def add_volumes(xml, miqvm)
    return unless @vol_mgr_loaded
    return if miqvm.nil?
    xml.root << miqvm.volumeManager.toXml.root
  end

  def configuration_fixup
    fixup_keys = []
    @cfgHash.each_pair do |k, v|
      # If the cdrom does not have a filename force the 'startconnected' to false.
      # This is to handle "Client Device" settings in VC that does not maintain the startconnected value.
      if k.include?('.devicetype') && v.include?('cdrom')
        dskKey = k.gsub('.devicetype', '')
        fixup_keys << dskKey if @cfgHash[dskKey + ".filename"].to_s.delete('"').blank?
      end
    end
    fixup_keys.each { |k| @cfgHash["#{k}.startconnected"] = 'false' }
  end

  def collective_size_on_disk(device_id, files = [])
    total_size = 0
    disks_by_node[device_id].each do |disk_name|
      file = files.detect { |f| f[:path] == disk_name }
      total_size += file[:size] unless file.nil?
      find_additional_disk_files(disk_name) { |f| total_size += f[:size] } if @direct_file_access
    end
    total_size
  end

  def split_filename(filename)
    file = filename.dup
    ds = nil
    if file =~ /^\[.*\] /
      ds = $&.strip[1..-2]
      file = $'
    end
    return ds, File.dirname(file), File.basename(file)
  end

  def base_disk_name(filename)
    ds, dir, name = split_filename(filename)
    if name =~ /-\d{6}[-|\.]/
      # puts "#{$`}<<#{$&}>>#{$'}"
      return nil if $`.nil?
      basename = $`
    else
      basename = File.basename(name, '.*')
      basename.gsub!('-flat', '')
    end

    key = File.join(dir, basename)
    key = "[#{ds}] " + key unless ds.nil?
    key
  end

  def self.to_h(filename)
    dataHash = {}
    data = filename
    data = File.read(filename) unless filename.include?("\n")
    data.each_line do |l|
      l.strip!
      next if l[0..0] == '.' || l[0..0] == '#' || l.empty?

      parent = l.split('=')[0].split('.').inject(:current_level => dataHash, :hash => nil, :key => nil) do |h, k|
        a1 = h[:current_level][k.strip.to_sym] ||= {}
        a1 = h[:current_level][k.strip.to_sym] = {:_default => a1} unless a1.kind_of?(Hash)
        {:current_level => a1, :hash => h, :key => k.strip.to_sym}
      end
      uh, key = parent[:hash][:current_level], parent[:key]
      kkey = key.to_s.strip.to_sym
      value = eval_config(l.split('=')[1], kkey)
      uh[key] = value
    end
    dataHash
  end

  def self.eval_config(value, _key)
    value.strip!
    value = value[1..-2] if value[0, 1] == '"' && value[-1, 1] == '"'
    [true, false].each { |b| return b if value.downcase == b.to_s }
    return value.to_i if value =~ /^\d/
    return nil if value.blank?
    value
  end

  def normalize_file_paths(config_file = @configFile)
    @cfgHash.each_pair do |k, v|
      @cfgHash[k] = normalize_file_path(v, config_file) if k.include?('.filename')
    end
  end

  def normalize_file_path(filename, config_file = @configFile)
    cfg_ds, cfg_dir, _cfg_name = split_filename(config_file)
    ds, dir, name = split_filename(filename)
    if dir.blank? || dir == '.'
      name = File.join(cfg_dir, name)
      name = "[#{cfg_ds}] " + name unless cfg_ds.nil?
      return name
    else
      # Check for files that do not have a ds when the config file does
      return resolve_ds_path(filename) if cfg_ds && ds.nil?
    end
    filename
  end

  def disks_by_node
    result = Hash.new { |h, k| h[k] = [] }

    # Keep track of the disk we process so they do not get counted twice.
    # This handles independent-persistent disk that do not take part in snapshots but will
    # show up in each snapshot's disk list.
    processed_disks = []

    normalize_file_paths
    getDiskFileHash.each_pair do |disk_id, path|
      unless processed_disks.include?(path)
        result[disk_id] << path
        processed_disks << path
      end
    end

    getSnapshotDiskFileHash.each_pair do |_id, h|
      h['disks'].each_pair do |disk_id, path|
        unless processed_disks.include?(path)
          result[disk_id] << path
          processed_disks << path
        end
      end
    end

    result
  end

  def each_disks
    # Call disks_by_node since it will remove duplicate disks
    disks_by_node.each_pair { |device_id, disks| disks.each { |d| yield(device_id, d) } }
  end

  def disks_by_snapshot_and_node
    result = Hash.new { |h, k| h[k] = {} }
    getSnapshotDiskFileHash.each_pair { |id, h| result[id] = h }

    result[:current] = {'disks' => {}}
    current_disks = result[:current]['disks']
    getDiskFileHash.each_pair do |device_id, d|
      # Independent disks do not par-take in snapshots.  Check the mode of the parent disk.
      next if @cfgHash["#{device_id}.mode"].to_s.include?('independent')
      current_disks[device_id] = d
    end

    result
  end

  def find_additional_disk_files(filename)
    ds, dir, name = split_filename(normalize_file_path(filename))
    dfBase, ext = File.basename(name, ".*"), File.extname(name)
    %w(-flat -delta).each do |disk_type|
      search_filename = file_join(dir, dfBase + disk_type + ext)
      search_filename = "[#{ds}] " + search_filename unless ds.nil?
      f = find_file(search_filename)
      yield(f) unless f.nil?
    end
  end

  # Ensures that windows paths use forward slashes to full path lookups work.
  def file_join(*args)
    filename = File.join(*args)
    filename.tr!('\\', '/') if filename[1, 1] == ':'
    filename
  end
end