Project

General

Profile

Download (18.3 KB) Statistics
| Branch: | Tag: | Revision:
require 'foreman/exception'
require 'uri'

module Foreman::Model
class Ovirt < ComputeResource
validates :url, :format => { :with => URI::DEFAULT_PARSER.make_regexp }, :presence => true,
:url_schema => ['http', 'https']
validates :user, :password, :presence => true
after_validation :connect, :update_available_operating_systems unless Rails.env.test?

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 self.attrs.key?(:available_operating_systems)
update_available_operating_systems
save
end
self.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
ret = "other_linux"
return ret unless host.operatingsystem
os_name = os_name_mapping(host)
arch_name = arch_name_mapping(host)

best_match = available_operating_systems.select { |os| os[:name].present? }.max_by do |os|
rating = 0.0
if os[:name].include?(os_name)
rating += 100
rating += (1.0 / os[:name].length) # prefer the shorter names a bit in case we have not found more important some specifics
rating += 10 if os[:name].include?("#{os_name}_#{host.operatingsystem.major}")
rating += 10 if arch_name && os[:name].include?(arch_name)
end
rating
end

best_match[:name] if best_match
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

# FIXME
def max_cpu_count
8
end

def max_memory
16.gigabytes
end

def use_v4=(value)
value = case value
when true, '1'
true
else
false
end
self.attrs[:ovirt_use_v4] = value
end

def use_v4
self.attrs[:ovirt_use_v4] || false
end
alias_method :use_v4?, :use_v4

def ovirt_quota=(ovirt_quota_id)
self.attrs[:ovirt_quota_id] = ovirt_quota_id
end

def ovirt_quota
self.attrs[:ovirt_quota_id].presence
end

def available_images
templates
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

# 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[:url] << e.message
when /302/
errors[:url] << _('HTTPS URL is required for API access')
when /401/
errors[:user] << e.message
else
errors[: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 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 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(storage_domain = nil)
storage_domains
end

def storage_domains(opts = {})
client.storage_domains({:role => 'data'}.merge(opts))
end

def start_vm(uuid)
vm = find_vm_by_uuid(uuid)
if vm.comment.include? "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)
# Cleanup memory an cores values if template and/or instance type provided when VM values are
# * Blank values for these attributes, because oVirt will fail if empty values are present in VM definition
# * Provided but identical to values present in the template or instance type
# Instance type values take precedence on templates values
if args[:template].present?
template = template(args[:template])
cores = template.cores.to_i if template.cores.present?
memory = template.memory.to_i if template.memory.present?
end
if args[:instance_type].present?
instance_type = instance_type(args[:instance_type])
cores = instance_type.cores.to_i if instance_type.cores.present?
memory = instance_type.memory.to_i if instance_type.memory.present?
end
args.delete(:cores) if (args[:cores].blank? && cores) || (args[:cores].to_i == cores)
args.delete(:memory) if (args[:memory].blank? && memory) || (args[:memory].to_i == memory)
end

def create_vm(args = {})
args[:comment] = args[:user_data] if args[:user_data]
args[:template] = args[:image_id] if args[:image_id]

sanitize_inherited_vm_attributes(args)

preallocate_disks(args) if args[:volumes_attributes].present?

vm = super({ :first_boot_dev => 'network', :quota => ovirt_quota }.merge(args))

begin
create_interfaces(vm, args[:interfaces_attributes])
create_volumes(vm, args[:volumes_attributes])
rescue => e
destroy_vm vm.id
raise e
end
vm
end

def preallocate_disks(args)
change_allocation_volumes = args[:volumes_attributes].values.select{ |x| x[:preallocate] == '1' }
if args[:template].present? && change_allocation_volumes.present?
disks = change_allocation_volumes.map do |volume|
{ :id => volume[:id], :sparse => 'false', :format => 'raw', :storagedomain => volume[:storage_domain] }
end
args.merge!(:clone => true, :disks => disks)
end
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::Compute::Ovirt::Interface.new(attr)
end

def new_volume(attr = {})
set_preallocated_attributes!(attr, attr[:preallocate])
Fog::Compute::Ovirt::Volume.new(attr)
end

def save_vm(uuid, attr)
vm = find_vm_by_uuid(uuid)
vm.attributes.merge!(attr.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"
if vm.display[:type] =~ /spice/i
xpi_opts = {:name => vm.name, :address => vm.display[:address], :secure_port => vm.display[:secure_port], :ca_cert => public_key, :subject => vm.display[:subject] }
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(xpi_opts).merge(:type => 'spice')
else
WsProxy.start(:host => vm.display[:address], :host_port => vm.display[:port], :password => vm.ticket).merge(:name => vm.name, :type => 'vnc')
end
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 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'] = vm_attrs['cluster']
normalized['cluster_name'] = self.clusters.detect { |c| c.id == normalized['cluster_id'] }.try(:name)

normalized['template_id'] = vm_attrs['template']
normalized['template_name'] = self.templates.detect { |t| t.id == normalized['template_id'] }.try(:name)

cluster_networks = self.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

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 => use_v4? ? 'v4' : 'v3'
)
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/
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 self.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)
(host.architecture.name == 'x86_64') ? 'x64' : host.architecture.name.downcase if host.architecture
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)
# 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
interfaces = nested_attributes_for :interfaces, attrs
interfaces.map do |interface|
interface[:name] = default_iface_name(interfaces) if interface[:name].empty?
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|
set_preallocated_attributes!(vol, vol[:preallocate])
# The blocking true is a work-around for ovirt bug fixed in ovirt version 3.1.
vm.add_volume({:bootable => 'false', :quota => ovirt_quota, :blocking => api_version.to_f < 3.1}.merge(vol)) if vol[:id].blank?
end
vm.volumes.reload
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
end
end
(5-5/7)