#!/usr/bin/perl
# 
# This daemon watches for changes in KVM/qemu virtual servers; Booted servers, stopped servers, and changed 
# servers. 
# 
# At this point, the only thing this does is call 'scan-server' when a change is detected.
# 
# NOTE: This is designed to be minimal overhead, so there is no attempt to connect to the database. As such, 
#       be mindful of what this daemon is used for.
# 

use strict;
use warnings;
use Data::Dumper;
use Text::Diff;
use Anvil::Tools;
use Sys::Virt;

my $THIS_FILE           =  ($0 =~ /^.*\/(.*)$/)[0];
my $running_directory   =  ($0 =~ /^(.*?)\/$THIS_FILE$/)[0];
if (($running_directory =~ /^\./) && ($ENV{PWD}))
{
	$running_directory =~ s/^\./$ENV{PWD}/;
}

# Turn off buffering so that the pinwheel will display while waiting for the SSH call(s) to complete.
$| = 1;

my $anvil = Anvil::Tools->new();

# Read switches
$anvil->Get->switches({list => [], man => $THIS_FILE});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => $anvil->data->{switches}});
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 2, 'print' => 1, key => "log_0115", variables => { program => $THIS_FILE }});

# If this is a striker, exit, we shouldn't be running here.
if ($anvil->Get->host_type eq "striker")
{
	$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, 'print' => 1, key => "log_0744"});
	sleep 2;
	$anvil->nice_exit({exit_code => 1});
}

$anvil->data->{libvirtd}{target} = "127.0.0.1";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { 
	"libvirtd::target" => $anvil->data->{libvirtd}{target},
}});
$anvil->Remote->test_access({target => $anvil->data->{libvirtd}{target}});

# Calculate my sum so that we can exit if it changes later.
$anvil->Storage->record_md5sums;
my $next_md5sum_check = time + 30;

# Now go into the main loop
while(1)
{
	### NOTE: A lot of this logic comes from scan-server
	my $scan_time = time;
	my $trigger   = 0;
	
	# Connect to libvirtd
	connect_to_libvirtd($anvil);
	$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { 
		"libvirtd::connection" => $anvil->data->{libvirtd}{connection},
	}});
	if (ref($anvil->data->{libvirtd}{connection}) ne "Sys::Virt")
	{
		$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, 'print' => 1, key => "log_0820"});
		sleep 2;
		next;
	}
	
	my @domains = $anvil->data->{libvirtd}{connection}->list_all_domains();
	my $count   = @domains;
	delete $anvil->data->{this_scan};
	foreach my $domain (@domains)
	{
		my $server_name         = $domain->get_name;
		my $server_id           = $domain->get_id == -1 ? "" : $domain->get_id; 
		my $server_uuid         = $domain->get_uuid_string;
		my ($state, $reason)    = $domain->get_state();
		my $active_definition   = $domain->get_xml_description();
		my $inactive_definition = $domain->get_xml_description(Sys::Virt::Domain::XML_INACTIVE);
		$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { 
			"s1:server_name" => $server_name,
			"s2:server_id"   => $server_id, 
			"s3:server_uuid" => $server_uuid, 
			"s4:state"       => $state, 
			"s5:reason"      => $reason, 
		}});
		$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { 
			"s1:active_definition"   => $active_definition, 
			"s2:inactive_definition" => $inactive_definition, 
		}});
		
		### Reasons are dependent on the state. 
		### See: https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainShutdownReason
		my $server_state = "unknown";
		if ($state == 1)    { $server_state = "running"; }	# Server is running.
		elsif ($state == 2) { $server_state = "blocked"; }	# Server is blocked (IO contention?).
		elsif ($state == 3) { $server_state = "paused"; }	# Server is paused (migration target?).
		elsif ($state == 4) { $server_state = "in shutdown"; }	# Server is shutting down.
		elsif ($state == 5) { $server_state = "shut off"; }	# Server is shut off.
		elsif ($state == 6) { $server_state = "crashed"; }	# Server is crashed!
		elsif ($state == 7) { $server_state = "pmsuspended"; }	# Server is suspended.
		$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { server_state => $server_state }});
		
		$anvil->data->{this_scan}{server_uuid}{$server_uuid}{name}                = $server_name;
		$anvil->data->{this_scan}{server_uuid}{$server_uuid}{id}                  = $server_id;
		$anvil->data->{this_scan}{server_uuid}{$server_uuid}{'state'}             = $server_state;
		$anvil->data->{this_scan}{server_uuid}{$server_uuid}{active_definition}   = $active_definition;
		$anvil->data->{this_scan}{server_uuid}{$server_uuid}{inactive_definition} = $inactive_definition;
		$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { 
			"s1:this_scan::server_uuid::${server_uuid}::name"  => $anvil->data->{this_scan}{server_uuid}{$server_uuid}{name}, 
			"s2:this_scan::server_uuid::${server_uuid}::id"    => $anvil->data->{this_scan}{server_uuid}{$server_uuid}{id}, 
			"s2:this_scan::server_uuid::${server_uuid}::state" => $anvil->data->{this_scan}{server_uuid}{$server_uuid}{'state'}, 
		}});
		$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { 
			"s1:this_scan::server_uuid::${server_uuid}::active_definition"   => $anvil->data->{this_scan}{server_uuid}{$server_uuid}{active_definition}, 
			"s2:this_scan::server_uuid::${server_uuid}::inactive_definition" => $anvil->data->{this_scan}{server_uuid}{$server_uuid}{inactive_definition}, 
		}});
		
		if (not exists $anvil->data->{last_scan}{server_uuid}{$server_uuid})
		{
			# First time seeing it.
			$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, 'print' => 1, key => "log_0844", variables => { 
				name => $server_name,
				uuid => $server_uuid, 
			}});
			
			$trigger = 1;
			$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { trigger => $trigger }});
		}
		else
		{
			if (($anvil->data->{this_scan}{server_uuid}{$server_uuid}{name}                ne $anvil->data->{last_scan}{server_uuid}{$server_uuid}{name})              or 
			    ($anvil->data->{this_scan}{server_uuid}{$server_uuid}{id}                  ne $anvil->data->{last_scan}{server_uuid}{$server_uuid}{id})                or 
			    ($anvil->data->{this_scan}{server_uuid}{$server_uuid}{'state'}             ne $anvil->data->{last_scan}{server_uuid}{$server_uuid}{'state'})           or 
			    ($anvil->data->{this_scan}{server_uuid}{$server_uuid}{active_definition}   ne $anvil->data->{last_scan}{server_uuid}{$server_uuid}{active_definition}) or 
			    ($anvil->data->{this_scan}{server_uuid}{$server_uuid}{inactive_definition} ne $anvil->data->{last_scan}{server_uuid}{$server_uuid}{inactive_definition}))
			{
				$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, 'print' => 1, key => "log_0845", variables => { 
					name => $server_name,
					uuid => $server_uuid, 
				}});
				
				# Something changed.
				$trigger = 1;
				$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { trigger => $trigger }});
			}
			
			# Done with the old data, delete it. We'll store it again after checking for deleted servers.
			delete $anvil->data->{last_scan}{server_uuid}{$server_uuid};
		}
	}
	
	# Are there any old servers we've not seen?
	if ((exists $anvil->data->{last_scan}{server_uuid}) && (ref($anvil->data->{last_scan}{server_uuid}) eq "HASH"))
	{
		my $count = keys %{$anvil->data->{last_scan}{server_uuid}};
		$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { count => $count }});
		
		if ($count)
		{
			foreach my $server_uuid (sort {$a cmp $b} keys %{$anvil->data->{last_scan}{server_uuid}})
			{
				my $server_name = $anvil->data->{last_scan}{server_uuid}{$server_uuid}{name};
				$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, 'print' => 1, key => "log_0705", variables => { 
					name => $server_name,
					uuid => $server_uuid, 
				}});
				
				$trigger = 1;
				$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { trigger => $trigger }});
			}
		}
	}
	
	# Copy this scan to the last scan.
	delete $anvil->data->{last_scan};
	foreach my $server_uuid (sort {$a cmp $b} keys %{$anvil->data->{this_scan}{server_uuid}})
	{
		$anvil->data->{last_scan}{server_uuid}{$server_uuid}{name}                = $anvil->data->{this_scan}{server_uuid}{$server_uuid}{name};
		$anvil->data->{last_scan}{server_uuid}{$server_uuid}{id}                  = $anvil->data->{this_scan}{server_uuid}{$server_uuid}{id};
		$anvil->data->{last_scan}{server_uuid}{$server_uuid}{'state'}             = $anvil->data->{this_scan}{server_uuid}{$server_uuid}{'state'};
		$anvil->data->{last_scan}{server_uuid}{$server_uuid}{active_definition}   = $anvil->data->{this_scan}{server_uuid}{$server_uuid}{active_definition};
		$anvil->data->{last_scan}{server_uuid}{$server_uuid}{inactive_definition} = $anvil->data->{this_scan}{server_uuid}{$server_uuid}{inactive_definition};
		$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { 
			"s1:last_scan::server_uuid::${server_uuid}::name"  => $anvil->data->{last_scan}{server_uuid}{$server_uuid}{name}, 
			"s2:last_scan::server_uuid::${server_uuid}::id"    => $anvil->data->{last_scan}{server_uuid}{$server_uuid}{id}, 
			"s2:last_scan::server_uuid::${server_uuid}::state" => $anvil->data->{last_scan}{server_uuid}{$server_uuid}{'state'}, 
		}});
		$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { 
			"s1:last_scan::server_uuid::${server_uuid}::active_definition"   => $anvil->data->{last_scan}{server_uuid}{$server_uuid}{active_definition}, 
			"s2:last_scan::server_uuid::${server_uuid}::inactive_definition" => $anvil->data->{last_scan}{server_uuid}{$server_uuid}{inactive_definition}, 
		}});
	}
	
	# Trigger?
	$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { trigger => $trigger }});
	if ($trigger)
	{
		my $shell_call = $anvil->data->{path}{directories}{scan_agents}."/scan-server/scan-server".$anvil->Log->switches;
		$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 2, 'print' => 1, key => "log_0742", variables => { shell_call => $shell_call }});
		
		my ($output, $return_code) = $anvil->System->call({shell_call => $shell_call});
		$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { 
			output      => $output,
			return_code => $return_code, 
		}});
	}
	
	if (time > $next_md5sum_check)
	{
		$next_md5sum_check = time + 30;
		$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { next_md5sum_check => $next_md5sum_check }});
		if ($anvil->Storage->check_md5sums)
		{
			# NOTE: We exit with '0' to prevent systemctl from showing a scary red message.
			$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "alert", key => "message_0014"});
			$anvil->nice_exit({exit_code => 0});
		}
	}
	
	sleep 2;
}

sub connect_to_libvirtd
{
	my ($anvil) = @_;
	
	# Does the handle already exist? We check the local connection to make sure the fingerprint has been recorded.
	$anvil->data->{libvirtd}{connection} = "" if not defined $anvil->data->{libvirtd}{connection};
	if (ref($anvil->data->{libvirtd}{connection}) eq "Sys::Virt")
	{
		# Is this connection alive?
		my $info = $anvil->data->{libvirtd}{connection}->get_node_info();
		if (ref($info) eq "HASH")
		{
			# No need to connect.
			$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 3, key => "log_0814", variables => { target => $anvil->data->{libvirtd}{target} }});
		}
		else
		{
			# Stale connection.
			$anvil->data->{libvirtd}{connection} = "";
		}
	}
	else
	{
		$anvil->data->{libvirtd}{connection} = "";
	}
	
	# Connect if needed.
	if (not $anvil->data->{libvirtd}{connection})
	{
		# Make sure the target is known.
		# Test connect
		my $uri = "qemu+ssh://127.0.0.1/system";
		$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { uri => $uri }});
		eval 
		{
			local $SIG{ALRM} = sub { die "Connection to: [".$uri."] timed out!\n" }; # NB: \n required
			alarm 10;
			$anvil->data->{libvirtd}{connection} = Sys::Virt->new(uri => $uri); 
			alarm 0;
		};
		$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { 
			"libvirtd::connection" => $anvil->data->{libvirtd}{connection},
		}});
		if ($@)
		{
			# Throw an error, then clear the URI so that we just update the DB/on-disk definitions.
			$anvil->data->{libvirtd}{connection} = 0;
			$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, 'print' => 1, key => "warning_0162", variables => { 
				host_name => $anvil->data->{libvirtd}{target},
				uri       => $uri,
				error     => $@,
			}});
			return(1);
		}
	}
	
	return(0);
}
