diff --git a/Anvil/Tools.pm b/Anvil/Tools.pm index 168fccb6..f358106e 100644 --- a/Anvil/Tools.pm +++ b/Anvil/Tools.pm @@ -1106,6 +1106,7 @@ sub _set_paths 'anvil-delete-server' => "/usr/sbin/anvil-delete-server", 'anvil-download-file' => "/usr/sbin/anvil-download-file", 'anvil-file-details' => "/usr/sbin/anvil-file-details", + 'anvil-get-server-screenshot' => "/usr/sbin/anvil-get-server-screenshot", 'anvil-join-anvil' => "/usr/sbin/anvil-join-anvil", 'anvil-maintenance-mode' => "/usr/sbin/anvil-maintenance-mode", 'anvil-manage-firewall' => "/usr/sbin/anvil-manage-firewall", diff --git a/anvil.spec.in b/anvil.spec.in index a7804d62..d1952b3b 100644 --- a/anvil.spec.in +++ b/anvil.spec.in @@ -157,13 +157,14 @@ Requires: libvirt-daemon Requires: libvirt-daemon-driver-qemu Requires: libvirt-daemon-kvm Requires: libvirt-docs +Requires: netpbm-progs Requires: pacemaker Requires: pcs +Requires: python3-websockify Requires: qemu-kvm Requires: qemu-kvm-core Requires: virt-install Requires: virt-top -Requires: python3-websockify # A node is allowed to host servers and be a live migration target. It is not # allowed to host a database or be a DR host. Conflicts: anvil-striker @@ -188,11 +189,12 @@ Requires: libvirt-daemon Requires: libvirt-daemon-driver-qemu Requires: libvirt-daemon-kvm Requires: libvirt-docs +Requires: netpbm-progs +Requires: python3-websockify Requires: qemu-kvm Requires: qemu-kvm-core Requires: virt-install Requires: virt-top -Requires: python3-websockify # A DR host is not allowed to be a live-migration target or host a database. Conflicts: anvil-striker Conflicts: anvil-node diff --git a/cgi-bin/Makefile.am b/cgi-bin/Makefile.am index 5343314e..92fc1a55 100644 --- a/cgi-bin/Makefile.am +++ b/cgi-bin/Makefile.am @@ -7,6 +7,7 @@ dist_cgibin_SCRIPTS = \ get_memory \ get_networks \ get_replicated_storage \ + get_server_screenshot \ get_servers \ get_shared_storage \ get_status \ diff --git a/cgi-bin/get_server_screenshot b/cgi-bin/get_server_screenshot new file mode 100755 index 00000000..184de9ec --- /dev/null +++ b/cgi-bin/get_server_screenshot @@ -0,0 +1,136 @@ +#!/usr/bin/perl +# +# Gets a server VM's screenshot and convert it to a Base64 string. +# + +use strict; +use warnings; +use Anvil::Tools; +use JSON; + +$| = 1; + +my $THIS_FILE = ($0 =~ /^.*\/(.*)$/)[0]; +my $running_directory = ($0 =~ /^(.*?)\/$THIS_FILE$/)[0]; +if (($running_directory =~ /^\./) && ($ENV{PWD})) +{ + $running_directory =~ s/^\./$ENV{PWD}/; +} + +my $anvil = Anvil::Tools->new(); + +sub is_job_incomplete +{ + my $parameters = shift; + my $job_uuid = $parameters->{job_uuid}; + + my $query = " +SELECT + job_progress +FROM + public.jobs +WHERE + job_uuid = ".$anvil->Database->quote($job_uuid)." +;"; + + my $job_progress = $anvil->Database->query({ query => $query, source => $THIS_FILE, line => __LINE__ })->[0]->[0]; + + return $job_progress == 100 ? 0 : 1; +} + +sub get_server_host_uuid +{ + my $parameters = shift; + my $server_uuid = $parameters->{server_uuid}; + + my $query = " +SELECT + server_host_uuid +FROM + public.servers +WHERE + server_uuid = ".$anvil->Database->quote($server_uuid)." +;"; + + return $anvil->Database->query({ query => $query, source => $THIS_FILE, line => __LINE__ })->[0]->[0]; +} + +sub get_screenshot +{ + my $parameters = shift; + my $server_uuid = $parameters->{server_uuid}; + my $server_host_uuid = $parameters->{server_host_uuid}; + my $resize_args = defined $parameters->{resize_args} ? $parameters->{resize_args} : "512x512"; + + my ($job_uuid) = $anvil->Database->insert_or_update_jobs({ + job_command => $anvil->data->{path}{exe}{'anvil-get-server-screenshot'}, + job_data => "server-uuid=".$server_uuid."\nresize=".$resize_args, + job_host_uuid => $server_host_uuid, + job_description => "job_0357", + job_name => "cgi-bin::get_server_screenshot::".$server_uuid, + job_progress => 0, + job_title => "job_0356" + }); + + # Wait until the job is complete before continuing. + while(is_job_incomplete({ job_uuid => $job_uuid })) + { + sleep(2); + } + + my $query = " +SELECT state_note +FROM public.states +WHERE state_name = ".$anvil->Database->quote("server_screenshot::".$server_uuid)." +;"; + + my $encoded_image = $anvil->Database->query({ query => $query, source => $THIS_FILE, line => __LINE__ })->[0]->[0]; + + return $encoded_image; +} + +$anvil->Get->switches; + +$anvil->Database->connect; +$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 2, secure => 0, key => "log_0132"}); +if (not $anvil->data->{sys}{database}{connections}) +{ + # No databases, exit. + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "err", key => "error_0003"}); + $anvil->nice_exit({exit_code => 1}); +} + +my $cookie_problem = $anvil->Account->read_cookies(); + +# Don't do anything data-related if the user is not logged in. +if ($cookie_problem) +{ + $anvil->Log->entry({ source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "err", key => "error_0307" }); + $anvil->nice_exit({ exit_code => 1 }); +} + +# Read in any CGI variables, if needed. +$anvil->Get->cgi(); + +print $anvil->Template->get({ file => "shared.html", name => "json_headers", show_name => 0 })."\n"; + +my $server_uuid = defined $anvil->data->{cgi}{server_uuid}{value} ? $anvil->data->{cgi}{server_uuid}{value} : $anvil->data->{switches}{'server-uuid'}; +my $resize_args = defined $anvil->data->{cgi}{resize}{value} ? $anvil->data->{cgi}{resize}{value} : $anvil->data->{switches}{'resize'}; + +my $response_body = {}; + +if ($server_uuid) +{ + my $encoded_image = get_screenshot({ + server_uuid => $server_uuid, + server_host_uuid => get_server_host_uuid({ server_uuid => $server_uuid }), + resize_args => $resize_args + }); + + if (defined $encoded_image) + { + $response_body->{screenshot} = $encoded_image; + } +} + +print JSON->new->utf8->encode($response_body)."\n"; diff --git a/share/words.xml b/share/words.xml index 5e521616..368f2463 100644 --- a/share/words.xml +++ b/share/words.xml @@ -1148,6 +1148,8 @@ It should be provisioned in the next minute or two. * Please enter the name of the server you want to manage -=] Servers available to manage on the Anvil! [#!variable!anvil_name!#] [=- -=] Managing the server: [#!variable!server_name!#] on the Anvil!: [#!variable!anvil_name!#] + Get Server VM Screenshot + Fetch a screenshot of the specified server VM and represent it as a Base64 string. Starting: [#!variable!program!#]. @@ -2256,6 +2258,10 @@ Are you sure that you want to delete the server: [#!variable!server_name!#]? [Ty Finished [#!variable!operation!#] VNC pipe for server UUID [#!variable!server_uuid!#] from host UUID [#!variable!host_uuid!#]. Finished dropping VNC pipes table. Finished managing VNC pipes; no operations happened because requirements not met. + Preparing to get server VM screenshot. + Finished getting server VM screenshot. + Failed to get server VM screenshot; got non-zero return code. + Finished attempting to get server VM screenshot; no operations happened because requirements not met. Saved the mail server information successfully! diff --git a/tools/Makefile.am b/tools/Makefile.am index f23aa192..d2b6c074 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -14,6 +14,7 @@ dist_sbin_SCRIPTS = \ anvil-delete-server \ anvil-download-file \ anvil-file-details \ + anvil-get-server-screenshot \ anvil-join-anvil \ anvil-maintenance-mode \ anvil-manage-files \ diff --git a/tools/anvil-get-server-screenshot b/tools/anvil-get-server-screenshot new file mode 100755 index 00000000..57579c4e --- /dev/null +++ b/tools/anvil-get-server-screenshot @@ -0,0 +1,186 @@ +#!/usr/bin/perl +# +# +# + +use strict; +use warnings; +use Anvil::Tools; + +$| = 1; + +my $THIS_FILE = ($0 =~ /^.*\/(.*)$/)[0]; +my $running_directory = ($0 =~ /^(.*?)\/$THIS_FILE$/)[0]; +if (($running_directory =~ /^\./) && ($ENV{PWD})) +{ + $running_directory =~ s/^\./$ENV{PWD}/; +} + +my $anvil = Anvil::Tools->new(); + +sub system_call +{ + my $parameters = shift; + my $shell_call = $parameters->{shell_call}; + + my ($shell_output, $shell_return_code) = $anvil->System->call({ shell_call => $shell_call }); + $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => 2, list => { + shell_call => $shell_call, + shell_output => $shell_output, + shell_return_code => $shell_return_code + } }); + + return ($shell_output, $shell_return_code); +} + +sub is_existing_server_screenshot_outdated +{ + my $parameters = shift; + my $server_uuid = $parameters->{server_uuid}; + + my ($encoded_image, $variable_uuid, $variable_mtime) = $anvil->Database->read_variable({ variable_name => "server_screenshot::".$server_uuid }); + + my $time_difference = time - $variable_mtime; + + return $time_difference > 120 ? 1 : 0; +} + +sub get_server_screenshot +{ + my $parameters = shift; + my $server_uuid = $parameters->{server_uuid}; + my ($resize_x, $resize_y) = split(/x/ , $parameters->{resize_args}); + + my $shell_call = "virsh screenshot --domain ".$server_uuid." --file /dev/stdout | sed 's/Screenshot.*//'"; + + if ($resize_x =~ /^\d+$/ && $resize_y =~ /^\d+$/) + { + $shell_call .= " | pamscale -quiet -xyfit ".$resize_x." ".$resize_y; + $shell_call .= " | pamtopng -quiet"; + } + + $shell_call .= " | base64 --wrap 0"; + + my ($shell_output, $shell_return_code) = system_call({ shell_call => $shell_call }); + + return $shell_return_code == 0 ? $shell_output : undef; +} + +sub insert_server_screenshot +{ + my $parameters = shift; + my $server_uuid = $parameters->{server_uuid}; + my $encoded_image = $parameters->{encoded_image}; + + $anvil->Database->insert_or_update_states({ + state_name => "server_screenshot::".$server_uuid, + state_note => $encoded_image + }); +} + +$anvil->Get->switches; + +$anvil->Database->connect; +$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 2, secure => 0, key => "log_0132"}); +if (not $anvil->data->{sys}{database}{connections}) +{ + # No databases, exit. + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "err", key => "error_0003"}); + $anvil->nice_exit({exit_code => 1}); +} + +# Try to get a job UUID if not given. +if (not $anvil->data->{switches}{'job-uuid'}) +{ + $anvil->data->{switches}{'job-uuid'} = $anvil->Job->get_job_uuid({ program => $THIS_FILE }); + $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => 2, list => { + "switches::job-uuid" => $anvil->data->{switches}{'job-uuid'} + } }); +} + +# Handle this script as a job when job UUID is provided. +if ($anvil->data->{switches}{'job-uuid'}) +{ + $anvil->Job->clear(); + $anvil->Job->get_job_details(); + $anvil->Job->update_progress({ + progress => 1, + job_picked_up_by => $$, + job_picked_up_at => time, + message => "message_0263" + }); + + foreach my $line (split/\n/, $anvil->data->{jobs}{job_data}) + { + if ($line =~ /server-uuid=(.*?)$/) + { + $anvil->data->{switches}{'server-uuid'} = $1; + } + + if ($line =~ /resize=(.*?)$/) + { + $anvil->data->{switches}{'resize'} = $1; + } + + if ($line =~ /stdout=(.*?)$/) + { + $anvil->data->{switches}{'stdout'} = $1; + } + } +} + +my $server_uuid = $anvil->data->{switches}{'server-uuid'}; +my $resize_args = $anvil->data->{switches}{'resize'}; +my $is_stdout = $anvil->data->{switches}{'stdout'}; +my $job_uuid = $anvil->data->{switches}{'job-uuid'}; + +$anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => 2, list => { + server_uuid => $server_uuid, + resize_args => $resize_args, + is_stdout => $is_stdout, + job_uuid => $job_uuid +} }); + +if ($server_uuid) +{ + my $encoded_image; + + if ($is_stdout) + { + $encoded_image = get_server_screenshot({ server_uuid => $server_uuid, resize_args => $resize_args }); + + if (defined $encoded_image) + { + print($encoded_image); + + $anvil->Job->update_progress({ progress => 100, message => "message_0264" }); + } + else + { + $anvil->Job->update_progress({ progress => 100, message => "message_0265" }); + } + } + elsif (is_existing_server_screenshot_outdated({ server_uuid => $server_uuid })) + { + $encoded_image = get_server_screenshot({ server_uuid => $server_uuid, resize_args => $resize_args }); + + if (defined $encoded_image) + { + insert_server_screenshot({ server_uuid => $server_uuid, encoded_image => $encoded_image }); + + $anvil->Job->update_progress({ progress => 100, message => "message_0264" }); + } + else + { + $anvil->Job->update_progress({ progress => 100, message => "message_0265" }); + } + } + else + { + $anvil->Job->update_progress({ progress => 100, message => "message_0266" }); + } +} +else +{ + $anvil->Job->update_progress({ progress => 100, message => "message_0266" }); +}