class VagrantPlugins::ProviderLibvirt::Action::StartDomain

Just start the domain.

Public Class Methods

new(app, _env) click to toggle source
# File lib/vagrant-libvirt/action/start_domain.rb, line 17
def initialize(app, _env)
  @logger = Log4r::Logger.new('vagrant_libvirt::action::start_domain')
  @app = app
end

Public Instance Methods

call(env) click to toggle source
# File lib/vagrant-libvirt/action/start_domain.rb, line 22
def call(env)
  domain = env[:machine].provider.driver.connection.servers.get(env[:machine].id.to_s)
  raise Errors::NoDomainError if domain.nil?

  config = env[:machine].provider_config

  # update domain settings on change.
  libvirt_domain = env[:machine].provider.driver.connection.client.lookup_domain_by_uuid(env[:machine].id)

  # Libvirt API doesn't support modifying memory on NUMA enabled CPUs
  # http://libvirt.org/git/?p=libvirt.git;a=commit;h=d174394105cf00ed266bf729ddf461c21637c736
  if config.numa_nodes == nil
    if config.memory.to_i * 1024 != libvirt_domain.max_memory
      libvirt_domain.max_memory = config.memory.to_i * 1024
      libvirt_domain.memory = libvirt_domain.max_memory
    end
  end

  # XML definition manipulation
  descr = libvirt_domain.xml_desc(1)
  xml_descr = REXML::Document.new descr
  descr_changed = false

  # For outputting XML for comparison
  formatter = REXML::Formatters::Pretty.new

  # additional disk bus
  config.disks.each do |disk|
    device = disk[:device]
    bus = disk[:bus]
    REXML::XPath.each(xml_descr, '/domain/devices/disk[@device="disk"]/target[@dev="' + device + '"]') do |disk_target|
      next unless disk_target.attributes['bus'] != bus
      @logger.debug "disk #{device} bus updated from '#{disk_target.attributes['bus']}' to '#{bus}'"
      descr_changed = true
      disk_target.attributes['bus'] = bus
      disk_target.parent.delete_element("#{disk_target.parent.xpath}/address")
    end
  end

  # disk_bus
  REXML::XPath.each(xml_descr, '/domain/devices/disk[@device="disk"]/target[@dev="vda"]') do |disk_target|
    next unless disk_target.attributes['bus'] != config.disk_bus
    @logger.debug "domain disk bus updated from '#{disk_target.attributes['bus']}' to '#{config.disk_bus}'"
    descr_changed = true
    disk_target.attributes['bus'] = config.disk_bus
    disk_target.parent.delete_element("#{disk_target.parent.xpath}/address")
  end

  # Interface type
  unless config.nic_model_type.nil?
    REXML::XPath.each(xml_descr, '/domain/devices/interface/model') do |iface_model|
      if iface_model.attributes['type'] != config.nic_model_type
        @logger.debug "network type updated from '#{iface_model.attributes['type']}' to '#{config.nic_model_type}'"
        descr_changed = true
        iface_model.attributes['type'] = config.nic_model_type
      end
    end
  end

  # vCpu count
  vcpus_count = libvirt_domain.num_vcpus(0)
  if config.cpus.to_i != vcpus_count
    @logger.debug "cpu count updated from '#{vcpus_count}' to '#{config.cpus}'"
    descr_changed = true
    REXML::XPath.first(xml_descr, '/domain/vcpu').text = config.cpus
  end

  # cpu_mode
  if !config.cpu_mode.nil?
    cpu = REXML::XPath.first(xml_descr, '/domain/cpu')
    if cpu.nil?
      @logger.debug "cpu_mode updated from not set to '#{config.cpu_mode}'"
      descr_changed = true
      cpu = REXML::Element.new('cpu', REXML::XPath.first(xml_descr, '/domain'))
      cpu.attributes['mode'] = config.cpu_mode
    else
      if cpu.attributes['mode'] != config.cpu_mode
        @logger.debug "cpu_mode updated from '#{cpu.attributes['mode']}' to '#{config.cpu_mode}'"
        descr_changed = true
        cpu.attributes['mode'] = config.cpu_mode
      end
    end

    if config.cpu_mode != 'host-passthrough'
      cpu_model = REXML::XPath.first(xml_descr, '/domain/cpu/model')
      if cpu_model.nil?
        if config.cpu_model.strip != ''
          @logger.debug "cpu_model updated from not set to '#{config.cpu_model}'"
          descr_changed = true
          cpu_model = REXML::Element.new('model', REXML::XPath.first(xml_descr, '/domain/cpu'))
          cpu_model.attributes['fallback'] = config.cpu_fallback
          cpu_model.text = config.cpu_model
        end
      else
        if (cpu_model.text or '').strip != config.cpu_model.strip
          @logger.debug "cpu_model text updated from #{cpu_model.text} to '#{config.cpu_model}'"
          descr_changed = true
          cpu_model.text = config.cpu_model
        end
        if cpu_model.attributes['fallback'] != config.cpu_fallback
          @logger.debug "cpu_model fallback attribute updated from #{cpu_model.attributes['fallback']} to '#{config.cpu_fallback}'"
          descr_changed = true
          cpu_model.attributes['fallback'] = config.cpu_fallback
        end
      end
      vmx_feature = REXML::XPath.first(xml_descr, '/domain/cpu/feature[@name="vmx"]')
      svm_feature = REXML::XPath.first(xml_descr, '/domain/cpu/feature[@name="svm"]')
      if config.nested
        if vmx_feature.nil?
          @logger.debug "nested mode enabled from unset by setting cpu vmx feature"
          descr_changed = true
          vmx_feature = REXML::Element.new('feature', REXML::XPath.first(xml_descr, '/domain/cpu'))
          vmx_feature.attributes['policy'] = 'optional'
          vmx_feature.attributes['name'] = 'vmx'
        end
        if svm_feature.nil?
          @logger.debug "nested mode enabled from unset by setting cpu svm feature"
          descr_changed = true
          svm_feature = REXML::Element.new('feature', REXML::XPath.first(xml_descr, '/domain/cpu'))
          svm_feature.attributes['policy'] = 'optional'
          svm_feature.attributes['name'] = 'svm'
        end
      else
        unless vmx_feature.nil?
          @logger.debug "nested mode disabled for cpu by removing vmx feature"
          descr_changed = true
          cpu.delete_element(vmx_feature)
        end
        unless svm_feature.nil?
          @logger.debug "nested mode disabled for cpu by removing svm feature"
          descr_changed = true
          cpu.delete_element(svm_feature)
        end
      end
    elsif config.numa_nodes == nil
      unless cpu.elements.to_a.empty?
        @logger.debug "switching cpu_mode to host-passthrough and removing emulated cpu features"
        descr_changed = true
        cpu.elements.each do |elem|
          cpu.delete_element(elem)
        end
      end
    end
  else
    xml_descr.delete_element('/domain/cpu')
  end

  # Clock
  clock = REXML::XPath.first(xml_descr, '/domain/clock')
  if clock.attributes['offset'] != config.clock_offset
    @logger.debug "clock offset changed"
    descr_changed = true
    clock.attributes['offset'] = config.clock_offset
  end

  # clock timers - because timers can be added/removed, just rebuild and then compare
  if !config.clock_timers.empty? || clock.has_elements?
    oldclock = String.new
    formatter.write(REXML::XPath.first(xml_descr, '/domain/clock'), oldclock)
    clock.delete_element('//timer')
    config.clock_timers.each do |clock_timer|
      timer = REXML::Element.new('timer', clock)
      clock_timer.each do |attr, value|
        timer.attributes[attr.to_s] = value
      end
    end

    newclock = String.new
    formatter.write(clock, newclock)
    unless newclock.eql? oldclock
      @logger.debug "clock timers config changed"
      descr_changed = true
    end
  end

  # Launch security
  launchSecurity = REXML::XPath.first(xml_descr, '/domain/launchSecurity')
  unless config.launchsecurity_data.nil?
    if launchSecurity.nil?
      @logger.debug "Launch security has been added"
      launchSecurity = REXML::Element.new('launchSecurity', REXML::XPath.first(xml_descr, '/domain'))
      descr_changed = true
    end

    if launchSecurity.attributes['type'] != config.launchsecurity_data[:type]
      launchSecurity.attributes['type'] = config.launchsecurity_data[:type]
      descr_changed = true
    end

    [:cbitpos, :policy, :reducedPhysBits].each do |setting|
      setting_value = config.launchsecurity_data[setting]
      element = REXML::XPath.first(launchSecurity, setting.to_s)
      if !setting_value.nil?
        if element.nil?
          element = launchSecurity.add_element(setting.to_s)
          descr_changed = true
        end

        if element.text != setting_value
          @logger.debug "launchSecurity #{setting.to_s} config changed"
          element.text = setting_value
          descr_changed = true
        end
      else
        if !element.nil?
          launchSecurity.delete_element(setting.to_s)
          descr_changed = true
        end
      end
    end

    controllers = REXML::XPath.each( xml_descr, '/domain/devices/controller')
    memballoon = REXML::XPath.each( xml_descr, '/domain/devices/memballoon')
    [controllers, memballoon].lazy.flat_map(&:lazy).each do |controller|
      driver_node = REXML::XPath.first(controller, 'driver')
      driver_node = controller.add_element('driver') if driver_node.nil?
      descr_changed = true if driver_node.attributes['iommu'] != 'on'
      driver_node.attributes['iommu'] = 'on'
    end
  else
    unless launchSecurity.nil?
      @logger.debug "Launch security to be deleted"

      descr_changed = true

      launchSecurity.parent.delete_element(launchSecurity)
    end

    REXML::XPath.each( xml_descr, '/domain/devices/controller') do | controller |
      driver_node = REXML::XPath.first(controller, 'driver')
      if !driver_node.nil?
        descr_changed = true if driver_node.attributes['iommu']
        driver_node.attributes.delete('iommu')
      end
    end
  end

  # Graphics
  graphics = REXML::XPath.first(xml_descr, '/domain/devices/graphics')
  if config.graphics_type != 'none'
    if graphics.nil?
      descr_changed = true
      graphics = REXML::Element.new('graphics', REXML::XPath.first(xml_descr, '/domain/devices'))
    end
    if graphics.attributes['type'] != config.graphics_type
      descr_changed = true
      graphics.attributes['type'] = config.graphics_type
    end
    if graphics.attributes['listen'] != config.graphics_ip
      descr_changed = true
      graphics.attributes['listen'] = config.graphics_ip
      graphics.delete_element('//listen')
    end
    if graphics.attributes['autoport'] != config.graphics_autoport
      descr_changed = true
      graphics.attributes['autoport'] = config.graphics_autoport
      if config.graphics_autoport == 'no'
        graphics.attributes.delete('autoport')
        graphics.attributes['port'] = config.graphics_port
      end
    end
    if graphics.attributes['websocket'] != config.graphics_websocket.to_s
      descr_changed = true
      graphics.attributes['websocket'] = config.graphics_websocket
    end
    if graphics.attributes['keymap'] != config.keymap
      descr_changed = true
      graphics.attributes['keymap'] = config.keymap
    end
    if graphics.attributes['passwd'] != config.graphics_passwd
      descr_changed = true
      if config.graphics_passwd.nil?
        graphics.attributes.delete 'passwd'
      else
        graphics.attributes['passwd'] = config.graphics_passwd
      end
    end
    graphics_gl = REXML::XPath.first(xml_descr, '/domain/devices/graphics/gl')
    if graphics_gl.nil?
      if config.graphics_gl
        graphics_gl = REXML::Element.new('gl', REXML::XPath.first(xml_descr, '/domain/devices/graphics'))
        graphics_gl.attributes['enable'] = 'yes'
        descr_changed = true
      end
    else
      if config.graphics_gl
        if graphics_gl.attributes['enable'] != 'yes'
          graphics_gl.attributes['enable'] = 'yes'
          descr_changed = true
        end
      else
        graphics_gl.parent.delete_element(graphics_gl)
        descr_changed = true
      end
    end
  else
    # graphics_type = none, remove entire element
    graphics.parent.delete_element(graphics) unless graphics.nil?
  end

  # TPM
  if [config.tpm_path, config.tpm_version].any?
    if config.tpm_path
      raise Errors::UpdateServerError, 'The TPM Path must be fully qualified' unless config.tpm_path[0].chr == '/'
    end

    # just build the tpm element every time
    # check at the end if it is different
    oldtpm = REXML::XPath.first(xml_descr, '/domain/devices/tpm')
    REXML::XPath.first(xml_descr, '/domain/devices').delete_element("tpm")
    newtpm = REXML::Element.new('tpm', REXML::XPath.first(xml_descr, '/domain/devices'))

    newtpm.attributes['model'] = config.tpm_model
    backend = newtpm.add_element('backend')
    backend.attributes['type'] = config.tpm_type

    case config.tpm_type
    when 'emulator'
      backend.attributes['version'] = config.tpm_version
    when 'passthrough'
      backend.add_element('device').attributes['path'] = config.tpm_path
    end

    unless "'#{newtpm}'".eql? "'#{oldtpm}'"
      @logger.debug "tpm config changed"
      descr_changed = true
    end
  end

  # Video device
  video = REXML::XPath.first(xml_descr, '/domain/devices/video')
  if !video.nil? && (config.graphics_type == 'none')
    # graphics_type = none, video devices are removed since there is no possible output
    @logger.debug "deleting video elements as config.graphics_type is none"
    descr_changed = true
    video.parent.delete_element(video)
  else
    video_model = REXML::XPath.first(xml_descr, '/domain/devices/video/model')
    if video_model.nil?
      @logger.debug "video updated from not set to type '#{config.video_type}' and vram '#{config.video_vram}'"
      descr_changed = true
      video_model = REXML::Element.new('model', REXML::XPath.first(xml_descr, '/domain/devices/video'))
      video_model.attributes['type'] = config.video_type
      video_model.attributes['vram'] = config.video_vram
    else
      if video_model.attributes['type'] != config.video_type || video_model.attributes['vram'] != config.video_vram.to_s
        @logger.debug "video type updated from '#{video_model.attributes['type']}' to '#{config.video_type}'"
        @logger.debug "video vram updated from '#{video_model.attributes['vram']}' to '#{config.video_vram}'"
        descr_changed = true
        video_model.attributes['type'] = config.video_type
        video_model.attributes['vram'] = config.video_vram
      end
    end
    video_accel = REXML::XPath.first(xml_descr, '/domain/devices/video/model/acceleration')
    if video_accel.nil?
      if config.video_accel3d
        video_accel = REXML::Element.new('acceleration', REXML::XPath.first(xml_descr, '/domain/devices/video/model'))
        video_accel.attributes['accel3d'] = 'yes'
        descr_changed = true
      end
    else
      if config.video_accel3d
        if video_accel.attributes['accel3d'] != 'yes'
          video_accel.attributes['accel3d'] = 'yes'
          descr_changed = true
        end
      else
        video_accel.parent.delete_element(video_accel)
        descr_changed = true
      end
    end
  end

  # Sound device
  if config.sound_type
    sound = REXML::XPath.first(xml_descr,'/domain/devices/sound/model')
  end


  # dtb
  if config.dtb
    dtb = REXML::XPath.first(xml_descr, '/domain/os/dtb')
    if dtb.nil?
      @logger.debug "dtb updated from not set to '#{config.dtb}'"
      descr_changed = true
      dtb = REXML::Element.new('dtb', REXML::XPath.first(xml_descr, '/domain/os'))
      dtb.text = config.dtb
    else
      if (dtb.text or '') != config.dtb
        @logger.debug "dtb updated from '#{dtb.text}' to '#{config.dtb}'"
        descr_changed = true
        dtb.text = config.dtb
      end
    end
  end

  # kernel and initrd
  if config.kernel
    kernel = REXML::XPath.first(xml_descr, '/domain/os/kernel')
    if kernel.nil?
      @logger.debug "kernel updated from not set to '#{config.kernel}'"
      descr_changed = true
      kernel = REXML::Element.new('kernel', REXML::XPath.first(xml_descr, '/domain/os'))
      kernel.text = config.kernel
    else
      if (kernel.text or '').strip != config.kernel
        @logger.debug "kernel updated from '#{kernel.text}' to '#{config.kernel}'"
        descr_changed = true
        kernel.text = config.kernel
      end
    end
  end
  if config.initrd
    initrd = REXML::XPath.first(xml_descr, '/domain/os/initrd')
    if initrd.nil?
      if config.initrd.strip != ''
        @logger.debug "initrd updated from not set to '#{config.initrd}'"
        descr_changed = true
        initrd = REXML::Element.new('initrd', REXML::XPath.first(xml_descr, '/domain/os'))
        initrd.text = config.initrd
      end
    else
      if (initrd.text or '').strip != config.initrd
        @logger.debug "initrd updated from '#{initrd.text}' to '#{config.initrd}'"
        descr_changed = true
        initrd.text = config.initrd
      end
    end
  end

  loader = REXML::XPath.first(xml_descr, '/domain/os/loader')
  if config.loader
    if loader.nil?
      descr_changed = true
      loader = REXML::Element.new('loader')
      REXML::XPath.first(xml_descr, '/domain/os').insert_after('//type', loader)
      loader.text = config.loader
    else
      if (loader.text or '').strip != config.loader
        descr_changed = true
        loader.text = config.loader
      end
    end
    loader.attributes['type'] = config.nvram ? 'pflash' : 'rom'
  elsif !loader.nil?
    descr_changed = true
    loader.parent.delete_element(loader)
  end

  nvram = REXML::XPath.first(xml_descr, '/domain/os/nvram')
  if config.nvram
    if nvram.nil?
      descr_changed = true
      nvram = REXML::Element.new('nvram')
      REXML::XPath.first(xml_descr, '/domain/os').insert_after(loader, nvram)
      nvram.text = config.nvram
    else
      if (nvram.text or '').strip != config.nvram
        descr_changed = true
        nvram.text = config.nvram
      end
    end
  elsif !nvram.nil?
    descr_changed = true
    nvram.parent.delete_element(nvram)
  end

  # Apply
  if descr_changed
    env[:ui].info(I18n.t('vagrant_libvirt.updating_domain'))
    new_xml = String.new
    xml_descr.write(new_xml)
    begin
      # providing XML for the same name and UUID will update the existing domain
      libvirt_domain = env[:machine].provider.driver.connection.define_domain(new_xml)
    rescue ::Libvirt::Error => e
      env[:ui].error("Error when updating domain settings: #{e.message}")
      raise Errors::UpdateServerError, error_message: e.message
    end

    # this normalises the attribute order to be the same as what was sent in the above
    # request to update the domain XML. Without this, if the XML documents are not
    # equivalent, many more differences will be reported than there actually are.
    applied_xml = String.new
    REXML::Document.new(libvirt_domain.xml_desc(1)).write(applied_xml)

    # need to check whether the updated XML contains all the changes requested
    proposed = VagrantPlugins::ProviderLibvirt::Util::Xml.new(new_xml)
    applied = VagrantPlugins::ProviderLibvirt::Util::Xml.new(applied_xml)

    # perform some sorting to allow comparison otherwise order of devices differing
    # even if they are equivalent will be reported as being different.
    proposed.xml['devices'][0].each { |_, v| next unless v.is_a?(Array); v.sort_by! { |e| [e['type'], e['index']]} }
    applied.xml['devices'][0].each { |_, v| next unless v.is_a?(Array); v.sort_by! { |e| [e['type'], e['index']]} }

    if proposed != applied
      require 'diffy'

      diff = Diffy::Diff.new(proposed.to_str, applied.to_str, :context => 3).to_s(:text)

      error_msg = "Libvirt failed to fully update the domain with the specified XML. Result differs from requested:\n" +
        "--- requested\n+++ result\n#{diff}\n" +
        "Typically this means there is a bug in the XML being sent, please log an issue as this will halt machine start in the future."

      env[:ui].warn(error_msg)
      #env[:ui].error("Updated domain settings did not fully apply, attempting restore to previous definition: #{error_msg}")
      #begin
      #  env[:machine].provider.driver.connection.define_domain(descr)
      #rescue Fog::Errors::Error => e
      #  env[:ui].error("Failed to restore previous domain definition: #{e.message}")
      #end

      #raise Errors::UpdateServerError, error_message: error_msg
    end
  end

  begin
    # Autostart with host if enabled in Vagrantfile
    libvirt_domain.autostart = config.autostart
    @logger.debug {
      "Starting Domain with XML:\n#{libvirt_domain.xml_desc}"
    }
    # Actually start the domain
    env[:ui].info(I18n.t('vagrant_libvirt.starting_domain'))
    domain.start
  rescue Fog::Errors::Error, Errors::VagrantLibvirtError => e
    raise Errors::DomainStartError, error_message: e.message
  end

  #libvirt_domain = env[:machine].provider.driver.connection.client.lookup_domain_by_uuid(env[:machine].id)
  xmldoc = REXML::Document.new(libvirt_domain.xml_desc)
  graphics = REXML::XPath.first(xmldoc, '/domain/devices/graphics')

  if !graphics.nil?
    if config.graphics_autoport
      env[:ui].info(I18n.t('vagrant_libvirt.starting_domain_with_graphics'))
      env[:ui].info(" -- Graphics Port:      #{graphics.attributes['port']}")
      env[:ui].info(" -- Graphics IP:        #{graphics.attributes['listen']}")
      env[:ui].info(" -- Graphics Password:  #{config.graphics_passwd.nil? ? 'Not defined' : 'Defined'}")
    end

    if config.graphics_websocket == -1
      env[:ui].info(" -- Graphics Websocket: #{graphics.attributes['websocket']}")
    end
  end

  @app.call(env)
end