app/models/compute_resources/foreman/model/ovirt.rb
require 'foreman/exception'
require 'uri'
module Foreman::Model
class Ovirt < ComputeResource
ALLOWED_DISPLAY_TYPES = %w(vnc spice)
validates :url, :format => { :with => URI::DEFAULT_PARSER.make_regexp }, :presence => true,
:url_schema => ['http', 'https']
validates :display_type, :inclusion => { :in => ALLOWED_DISPLAY_TYPES }
validates :keyboard_layout, :inclusion => { :in => ALLOWED_KEYBOARD_LAYOUTS }
validates :user, :password, :presence => true
after_validation :connect, :update_available_operating_systems unless Rails.env.test?
before_save :validate_quota
alias_attribute :datacenter, :uuid
delegate :clusters, :quotas, :templates, :instance_types, :to => :client
def self.available?
Fog::Compute.providers.include?(:ovirt)
end
def self.model_name
ComputeResource.model_name
end
def user_data_supported?
true
end
def host_compute_attrs(host)
super.tap do |attrs|
attrs[:os] = { :type => determine_os_type(host) } if supports_operating_systems?
end
end
def capabilities
[:build, :image, :new_volume]
end
def find_vm_by_uuid(uuid)
super
rescue Fog::Ovirt::Errors::OvirtEngineError
raise(ActiveRecord::RecordNotFound)
end
def supports_update?
true
end
def supports_operating_systems?
if client.respond_to?(:operating_systems)
unless attrs.key?(:available_operating_systems)
update_available_operating_systems
save
end
attrs[:available_operating_systems] != :unsupported
else
false
end
rescue Foreman::FingerprintException
logger.info "Unable to verify OS capabilities, SSL certificate verification failed"
false
end
def determine_os_type(host)
return nil unless host
return host.params['ovirt_ostype'] if host.params['ovirt_ostype']
ret = "other_linux"
return ret unless host.operatingsystem
os_name = os_name_mapping(host)
arch_name = arch_name_mapping(host)
available = available_operating_systems.select { |os| os[:name].present? }
match_found = false
best_matches = available.sort_by do |os|
rating = 0.0
if os[:name].include?(os_name)
match_found = true
rating += 100
# prefer the shorter names a bit in case we have not found more important some specifics
rating += (1.0 / os[:name].length)
# bonus for major or major_minor
rating += 10 if os[:name].include?("#{os_name}_#{host.operatingsystem.major}")
rating += 5 if os[:name].include?("#{os_name}_#{host.operatingsystem.major}_#{host.operatingsystem.minor}")
# bonus for architecture
rating += 10 if arch_name && os[:name].include?(arch_name)
end
rating
end
unless match_found
logger.debug { "No oVirt OS type found, returning other OS" }
return available.first[:name]
end
logger.debug { "Available oVirt OS types: #{best_matches.map { |x| x[:name] }.join(',')}" }
best_matches.last[:name] if best_matches.last
end
def available_operating_systems
if attrs.key?(:available_operating_systems)
attrs[:available_operating_systems]
else
raise Foreman::Exception.new("Listing operating systems is not supported by the current version")
end
end
def provided_attributes
super.merge({:mac => :mac})
end
def ovirt_quota=(ovirt_quota_id)
attrs[:ovirt_quota_id] = ovirt_quota_id
end
def ovirt_quota
attrs[:ovirt_quota_id].presence
end
def available_images
templates
end
def image_exists?(image)
client.templates.get(image).present?
rescue => e
Foreman::Logging.exception("Error while checking if image exists", e)
false
end
def template(id)
compute = client.templates.get(id) || raise(ActiveRecord::RecordNotFound)
compute.interfaces
compute.volumes
compute
end
def instance_type(id)
client.instance_types.get(id) || raise(ActiveRecord::RecordNotFound)
end
def display_types
ALLOWED_DISPLAY_TYPES
end
# Check if HTTPS is mandatory, since rest_client will fail with a POST
def test_https_required
RestClient.post url, {} if URI(url).scheme == 'http'
true
rescue => e
case e.message
when /406/
true
else
raise e
end
end
private :test_https_required
def test_connection(options = {})
super
connect(options)
end
def connect(options = {})
return unless connection_properties_valid?
update_public_key options
datacenters && test_https_required
rescue => e
case e.message
when /404/
errors.add(:url, e.message)
when /302/
errors.add(:url, _('HTTPS URL is required for API access'))
when /401/
errors.add(:user, e.message)
else
errors.add(:base, e.message)
end
end
def connection_properties_valid?
errors[:url].empty? && errors[:username].empty? && errors[:password].empty?
end
def datacenters(options = {})
client.datacenters(options).map { |dc| [dc[:name], dc[:id]] }
end
def get_datacenter_uuid(datacenter)
return @datacenter_uuid if @datacenter_uuid
if Foreman.is_uuid?(datacenter)
@datacenter_uuid = datacenter
else
@datacenter_uuid = datacenters.select { |dc| dc[0] == datacenter }
raise ::Foreman::Exception.new(N_('Datacenter was not found')) if @datacenter_uuid.empty?
@datacenter_uuid = @datacenter_uuid.first[1]
end
@datacenter_uuid
end
def editable_network_interfaces?
# we can't decide whether the networks are available when we
# don't know the cluster_id, assuming it's possible
true
end
def vnic_profiles
client.list_vnic_profiles
end
def networks(opts = {})
if opts[:cluster_id]
client.clusters.get(opts[:cluster_id]).networks
else
[]
end
end
def available_clusters
clusters
end
def available_networks(cluster_id = nil)
raise ::Foreman::Exception.new(N_('Cluster ID is required to list available networks')) if cluster_id.nil?
networks({:cluster_id => cluster_id})
end
def available_storage_domains(cluster_id = nil)
storage_domains
end
def storage_domains(opts = {})
client.storage_domains({:role => ['data', 'volume']}.merge(opts))
end
def start_vm(uuid)
vm = find_vm_by_uuid(uuid)
if vm.comment.to_s =~ %r{cloud-config|^#!/}
vm.start_with_cloudinit(:blocking => true, :user_data => vm.comment, :use_custom_script => true)
vm.comment = ''
vm.save
else
vm.start(:blocking => true)
end
end
def start_with_cloudinit(uuid, user_data = nil)
find_vm_by_uuid(uuid).start_with_cloudinit(:blocking => true, :user_data => user_data, :use_custom_script => true)
end
def sanitize_inherited_vm_attributes(args, template, instance_type)
# Override memory an cores values if template and/or instance type is/are provided.
# Take template values if blank values for VM attributes, because oVirt will fail if empty values are present in VM definition
# Instance type values always take precedence on templates or vm provided values
if template
template_cores = template.cores.to_i if template.cores.present?
template_memory = template.memory.to_i if template.memory.present?
args[:cores] = template_cores if template_cores && args[:cores].blank?
args[:memory] = template_memory if template_memory && args[:memory].blank?
end
if instance_type
instance_type_cores = instance_type.cores.to_i if instance_type.cores.present?
instance_type_sockets = instance_type.sockets.to_i if instance_type.sockets.present?
instance_type_memory = instance_type.memory.to_i if instance_type.memory.present?
args[:cores] = instance_type_cores if instance_type_cores
args[:sockets] = instance_type_sockets if instance_type_sockets
args[:memory] = instance_type_memory if instance_type_memory
end
end
def create_vm(args = {})
args[:comment] = args[:user_data] if args[:user_data]
args[:template] = args[:image_id] if args[:image_id]
template = template(args[:template]) if args[:template]
instance_type = instance_type(args[:instance_type]) unless args[:instance_type].empty?
args[:cluster] = get_ovirt_id(clusters, 'cluster', args[:cluster])
sanitize_inherited_vm_attributes(args, template, instance_type)
preallocate_and_clone_disks(args, template) if args[:volumes_attributes].present? && template.present?
vm = super({ :first_boot_dev => 'network', :quota => ovirt_quota }.merge(args))
begin
create_interfaces(vm, args[:interfaces_attributes], args[:cluster]) unless args[:interfaces_attributes].empty?
create_volumes(vm, args[:volumes_attributes]) unless args[:volumes_attributes].empty?
rescue => e
destroy_vm vm.id
raise e
end
vm
end
def get_ovirt_id(argument_list, argument_key, argument_value)
return argument_value if argument_value.blank?
if argument_list.none? { |a| a.name == argument_value || a.id == argument_value }
raise Foreman::Exception.new("The #{argument_key} #{argument_value} is not valid, enter a correct id or name")
else
argument_list.detect { |a| a.name == argument_value }.try(:id) || argument_value
end
end
def preallocate_and_clone_disks(args, template)
volumes_to_change = args[:volumes_attributes].values.select { |x| x[:id].present? }
return unless volumes_to_change.present?
template_disks = template.volumes
disks = volumes_to_change.map do |volume|
if volume[:preallocate] == '1'
{:id => volume[:id], :sparse => 'false', :format => 'raw', :storage_domain => volume[:storage_domain]}
else
template_volume = template_disks.detect { |v| v.id == volume["id"] }
{:id => volume["id"], :storage_domain => volume["storage_domain"]} if template_volume.storage_domain != volume["storage_domain"]
end
end.compact
args.merge!(:clone => true, :disks => disks) if disks.present?
end
def vm_instance_defaults
super.merge(
:memory => 1024.megabytes,
:cores => '1',
:sockets => '1',
:display => { :type => display_type,
:keyboard_layout => keyboard_layout,
:port => -1,
:monitors => 1 }
)
end
def new_vm(attr = {})
vm = super
interfaces = nested_attributes_for :interfaces, attr[:interfaces_attributes]
interfaces.map { |i| vm.interfaces << new_interface(i) }
volumes = nested_attributes_for :volumes, attr[:volumes_attributes]
volumes.map { |v| vm.volumes << new_volume(v) }
vm
end
def new_interface(attr = {})
Fog::Ovirt::Compute::Interface.new(attr)
end
def new_volume(attr = {})
set_preallocated_attributes!(attr, attr[:preallocate])
raise ::Foreman::Exception.new(N_('VM volume attributes are not set properly')) unless attr.all? { |key, value| value.is_a? String }
Fog::Ovirt::Compute::Volume.new(attr)
end
def save_vm(uuid, attr)
vm = find_vm_by_uuid(uuid)
vm.attributes.deep_merge!(attr.deep_symbolize_keys).deep_symbolize_keys
update_interfaces(vm, attr[:interfaces_attributes])
update_volumes(vm, attr[:volumes_attributes])
vm.interfaces
vm.volumes
vm.save
end
def destroy_vm(uuid)
find_vm_by_uuid(uuid).destroy
rescue ActiveRecord::RecordNotFound
true
end
def supports_vms_pagination?
true
end
def parse_vms_list_params(params)
max = (params['length'] || 10).to_i
{
:search => params['search']['value'] || '',
:max => max,
:page => (params['start'].to_i / max) + 1,
:without_details => true,
}
end
def console(uuid)
vm = find_vm_by_uuid(uuid)
raise "VM is not running!" if vm.status == "down"
opts = if vm.display[:secure_port]
{ :host_port => vm.display[:secure_port], :ssl_target => true }
else
{ :host_port => vm.display[:port] }
end
WsProxy.start(opts.merge(:host => vm.display[:address], :password => vm.ticket)).merge(:name => vm.name, :type => vm.display[:type])
end
def update_required?(old_attrs, new_attrs)
return true if super(old_attrs, new_attrs)
new_attrs[:interfaces_attributes]&.each do |key, interface|
return true if (interface[:id].blank? || interface[:_delete] == '1') && key != 'new_interfaces' # ignore the template
end
new_attrs[:volumes_attributes]&.each do |key, volume|
return true if (volume[:id].blank? || volume[:_delete] == '1') && key != 'new_volumes' # ignore the template
end
false
end
def associated_host(vm)
associate_by("mac", vm.interfaces.map(&:mac))
end
def self.provider_friendly_name
"oVirt"
end
def display_type
attrs[:display].presence || 'vnc'
end
def display_type=(display)
attrs[:display] = display.downcase
end
def keyboard_layout
attrs[:keyboard_layout].presence || 'en-us'
end
def keyboard_layout=(layout)
attrs[:keyboard_layout] = layout.downcase
end
def public_key
attrs[:public_key]
end
def public_key=(key)
attrs[:public_key] = key
end
def normalize_vm_attrs(vm_attrs)
normalized = slice_vm_attributes(vm_attrs, ['cores', 'interfaces_attributes', 'memory'])
normalized['cluster_id'] = get_ovirt_id(clusters, 'cluster', vm_attrs['cluster'])
normalized['cluster_name'] = clusters.detect { |c| c.id == normalized['cluster_id'] }.try(:name)
normalized['template_id'] = get_ovirt_id(templates, 'template', vm_attrs['template'])
normalized['template_name'] = templates.detect { |t| t.id == normalized['template_id'] }.try(:name)
cluster_networks = networks(:cluster_id => normalized['cluster_id'])
interface_attrs = vm_attrs['interfaces_attributes'] || {}
normalized['interfaces_attributes'] = interface_attrs.inject({}) do |interfaces, (key, nic)|
interfaces.update(key => { 'name' => nic['name'],
'network_id' => nic['network'],
'network_name' => cluster_networks.detect { |n| n.id == nic['network'] }.try(:name),
})
end
volume_attrs = vm_attrs['volumes_attributes'] || {}
normalized['volumes_attributes'] = volume_attrs.inject({}) do |volumes, (key, vol)|
volumes.update(key => { 'size' => memory_gb_to_bytes(vol['size_gb']).to_s,
'storage_domain_id' => vol['storage_domain'],
'storage_domain_name' => storage_domains.detect { |d| d.id == vol['storage_domain'] }.try(:name),
'preallocate' => to_bool(vol['preallocate']),
'bootable' => to_bool(vol['bootable']),
})
end
normalized
end
def nictypes
[
OpenStruct.new({:id => 'virtio', :name => 'VirtIO'}),
OpenStruct.new({:id => 'rtl8139', :name => 'rtl8139'}),
OpenStruct.new({:id => 'e1000', :name => 'e1000'}),
OpenStruct.new({:id => 'pci_passthrough', :name => 'PCI Passthrough'}),
]
end
def validate_quota
if attrs[:ovirt_quota_id].nil?
attrs[:ovirt_quota_id] = client.quotas.first.id
else
attrs[:ovirt_quota_id] = get_ovirt_id(client.quotas, 'quota', attrs[:ovirt_quota_id])
end
end
protected
def bootstrap(args)
client.servers.bootstrap vm_instance_defaults.merge(args.to_h)
rescue Fog::Errors::Error => e
Foreman::Logging.exception("Failed to bootstrap vm", e)
errors.add(:base, e.to_s)
false
end
def client
return @client if @client
client = ::Fog::Compute.new(
:provider => "ovirt",
:ovirt_username => user,
:ovirt_password => password,
:ovirt_url => url,
:ovirt_datacenter => uuid,
:ovirt_ca_cert_store => ca_cert_store(public_key),
:public_key => public_key,
:api_version => 'v4'
)
client.datacenters
@client = client
rescue => e
if e.message =~ /SSL_connect.*certificate verify failed/ ||
e.message =~ /Peer certificate cannot be authenticated with given CA certificates/ ||
e.message =~ /SSL peer certificate or SSH remote key was not OK/
raise Foreman::FingerprintException.new(
N_("The remote system presented a public key signed by an unidentified certificate authority. If you are sure the remote system is authentic, go to the compute resource edit page, press the 'Test Connection' or 'Load Datacenters' button and submit"),
ca_cert
)
else
raise e
end
end
def update_public_key(options = {})
return unless public_key.blank? || options[:force]
client
rescue Foreman::FingerprintException => e
self.public_key = e.fingerprint if public_key.blank?
end
def api_version
@api_version ||= client.api_version
end
def ca_cert_store(certs)
return if certs.blank?
store = OpenSSL::X509::Store.new
certs.split(/(?=-----BEGIN)/).each do |cert|
store.add_cert(OpenSSL::X509::Certificate.new(cert))
end
store
rescue => e
raise _("Failed to create X509 certificate, error: %s" % e.message)
end
def fetch_unverified(path, query = '')
ca_url = URI.parse(url)
ca_url.path = path
ca_url.query = query
http = Net::HTTP.new(ca_url.host, ca_url.port)
http.use_ssl = (ca_url.scheme == 'https')
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Get.new(ca_url)
response = http.request(request)
# response might be 404 or some other normal code,
# that would not trigger any exception so we rather check what kind of response we got
response.is_a?(Net::HTTPSuccess) ? response.body : nil
rescue => e
Foreman::Logging.exception("Unable to fetch CA certificate on path #{path}: #{e}", e)
nil
end
def ca_cert
fetch_unverified("/ovirt-engine/services/pki-resource", "resource=ca-certificate&format=X509-PEM-CA") || fetch_unverified("/ca.crt")
end
private
def update_available_operating_systems
return false if errors.any?
ovirt_operating_systems = client.operating_systems if client.respond_to?(:operating_systems)
attrs[:available_operating_systems] = ovirt_operating_systems.map do |os|
{ :id => os.id, :name => os.name, :href => os.href }
end
rescue Foreman::FingerprintException
logger.info "Unable to verify OS capabilities, SSL certificate verification failed"
true
rescue Fog::Ovirt::Errors::OvirtEngineError => e
if e.message =~ /404/
attrs[:available_operating_systems] ||= :unsupported
else
raise e
end
end
def os_name_mapping(host)
(host.operatingsystem.name =~ /redhat|centos/i) ? 'rhel' : host.operatingsystem.name.downcase
end
def arch_name_mapping(host)
return unless host.architecture
(host.architecture.name == 'x86_64') ? 'x64' : host.architecture.name.downcase
end
def default_iface_name(interfaces)
nic_name_num = 1
name_blacklist = interfaces.map { |i| i[:name] }.reject { |n| n.blank? }
nic_name_num += 1 while name_blacklist.include?("nic#{nic_name_num}")
"nic#{nic_name_num}"
end
def create_interfaces(vm, attrs, cluster_id)
# first remove all existing interfaces
vm.interfaces&.each do |interface|
# The blocking true is a work-around for ovirt bug, it should be removed.
vm.destroy_interface(:id => interface.id, :blocking => true)
end
# add interfaces
cluster_networks = networks(:cluster_id => cluster_id)
profiles = vnic_profiles
interfaces = nested_attributes_for :interfaces, attrs
interfaces.map do |interface|
interface[:name] = default_iface_name(interfaces) if interface[:name].empty?
raise Foreman::Exception.new("Interface network or vnic profile are missing.") if (interface[:network].nil? && interface[:vnic_profile].nil?)
interface[:network] = get_ovirt_id(cluster_networks, 'network', interface[:network]) if interface[:network].present?
interface[:vnic_profile] = get_ovirt_id(profiles, 'vnic profile', interface[:vnic_profile]) if interface[:vnic_profile].present?
if (interface[:network].present? && interface[:vnic_profile].present?)
unless (profiles.select { |profile| profile.network.id == interface[:network] }).present?
raise Foreman::Exception.new("Vnic Profile have a different network")
end
end
vm.add_interface(interface)
end
vm.interfaces.reload
end
def create_volumes(vm, attrs)
# add volumes
volumes = nested_attributes_for :volumes, attrs
volumes.map do |vol|
if vol[:id].blank?
set_preallocated_attributes!(vol, vol[:preallocate])
vol[:wipe_after_delete] = to_fog_ovirt_boolean(vol[:wipe_after_delete])
vol[:storage_domain] = get_ovirt_id(storage_domains, 'storage domain', vol[:storage_domain])
# The blocking true is a work-around for ovirt bug fixed in ovirt version 5.1
# The BZ in ovirt cause to the destruction of a host in foreman to fail in case a volume is locked
# Here we are enforcing blocking behavior which will wait until the volume is added
vm.add_volume({:bootable => 'false', :quota => ovirt_quota, :blocking => api_version.to_f < 5.1}.merge(vol))
end
end
vm.volumes.reload
end
def to_fog_ovirt_boolean(val)
case val
when '1'
'true'
when '0'
'false'
else
val
end
end
def set_preallocated_attributes!(volume_attributes, preallocate)
if preallocate == '1'
volume_attributes[:sparse] = 'false'
volume_attributes[:format] = 'raw'
else
volume_attributes[:sparse] = 'true'
end
end
def update_interfaces(vm, attrs)
interfaces = nested_attributes_for :interfaces, attrs
interfaces.each do |interface|
vm.destroy_interface(:id => interface[:id]) if interface[:_delete] == '1' && interface[:id]
if interface[:id].blank?
interface[:name] = default_iface_name(interfaces) if interface[:name].empty?
vm.add_interface(interface)
end
end
end
def update_volumes(vm, attrs)
volumes = nested_attributes_for :volumes, attrs
volumes.each do |volume|
vm.destroy_volume(:id => volume[:id], :blocking => api_version.to_f < 3.1) if volume[:_delete] == '1' && volume[:id].present?
vm.add_volume({:bootable => 'false', :quota => ovirt_quota, :blocking => api_version.to_f < 3.1}.merge(volume)) if volume[:id].blank?
end
end
def set_vm_interfaces_attributes(vm, vm_attrs)
if vm.respond_to?(:interfaces)
interfaces = vm.interfaces || []
vm_attrs[:interfaces_attributes] = interfaces.each_with_index.each_with_object({}) do |(interface, index), hsh|
interface_attrs = {
mac: interface.mac,
compute_attributes: {
name: interface.name,
network: interface.network,
interface: interface.interface,
vnic_profile: interface.vnic_profile,
},
}
hsh[index.to_s] = interface_attrs
end
end
vm_attrs
end
def get_template_volumes(vm_attrs)
return {} unless vm_attrs[:template]
template = template(vm_attrs[:template])
return {} unless template
return {} if template.volumes.nil?
template.volumes.index_by(&:name)
end
def volume_to_attributes(volume, template_volumes)
{
size_gb: (volume.size.to_i / 1.gigabyte),
storage_domain: volume.storage_domain,
preallocate: (volume.sparse == 'true') ? '0' : '1',
wipe_after_delete: volume.wipe_after_delete,
interface: volume.interface,
bootable: volume.bootable,
id: template_volumes.fetch(volume.name, nil)&.id,
}
end
def set_vm_volumes_attributes(vm, vm_attrs)
template_volumes = get_template_volumes(vm_attrs)
vm_volumes = vm.volumes || []
vm_attrs[:volumes_attributes] = vm_volumes.each_with_index.each_with_object({}) do |(volume, index), volumes|
volumes[index.to_s] = volume_to_attributes(volume, template_volumes)
end
vm_attrs
end
end
end