ManageIQ/ovirt

View on GitHub
lib/ovirt/vm.rb

Summary

Maintainability
C
1 day
Test Coverage
F
56%
require_relative 'legacy_support/cloud_init_via_floppy_payload'

module Ovirt
  class Vm < Template
    self.top_level_strings    = [:name, :origin, :type, :description]
    self.top_level_booleans   = [:stateless]
    self.top_level_integers   = [:memory]
    self.top_level_timestamps = [:creation_time, :start_time]
    self.top_level_objects    = [:cluster, :template, :host]

    include CloudInitViaFloppyPayload

    attr_accessor :creation_status_link

    def initialize(*args)
      super

      @creation_status_link = @relationships.delete(:creation_status)
    end

    def creation_status
      return nil if @creation_status_link.blank?
      @service.status(@creation_status_link)
    end

    def start
      if block_given?
        operation(:start) { |xml| yield xml }
      else
        operation(:start)
      end
    rescue Ovirt::Error => err
      raise VmAlreadyRunning, err.message if err.message.include?("VM is running.")
      raise
    end

    def stop
      operation(:stop)
    rescue Ovirt::Error => err
      raise VmIsNotRunning, err.message if err.message.include?("VM is not running")
      raise
    end

    def shutdown
      operation(:shutdown)
    rescue Ovirt::Error => err
      raise VmIsNotRunning, err.message if err.message.include?("VM is not running")
      raise
    end

    def destroy
      # TODO: If VM was running, wait for it to stop
      begin
        stop
      rescue VmIsNotRunning
      end

      super
    end

    def export(storage_domain)
      response = operation(:export) do |xml|
        xml.storage_domain(:id => self.class.object_to_id(storage_domain))
      end

      self.class.response_to_action(response)['href']
    end

    def move(storage_domain)
      response = operation(:move) do |xml|
        xml.storage_domain(:id => self.class.object_to_id(storage_domain))
      end

      # Sample Response
      # <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
      # <action id="13be9571-61a0-40ef-be6c-50696f61cab1" href="/api/vms/5819ddca-47c0-47d8-8b75-c5b8d1f2b354/move/13be9571-61a0-40ef-be6c-50696f61cab1">
      #   <link rel="parent" href="/api/vms/5819ddca-47c0-47d8-8b75-c5b8d1f2b354"/>
      #   <link rel="replay" href="/api/vms/5819ddca-47c0-47d8-8b75-c5b8d1f2b354/move"/>
      #   <async>true</async>
      #   <storage_domain id="08d61895-b465-406f-955c-72fd9ddbbe05"/>
      #   <status>
      #     <state>pending</state>
      #   </status>
      # </action>

      self.class.response_to_action(response)['href']
    end

    def boot_order=(devices)
      devices = [devices].flatten
      update! do |xml|
        xml.os do
          devices.each do |d|
            xml.boot(:dev => d)
          end
        end
      end
    end

    # cpu_hash needs to look like { :cores => 1, :sockets => 1 }
    def cpu_topology=(cpu_hash)
      update! do |xml|
        xml.cpu do
          xml.topology(cpu_hash)
        end
        xml.memory self[:memory]  # HACK: RHEVM BUG: RHEVM resets it to 10GB, unless we set it
      end
    end

    def description=(value)
      update! do |xml|
        xml.description value.to_s
        xml.memory self[:memory]  # HACK: RHEVM BUG: RHEVM resets it to 10GB, unless we set it
      end
    end

    def host_affinity=(host, affinity = :migratable)
      update! do |xml|
        xml.memory self[:memory]  # HACK: RHEVM BUG: RHEVM resets it to 10GB, unless we set it
        xml.placement_policy do
          if host.nil?
            xml.host
          else
            xml.host(:id => self.class.object_to_id(host))
          end
          xml.affinity affinity.to_s
        end
      end
    end

    #
    # Updates the memory of the virtual machine.
    #
    # @param memory [Integer] The virtual memory assigned to the virtual machine, in bytes. If it is `nil` then
    #   the virtual memory won't be updated.
    #
    # @param guaranteed [Integer] The amount of physical memory reserved for the virtual machine, in bytes. If
    #   it is `nil` then the guaranteed memory won't be updated.
    #
    # @param matrix [Hash] Optional matrix parameters to pass to the update operation, for example `next_run => true`.
    #
    def update_memory(memory, guaranteed, matrix = {})
      update!(matrix) do |xml|
        if memory
          xml.memory memory
        end
        if guaranteed
          xml.memory_policy do
            xml.guaranteed guaranteed
          end
        end
      end
    end

    def memory=(value)
      update_memory(value, nil)
    end

    def memory_reserve=(value)
      update_memory(nil, value)
    end

    def cloud_init=(content)
      if service.version[:major].to_i == 3 && service.version[:minor].to_i < 4
        super  # Use legacy method defined in CloudInitViaFloppyPayload
      else
        begin
          update! do |xml|
            xml.initialization do
              converted_cloud_init(content).each { |k, v| xml.send(k, v) }
            end
          end
        rescue Ovirt::UsageError => err
          raise CloudInitSyntaxError, err.message
        end
      end
    end

    # Attaches a payload.
    #
    # payloads:: Hash of payload_type => {file_name => content} that will be
    #   attached.  Acceptable payload_types are floppy or cdrom.
    def attach_payload(payloads)
      send("attach_payload_#{payload_version}".to_sym, payloads)
    end

    def payload_version
      version = service.version

      # Note that in this context "3_3" actually means 3.3 *or newer*, including 3.6 and 4.0.
      return "3_0" if version[:major].to_i == 3 && version[:minor].to_i < 3
      "3_3"
    end

    def attach_payload_3_0(payloads)
      update! do |xml|
        xml.payloads do
          payloads.each do |type, files|
            xml.payload(:type => type) do
              files.each do |file_name, content|
                xml.file(:name => file_name) do
                  xml.content content
                end
              end
            end
          end
        end
      end
    end

    def attach_payload_3_3(payloads)
      update! do |xml|
        xml.payloads do
          payloads.each do |type, files|
            xml.payload(:type => type) do
              xml.files do
                files.each do |file_name, content|
                  xml.file do
                    xml.name file_name
                    xml.content content
                  end
                end
              end
            end
          end
        end
      end
    end

    # Detaches a payload
    #
    # types:: A payload type or Array of payload types to detach.
    #         Acceptable types are floppy or cdrom.
    def detach_payload(types)
      send("detach_payload_#{payload_version}".to_sym, Array(types))
    end

    def detach_payload_3_0(types)
      # HACK: The removal of payloads is not supported until possibly RHEVM 3.1.1
      #       https://bugzilla.redhat.com/show_bug.cgi?id=882649
      #       For now, just set the payload to blank content.
      payload = types.each_with_object({}) { |t, h| h[t] = {} }
      attach_payload(payload)
    end

    def detach_payload_3_3(_types)
      update!(&:payloads)
    end

    # Attaches the +files+ as a floppy drive payload.
    #
    # files:: Hash of file_name => content that will be attached as a floppy
    def attach_floppy(files)
      attach_payload("floppy" => files || {})
    end

    # Detaches the floppy drive payload.
    def detach_floppy
      detach_payload("floppy")
    end

    def boot_from_network
      start do |xml|
        xml.vm do
          xml.os do
            xml.boot(:dev => 'network')
          end
        end
      end
    rescue Ovirt::Error => err
      raise VmNotReadyToBoot, [err.message, err] if err.message =~ /disks .+ are locked/
      raise
    end

    def boot_from_cdrom(iso_file_name)
      start do |xml|
        xml.vm do
          xml.os do
            xml.boot(:dev => 'cdrom')
          end
          xml.cdroms do
            xml.cdrom do
              xml.file(:id => iso_file_name)
            end
          end
        end
      end
    rescue Ovirt::Error => err
      raise VmNotReadyToBoot, [err.message, err] if err.message =~ /disks .+ are locked/
      raise
    end

    def self.parse_xml(xml)
      hash = super
      node = xml_to_nokogiri(xml)

      parse_first_node(node, :placement_policy, hash,
                       :node => [:affinity])

      parse_first_node_with_hash(node, 'placement_policy/host', hash[:placement_policy][:host] = {},
                                 :attribute => [:id]) if hash.key?(:placement_policy)

      parse_first_node(node, :memory_policy, hash,
                       :node_to_i => [:guaranteed])

      hash[:guest_info] = {}
      node.xpath('guest_info').each do |gi|
        ips = hash[:guest_info][:ips] = []
        gi.xpath('ips/ip').each do |ip|
          ips << {:address => ip[:address]}
        end

        fqdn = gi.xpath('fqdn').text
        hash[:guest_info][:fqdn] = fqdn unless fqdn.blank?
      end

      hash
    end

    def create_device(device_type)
      builder = Nokogiri::XML::Builder.new do |xml|
        xml.send(device_type) { yield xml if block_given? }
      end
      data = builder.doc.root.to_xml
      path = "#{api_endpoint}/#{device_type.pluralize}"

      @service.resource_post(path, data)
    end

    def create_nic(options = {})
      create_device("nic") do |xml|
        xml.name      options[:name]
        xml.interface options[:interface] unless options[:interface].blank?
        xml.network(:id => options[:network_id]) unless options[:network_id].blank?
        xml.mac(:address => options[:mac_address]) unless options[:mac_address].blank?
      end
    end

    def create_disk(options = {})
      create_device("disk") do |xml|
        [:name, :interface, :format, :size, :type].each do |key|
          next if options[key].blank?
          xml.send("#{key}_", options[key])
        end

        [:sparse, :bootable, :wipe_after_delete, :propagate_errors].each do |key|
          xml.send("#{key}_", options[key]) unless options[key].nil?
        end

        xml.storage_domains { xml.storage_domain(:id => options[:storage]) } if options[:storage]
      end
    end

    def create_snapshot(desc)
      builder = Nokogiri::XML::Builder.new do |xml|
        xml.snapshot do
          xml.description desc
        end
      end
      data     = builder.doc.root.to_xml
      path     = "#{api_endpoint}/snapshots"
      response = @service.resource_post(path, data)
      snap     = Snapshot.create_from_xml(@service, response)

      while snap[:snapshot_status] == "locked"
        sleep 2
        snap.reload
      end
      snap
    end

    def create_template(options = {})
      builder = Nokogiri::XML::Builder.new do |xml|
        xml.template do
          xml.name options[:name]
          xml.vm(:id => self[:id])
        end
      end
      data     = builder.doc.root.to_xml
      response = @service.resource_post(:templates, data)

      Template.create_from_xml(@service, response)
    rescue Ovirt::Error => err
      raise TemplateAlreadyExists, err.message if err.message.include?("Template name already exists")
      raise
    end

    private

    def converted_cloud_init(content)
      ovirt_cloud_init_keys = %w(active_directory_ou authorized_ssh_keys dns_search dns_servers domain host_name input_locale nic_configurations org_name regenerate_ssh_keys root_password system_locale timezone ui_language user_locale user_name)

      require 'yaml'
      raw_content          = YAML.load(content)
      hash                 = ovirt_cloud_init_keys.each_with_object({}) { |k, h| h[k] = raw_content.delete(k) }
      hash[:custom_script] = YAML.dump(raw_content).sub("---\n", "") if raw_content.present?
      hash.delete_nils
    end
  end
end