# frozen_string_literal: true require 'fog/libvirt' require 'libvirt' require 'log4r' require 'json' module VagrantPlugins module ProviderLibvirt class Driver # store the connection at the instance level as this will be per # thread and allows for individual machines to use different # connection settings. # # possibly this should be a connection pool using the connection # settings as a key to allow identical connections to be reused # across machines. @connection = nil @system_connection = nil def initialize(machine) @logger = Log4r::Logger.new('vagrant_libvirt::driver') @machine = machine end def connection # If already connected to Libvirt, just use it and don't connect # again. return @connection if @connection # Get config options for Libvirt provider. config = @machine.provider_config uri = config.uri # Setup command for retrieving IP address for newly created machine # with some MAC address. Get it from dnsmasq leases table ip_command = %q( awk "/$mac/ {print \$1}" /proc/net/arp ) conn_attr = { provider: 'libvirt', libvirt_uri: uri, libvirt_ip_command: ip_command, } conn_attr[:libvirt_username] = config.username if config.username conn_attr[:libvirt_password] = config.password if config.password @logger.info("Connecting to Libvirt (#{uri}) ...") begin @connection = Fog::Compute.new(conn_attr) rescue Fog::Errors::Error => e raise Errors::FogLibvirtConnectionError, error_message: e.message end @connection end def system_connection # If already connected to Libvirt, just use it and don't connect # again. return @system_connection if @system_connection config = @machine.provider_config @system_connection = Libvirt.open_read_only(config.system_uri) @system_connection end def created? domain = get_domain() !domain.nil? end def get_ipaddress(domain=nil) # Find the machine domain = get_domain() if domain.nil? if domain.nil? # The machine can't be found return nil end # attempt to get ip address from qemu agent if @machine.provider_config.qemu_use_agent == true @logger.info('Get IP via qemu agent') return get_ipaddress_from_qemu_agent(@machine.config.vm.boot_timeout, domain) end return get_ipaddress_from_system domain.mac if @machine.provider_config.qemu_use_session # Get IP address from dhcp leases table begin ip_address = get_ipaddress_from_domain(domain) rescue Fog::Errors::TimeoutError @logger.info("Timeout at waiting for an ip address for machine #{@machine.name}") raise end unless ip_address @logger.info("No arp table entry found for machine #{@machine.name}") return nil end ip_address end def restore_snapshot(snapshot_name) domain = get_libvirt_domain() snapshot = get_snapshot_if_exists(snapshot_name) begin # 4 is VIR_DOMAIN_SNAPSHOT_REVERT_FORCE # needed due to https://bugzilla.redhat.com/show_bug.cgi?id=1006886 domain.revert_to_snapshot(snapshot, 4) rescue Fog::Errors::Error => e raise Errors::SnapshotReversionError, error_message: e.message end end def list_snapshots get_libvirt_domain().list_snapshots rescue Fog::Errors::Error => e raise Errors::SnapshotListError, error_message: e.message end def delete_snapshot(snapshot_name) get_snapshot_if_exists(snapshot_name).delete rescue Errors::SnapshotMissing => e raise Errors::SnapshotDeletionError, error_message: e.message end def create_new_snapshot(snapshot_name) snapshot_desc = <<-EOF #{snapshot_name} Snapshot for vagrant sandbox EOF get_libvirt_domain().snapshot_create_xml(snapshot_desc) rescue Fog::Errors::Error => e raise Errors::SnapshotCreationError, error_message: e.message end def create_snapshot(snapshot_name) begin delete_snapshot(snapshot_name) rescue Errors::SnapshotDeletionError end create_new_snapshot(snapshot_name) end # if we can get snapshot description without exception it exists def get_snapshot_if_exists(snapshot_name) snapshot = get_libvirt_domain().lookup_snapshot_by_name(snapshot_name) return snapshot if snapshot.xml_desc rescue Libvirt::RetrieveError => e raise Errors::SnapshotMissing, error_message: e.message end def state # may be other error states with initial retreival we can't handle begin domain = get_domain rescue Libvirt::RetrieveError => e @logger.debug("Machine #{@machine.id} not found #{e}.") return :not_created end # TODO: terminated no longer appears to be a valid fog state, remove? return :not_created if domain.nil? return :unknown if domain.state.nil? return :not_created if domain.state.to_sym == :terminated state = domain.state.tr('-', '_').to_sym if state == :running begin get_ipaddress(domain) rescue Fog::Errors::TimeoutError => e @logger.debug("Machine #{@machine.id} running but no IP address available: #{e}.") return :inaccessible end end state end def get_domain begin domain = connection.servers.get(@machine.id) rescue Libvirt::RetrieveError => e raise e unless e.libvirt_code == ProviderLibvirt::Util::ErrorCodes::VIR_ERR_NO_DOMAIN @logger.debug("machine #{@machine.name} domain not found #{e}.") return nil end domain end def list_all_networks client = if @machine.provider_config.qemu_use_session system_connection else connection.client end client.list_all_networks.select do |net| begin net.bridge_name rescue Libvirt::Error # there does not appear to be a mechanism to determine the type of network, only by # querying the attribute and catching the error is it possible to ignore unsupported. @logger.debug "Ignoring #{net.name} as it does not support retrieval of bridge_name attribute" next end end end def host_devices @host_devices ||= begin cmd = [] unless @machine.provider_config.proxy_command.nil? || @machine.provider_config.proxy_command.empty? cmd = ['ssh', @machine.provider_config.host] cmd += ['-p', @machine.provider_config.port.to_s] if @machine.provider_config.port cmd += ['-l', @machine.provider_config.username] if @machine.provider_config.username cmd += ['-i', @machine.provider_config.id_ssh_key_file] if @machine.provider_config.id_ssh_key_file end ip_cmd = cmd + %W(ip -j link show) result = Vagrant::Util::Subprocess.execute(*ip_cmd) raise Errors::FogLibvirtConnectionError unless result.exit_code == 0 info = JSON.parse(result.stdout) ( info.map { |iface| iface['ifname'] } + connection.client.list_all_interfaces.map { |iface| iface.name } + list_all_networks.map { |net| net.bridge_name } ).uniq.reject(&:empty?) end end def attach_device(xml) get_libvirt_domain.attach_device(xml) end def detach_device(xml) get_libvirt_domain.detach_device(xml) end private def get_ipaddress_from_system(mac) ip_address = nil list_all_networks.each do |net| leases = net.dhcp_leases(mac, 0) # Assume the lease expiring last is the current IP address ip_address = leases.max_by { |lse| lse['expirytime'] }['ipaddr'] unless leases.empty? break if ip_address end ip_address end def get_ipaddress_from_qemu_agent(timeout, domain=nil) ip_address = nil addresses = nil libvirt_domain = get_libvirt_domain() begin response = libvirt_domain.qemu_agent_command('{"execute":"guest-network-get-interfaces"}', timeout) @logger.debug('Got Response from qemu agent') @logger.debug(response) addresses = JSON.parse(response) rescue StandardError => e @logger.debug("Unable to receive IP via qemu agent: [#{e.message}]") end unless addresses.nil? addresses['return'].each do |interface| next unless interface.key?('hardware-address') next unless domain.mac.downcase == interface['hardware-address'].downcase @logger.debug("Found matching interface: [#{interface['name']}]") next unless interface.key?('ip-addresses') interface['ip-addresses'].each do |ip| # returning ipv6 addresses might break windows guests because # winrm can't handle connection, winrm fails with "invalid uri" next unless ip['ip-address-type'] == 'ipv4' ip_address = ip['ip-address'] @logger.debug("Return IP: [#{ip_address}]") break end end end ip_address end def get_ipaddress_from_domain(domain) ip_address = nil domain.wait_for(2) do addresses.each_pair do |_type, ip| # Multiple leases are separated with a newline, return only # the most recent address ip_address = ip[0].split("\n").first unless ip[0].nil? end !ip_address.nil? end ip_address end def get_libvirt_domain begin libvirt_domain = connection.client.lookup_domain_by_uuid(@machine.id) rescue Libvirt::RetrieveError => e raise e unless e.libvirt_code == ProviderLibvirt::Util::ErrorCodes::VIR_ERR_NO_DOMAIN @logger.debug("machine #{@machine.name} not found #{e}.") return nil end libvirt_domain end end end end