lib/ovirt/vm.rb
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