lib/fog/libvirt/models/compute/server.rb
require 'fog/compute/models/server'
require 'fog/libvirt/models/compute/util/util'
require 'fileutils'
module Fog
module Libvirt
class Compute
class Server < Fog::Compute::Server
include Fog::Libvirt::Util
attr_reader :xml
identity :id, :aliases => 'uuid'
attribute :cpus
attribute :cputime
attribute :os_type
attribute :memory_size
attribute :max_memory_size
attribute :name
attribute :arch
attribute :persistent
attribute :domain_type
attribute :uuid
attribute :autostart
attribute :nics
attribute :volumes
attribute :active
attribute :boot_order
attribute :display
attribute :cpu
attribute :hugepages
attribute :guest_agent
attribute :virtio_rng
attribute :state
# The following attributes are only needed when creating a new vm
#TODO: Add depreciation warning
attr_accessor :iso_dir, :iso_file
attr_accessor :network_interface_type ,:network_nat_network, :network_bridge_name
attr_accessor :volume_format_type, :volume_allocation,:volume_capacity, :volume_name, :volume_pool_name, :volume_template_name, :volume_path
attr_accessor :password
attr_accessor :user_data
# Can be created by passing in :xml => "<xml to create domain/server>"
# or by providing :template_options => {
# :name => "", :cpus => 1, :memory_size => 256 , :volume_template
# }
def initialize(attributes={} )
@xml = attributes.delete(:xml)
verify_boot_order(attributes[:boot_order])
super defaults.merge(attributes)
initialize_nics
initialize_volumes
@user_data = attributes.delete(:user_data)
end
def new?
uuid.nil?
end
def save
raise Fog::Errors::Error.new('Saving an existing server may create a duplicate') unless new?
create_or_clone_volume unless xml or @volumes
create_user_data_iso if user_data
@xml ||= to_xml
self.id = (persistent ? service.define_domain(xml) : service.create_domain(xml)).uuid
reload
rescue => e
raise Fog::Errors::Error.new("Error saving the server: #{e}")
end
def start
return true if active?
action_status = service.vm_action(uuid, :create)
reload
action_status
end
def update_autostart(value)
service.update_autostart(uuid, value)
end
def mac
nics.first.mac if nics && nics.first
end
def disk_path
volumes.first.path if volumes and volumes.first
end
def destroy(options={ :destroy_volumes => false, :flags => ::Libvirt::Domain::UNDEFINE_NVRAM })
poweroff unless stopped?
if options.fetch(:flags, ::Libvirt::Domain::UNDEFINE_NVRAM).zero?
service.vm_action(uuid, :undefine)
else
service.vm_action(uuid, :undefine, options[:flags])
end
volumes.each { |vol| vol.destroy } if options[:destroy_volumes]
true
end
def reboot
action_status = service.vm_action(uuid, :reboot)
reload
action_status
end
def poweroff
action_status = service.vm_action(uuid, :destroy)
reload
action_status
end
def shutdown
action_status = service.vm_action(uuid, :shutdown)
reload
action_status
end
def resume
action_status = service.vm_action(uuid, :resume)
reload
action_status
end
def suspend
action_status = service.vm_action(uuid, :suspend)
reload
action_status
end
def stopped?
state == "shutoff"
end
def ready?
state == "running"
end
#alias methods
alias_method :halt, :poweroff
alias_method :stop, :shutdown
alias_method :active?, :active
alias_method :autostart?, :autostart
def volumes
# lazy loading of volumes
@volumes ||= (@volumes_path || []).map{ |path| service.volumes.all(:path => path).first }.compact
end
def private_ip_address
ip_address(:private)
end
def public_ip_address
ip_address(:public)
end
def ssh(commands)
requires :ssh_ip_address, :username
ssh_options={}
ssh_options[:password] = password unless password.nil?
ssh_options[:proxy]= ssh_proxy unless ssh_proxy.nil?
super(commands, ssh_options)
end
def ssh_proxy
begin
require 'net/ssh/proxy/command'
rescue LoadError
Fog::Logger.warning("'net/ssh' missing, please install and try again.")
exit(1)
end
# if this is a direct connection, we don't need a proxy to be set.
return nil unless connection.uri.ssh_enabled?
user_string= service.uri.user ? "-l #{service.uri.user}" : ""
Net::SSH::Proxy::Command.new("ssh #{user_string} #{service.uri.host} nc %h %p")
end
# Transfers a file
def scp(local_path, remote_path, upload_options = {})
requires :ssh_ip_address, :username
scp_options = {}
scp_options[:password] = password unless self.password.nil?
scp_options[:key_data] = [private_key] if self.private_key
scp_options[:proxy]= ssh_proxy unless self.ssh_proxy.nil?
Fog::SCP.new(ssh_ip_address, username, scp_options).upload(local_path, remote_path, upload_options)
end
# Sets up a new key
def setup(credentials = {})
requires :public_key, :ssh_ip_address, :username
credentials[:proxy]= ssh_proxy unless ssh_proxy.nil?
credentials[:password] = password unless self.password.nil?
credentials[:key_data] = [private_key] if self.private_key
commands = [
%{mkdir .ssh},
# %{passwd -l #{username}}, #Not sure if we need this here
# %{echo "#{Fog::JSON.encode(attributes)}" >> ~/attributes.json}
]
if public_key
commands << %{echo "#{public_key}" >> ~/.ssh/authorized_keys}
end
# wait for domain to be ready
Timeout::timeout(360) do
begin
Timeout::timeout(8) do
Fog::SSH.new(ssh_ip_address, username, credentials.merge(:timeout => 4)).run('pwd')
end
rescue Errno::ECONNREFUSED
sleep(2)
retry
rescue Net::SSH::AuthenticationFailed, Timeout::Error
retry
end
end
Fog::SSH.new(ssh_ip_address, username, credentials).run(commands)
end
def update_display attrs = {}
service.update_display attrs.merge(:uuid => uuid)
reload
end
# can't use deprecate method, as the value is part of the display hash
def vnc_port
Fog::Logger.deprecation("#{self.class} => #vnc_port is deprecated, use #display[:port] instead [light_black](#{caller.first})[/]")
display[:port]
end
def generate_config_iso(user_data, &blk)
Dir.mktmpdir('config') do |wd|
generate_config_iso_in_dir(wd, user_data, &blk)
end
end
def generate_config_iso_in_dir(dir_path, user_data, &blk)
FileUtils.touch(File.join(dir_path, "meta-data"))
File.open(File.join(dir_path, 'user-data'), 'w') { |f| f.write user_data }
isofile = Tempfile.new(['init', '.iso']).path
unless system("genisoimage -output #{isofile} -volid cidata -joliet -rock #{File.join(dir_path, 'user-data')} #{File.join(dir_path, 'meta-data')}")
raise Fog::Errors::Error.new("Couldn't generate cloud-init iso disk with genisoimage.")
end
blk.call(isofile)
end
def create_user_data_iso
generate_config_iso(user_data) do |iso|
vol = service.volumes.create(:name => cloud_init_volume_name, :capacity => "#{File.size(iso)}b", :allocation => "0G")
vol.upload_image(iso)
@iso_file = cloud_init_volume_name
@iso_dir = File.dirname(vol.path) if vol.path
end
end
def cloud_init_volume_name
"#{name}-cloud-init.iso"
end
# rubocop:disable Metrics
def to_xml
builder = Nokogiri::XML::Builder.new do |xml|
xml.domain(:type => domain_type) do
xml.name(name)
xml.memory(memory_size)
if hugepages
xml.memoryBacking do
xml.hugepages
end
end
xml.vcpu(cpus)
xml.os do
type = xml.type(os_type, :arch => arch)
type[:machine] = "q35" if ["i686", "x86_64"].include?(arch)
boot_order.each do |dev|
xml.boot(:dev => dev)
end
end
xml.features do
xml.acpi
xml.apic
end
unless cpu.empty?
if cpu.dig(:model, :name)
xml.cpu do
xml.model(cpu.dig(:model, :name), :fallback => cpu.dig(:model, :fallback) || "allow")
end
else
xml.cpu(
:mode => cpu.dig(:model, :name) || "host-passthrough",
:check => cpu.fetch(:check, "none"),
:migratable => cpu.fetch(:migratable, "on")
)
end
end
xml.clock(:offset => "utc") do
xml.timer(:name => "rtc", :tickpolicy => "catchup")
xml.timer(:name => "pit", :tickpolicy => "delay")
xml.timer(:name => "hpet", :present => "no")
end
xml.devices do
ceph_args = read_ceph_args
volumes.each_with_index do |volume, index|
target_device = "vd#{('a'..'z').to_a[index]}"
if ceph_args && volume.pool_name.include?(ceph_args["libvirt_ceph_pool"])
xml.disk(:type => "network", :device => "disk") do
xml.driver(:name => "qemu", :type => volume.format_type, :cache => "writeback", :discard => "unmap")
xml.source(:protocol => "rbd", :name => volume.path)
ceph_args["monitor"]&.split(",")&.each do |monitor|
xml.host(:name => monitor, :port => ceph_args["port"])
end
xml.auth(:username => ceph_args["auth_username"]) do
if ceph_args.key?("auth_uuid")
xml.secret(:type => "ceph", :uuid => ceph_args["auth_uuid"])
else
xml.secret(:type => "ceph", :usage => ceph_args["auth_usage"])
end
end
xml.target(:dev => target_device, :bus => args["bus_type"] == "virtio" ? "virtio" : "scsi")
end
else
is_block = volume.path.start_with?("/dev/")
xml.disk(:type => is_block ? "block" : "file", :device => "disk") do
xml.driver(:name => "qemu", :type => volume.format_type)
if is_block
xml.source(:dev => volume.path)
else
xml.source(:file => volume.path)
end
xml.target(:dev => target_device, :bus => "virtio")
end
end
end
if iso_file
xml.disk(:type => "file", :device => "cdrom") do
xml.driver(:name => "qemu", :type => "raw")
xml.source(:file => "#{iso_dir}/#{iso_file}")
xml.target(:dev => "hdc", :bus => "ide")
xml.readonly
xml.address(:type => "drive", :controller => 0, :bus => 1, :unit => 0)
end
end
nics.each do |nic|
xml.interface(:type => nic.type) do
if nic.type == "bridge"
xml.source(:bridge => nic.bridge)
else
xml.source(:network => nic.network)
end
xml.model(:type => nic.model)
end
end
if guest_agent
xml.channel(:type => "unix") do
xml.target(:type => "virtio", :name => "org.qemu.guest_agent.0")
end
end
xml.rng(:model => "virtio") do
xml.backend(virtio_rng[:backend_path], :model => virtio_rng.fetch(:backend_model, "random"))
end
if arch == "s390x"
xml.controller(:type => "scsi", :index => "0", :model => "virtio-scsi")
xml.console(:type => "pty") do
xml.target(:type => "sclp")
end
xml.memballoon(:model => "virtio")
else
xml.serial(:type => "pty") do
xml.target(:port => 0)
end
xml.console(:type => "pty") do
xml.target(:port => 0)
end
xml.input(:type => "tablet", :bus => "usb")
xml.input(:type => "mouse", :bus => "ps2")
graphics = xml.graphics(:type => display[:type])
if display[:port].empty?
graphics.port = display[:port]
graphics.autoport = "no"
else
graphics.port = -1
graphics.autoport = "yes"
end
graphics.listen = display[:listen] unless display[:listen].empty?
graphics.password = display[:password] unless display[:password].empty?
xml.video do
xml.model(:type => "cirrus", :vram => 9216, :heads => 1)
end
end
end
end
end
builder.to_xml
end
# rubocop:enable Metrics
private
attr_accessor :volumes_path
def read_ceph_args(path = "/etc/foreman/ceph.conf")
return unless File.file?(path)
args = {}
File.readlines(path).each do |line|
pair = line.strip.split("=")
key = pair[0]
value = pair[1]
args[key] = value
end
args
end
# This tests the library version before redefining the address
# method for this instance to use a method compatible with
# earlier libvirt libraries, or uses the dhcp method from more
# recent releases.
def addresses(service_arg=service, options={})
addresses_method = self.method(:addresses_dhcp)
# check if ruby-libvirt was compiled against a new enough version
# that can use dhcp_leases, as otherwise it will not provide the
# method dhcp_leases on any of the network objects.
has_dhcp_leases = true
begin
service.networks.first.dhcp_leases(self.mac)
rescue NoMethodError
has_dhcp_leases = false
rescue
# assume some other odd exception.
end
# if ruby-libvirt not compiled with support, or remote library is
# too old (must be newer than 1.2.8), then use old fallback
if not has_dhcp_leases or service.libversion() < 1002008
addresses_method = self.method(:addresses_ip_command)
end
# replace current definition for this instance with correct one for
# detected libvirt to perform check once for connection
(class << self; self; end).class_eval do
define_method(:addresses, addresses_method)
end
addresses(service_arg, options)
end
def ssh_ip_command(ip_command, uri)
# Retrieve the parts we need from the service to setup our ssh options
user=uri.user #could be nil
host=uri.host
keyfile=uri.keyfile
port=uri.port
# Setup the options
ssh_options={}
ssh_options[:keys]=[ keyfile ] unless keyfile.nil?
ssh_options[:port]=port unless keyfile.nil?
ssh_options[:paranoid]=true if uri.no_verify?
begin
result=Fog::SSH.new(host, user, ssh_options).run(ip_command)
rescue Errno::ECONNREFUSED
raise Fog::Errors::Error.new("Connection was refused to host #{host} to retrieve the ip_address for #{mac}")
rescue Net::SSH::AuthenticationFailed
raise Fog::Errors::Error.new("Error authenticating over ssh to host #{host} and user #{user}")
end
# Check for a clean exit code
if result.first.status == 0
return result.first.stdout.strip
else
# We got a failure executing the command
raise Fog::Errors::Error.new("The command #{ip_command} failed to execute with a clean exit code")
end
end
def local_ip_command(ip_command)
# Execute the ip_command locally
# Initialize empty ip_address string
ip_address=""
IO.popen("#{ip_command}") do |p|
p.each_line do |l|
ip_address+=l
end
status=Process.waitpid2(p.pid)[1].exitstatus
if status!=0
raise Fog::Errors::Error.new("The command #{ip_command} failed to execute with a clean exit code")
end
end
#Strip any new lines from the string
ip_address.chomp
end
# Locale-friendly removal of non-alpha nums
DOMAIN_CLEANUP_REGEXP = Regexp.compile('[\W_-]')
# This retrieves the ip address of the mac address using ip_command
# It returns an array of public and private ip addresses
# Currently only one ip address is returned, but in the future this could be multiple
# if the server has multiple network interface
def addresses_ip_command(service_arg=service, options={})
mac=self.mac
# Aug 24 17:34:41 juno arpwatch: new station 10.247.4.137 52:54:00:88:5a:0a eth0.4
# Aug 24 17:37:19 juno arpwatch: changed ethernet address 10.247.4.137 52:54:00:27:33:00 (52:54:00:88:5a:0a) eth0.4
# Check if another ip_command string was provided
ip_command_global=service_arg.ip_command.nil? ? 'grep $mac /var/log/arpwatch.log|sed -e "s/new station//"|sed -e "s/changed ethernet address//g" |sed -e "s/reused old ethernet //" |tail -1 |cut -d ":" -f 4-| cut -d " " -f 3' : service_arg.ip_command
ip_command_local=options[:ip_command].nil? ? ip_command_global : options[:ip_command]
ip_command="mac=#{mac}; server_name=#{name.gsub(DOMAIN_CLEANUP_REGEXP, '_')}; "+ip_command_local
ip_address=nil
if service_arg.uri.ssh_enabled?
ip_address=ssh_ip_command(ip_command, service_arg.uri)
else
# It's not ssh enabled, so we assume it is
if service_arg.uri.transport=="tls"
raise Fog::Errors::Error.new("TlS remote transport is not currently supported, only ssh")
end
ip_address=local_ip_command(ip_command)
end
# The Ip-address command has been run either local or remote now
if ip_address==""
#The grep didn't find an ip address result"
ip_address=nil
else
# To be sure that the command didn't return another random string
# We check if the result is an actual ip-address
# otherwise we return nil
unless ip_address=~/^(\d{1,3}\.){3}\d{1,3}$/
raise Fog::Errors::Error.new(
"The result of #{ip_command} does not have valid ip-address format\n"+
"Result was: #{ip_address}\n"
)
end
end
return { :public => [ip_address], :private => [ip_address]}
end
# This retrieves the ip address of the mac address using dhcp_leases
# It returns an array of public and private ip addresses
# Currently only one ip address is returned, but in the future this could be multiple
# if the server has multiple network interface
def addresses_dhcp(service_arg=service, options={})
mac=self.mac
ip_address = nil
nic = self.nics.find {|nic| nic.mac==mac}
if !nic.nil?
net = service.networks.all(:name => nic.network).first
if !net.nil?
leases = net.dhcp_leases(mac, 0)
# Assume the lease expiring last is the current IP address
ip_address = leases.sort_by { |lse| lse["expirytime"] }.last["ipaddr"] if !leases.empty?
end
end
return { :public => [ip_address], :private => [ip_address] }
end
def ip_address(key)
addresses[key].nil? ? nil : addresses[key].first
end
def initialize_nics
if nics
nics.map! { |nic| nic.is_a?(Hash) ? service.nics.new(nic) : nic }
else
self.nics = [service.nics.new({:type => network_interface_type, :bridge => network_bridge_name, :network => network_nat_network})]
end
end
def initialize_volumes
if attributes[:volumes] && !attributes[:volumes].empty?
@volumes = attributes[:volumes].map { |vol| vol.is_a?(Hash) ? service.volumes.new(vol) : vol }
end
end
def create_or_clone_volume
options = {:name => volume_name || default_volume_name}
# Check if a disk template was specified
if volume_template_name
template_volume = service.volumes.all(:name => volume_template_name).first
raise Fog::Errors::Error.new("Template #{volume_template_name} not found") unless template_volume
begin
volume = template_volume.clone("#{options[:name]}")
rescue => e
raise Fog::Errors::Error.new("Error creating the volume : #{e}")
end
else
# If no template volume was given, let's create our own volume
options[:pool_name] = volume_pool_name if volume_pool_name
options[:format_type] = volume_format_type if volume_format_type
options[:capacity] = volume_capacity if volume_capacity
options[:allocation] = volume_allocation if volume_allocation
begin
volume = service.volumes.create(options)
rescue => e
raise Fog::Errors::Error.new("Error creating the volume : #{e}")
end
end
@volumes.nil? ? @volumes = [volume] : @volumes << volume
end
def default_iso_dir
"/var/lib/libvirt/images"
end
def default_volume_name
"#{name}.#{volume_format_type || 'img'}"
end
def defaults
{
:persistent => true,
:cpus => 1,
:memory_size => 256 * 1024,
:name => randomized_name,
:os_type => "hvm",
:arch => "x86_64",
:domain_type => "kvm",
:autostart => false,
:iso_dir => default_iso_dir,
:network_interface_type => "network",
:network_nat_network => "default",
:network_bridge_name => "br0",
:boot_order => %w[hd cdrom network],
:display => default_display,
:cpu => {},
:hugepages => false,
:guest_agent => true,
:virtio_rng => {},
}
end
def verify_boot_order order = []
valid_boot_media = %w[cdrom fd hd network]
if order
order.each do |b|
raise "invalid boot order, possible values are any combination of: #{valid_boot_media.join(', ')}" unless valid_boot_media.include?(b)
end
end
end
def default_display
{:port => '-1', :listen => '127.0.0.1', :type => 'vnc', :password => '' }
end
end
end
end
end