#!/usr/bin/perl # # Manages VNC ports for server VMs that have VNC enabled. # 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(); my $manage_tunnel = $anvil->data->{path}{exe}{'anvil-manage-tunnel'}; my $echo = $anvil->data->{path}{exe}{'echo'}; my $grep = $anvil->data->{path}{exe}{'grep'}; my $kill = $anvil->data->{path}{exe}{'kill'}; my $pgrep = $anvil->data->{path}{exe}{'pgrep'}; my $ps = $anvil->data->{path}{exe}{'ps'}; my $ss = $anvil->data->{path}{exe}{'ss'}; my $sed = $anvil->data->{path}{exe}{'sed'}; my $websockify = $anvil->data->{path}{exe}{'websockify'}; sub drop_vnc_pipes_table { my $query = "DROP TABLE IF EXISTS public.vnc_pipes;"; $anvil->Database->write({ query => $query, source => $THIS_FILE, line => __LINE__ }); } $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 $switch_debug = $anvil->data->{switches}{'debug'}; my $open = $anvil->data->{switches}{'open'}; my $server = $anvil->data->{switches}{'server'}; my $server_uuid = $anvil->data->{switches}{'server-uuid'}; my $server_vnc_port = $anvil->data->{switches}{'server-vnc-port'}; if (defined $server) { $server_uuid //= is_uuid_v4($server) ? $server : $anvil->Get->server_uuid_from_name({ server_name => $server }); } $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $switch_debug, list => { open => $open, server => $server, server_uuid => $server_uuid, server_vnc_port => $server_vnc_port } }); my $map_to_operation = { start => \&start_pipe, stop => \&stop_pipe }; if ($server_uuid) { my $rcode; my $operation = $open ? "start" : "stop"; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $switch_debug, list => { operation => $operation } }); ($rcode) = $map_to_operation->{$operation}({ debug => $switch_debug, svr_uuid => $server_uuid, svr_vnc_port => $server_vnc_port, }); $anvil->nice_exit({ exit_code => $rcode }); } $anvil->nice_exit({ exit_code => 0 }); # # Functions # sub build_find_available_port_call { my $parameters = shift; my $debug = $parameters->{debug} || 3; my $start = $parameters->{start}; my $step_operator = $parameters->{step_operator} // "+"; my $step_size = $parameters->{step_size} || 1; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "build_find_available_port_call" }); return (1) if ( (not $step_operator =~ /^[+-]$/) || (not is_int($step_size)) || ($step_size < 1) ); my $call = "ss_output=\$($ss -ant) && port=${start} && while $grep -Eq \":\${port}[[:space:]]+[^[:space:]]+\" <<<\$ss_output; do (( port ${step_operator}= $step_size )); done && $echo \$port"; return (0, $call); } sub build_tunnel_call { my $parameters = shift; my $ctl_cmd = $parameters->{ctl_cmd} // "forward"; my $ctl_path = $parameters->{ctl_path}; my $debug = $parameters->{debug} || 3; my $lport = $parameters->{lport}; my $rport = $parameters->{rport}; my $svr_uuid = $parameters->{svr_uuid}; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "build_tunnel_call" }); return (1) if ( (not defined $ctl_path) || (not defined $lport) || (not defined $rport) ); my $ls_prefix_opt = defined $svr_uuid ? "--tunnel-ls-prefix '$svr_uuid'" : ""; my $call = "$manage_tunnel --child --ctl-cmd $ctl_cmd --ctl-path '$ctl_path' --debug $debug --forward-lport $lport --forward-rport $rport $ls_prefix_opt"; return (0, $call); } sub build_tunnel_variable_name { my ($svr_uuid, $tp_target_uuid) = @_; return "sshctl::${svr_uuid}::${tp_target_uuid}"; } sub call { my $parameters = shift; my $call = $parameters->{call}; my $debug = $parameters->{debug} || 3; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "call" }); return (1) if ( (not defined $call) || ($call eq "") ); my ($output, $rcode) = $anvil->System->call({ shell_call => $call }); $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => { output => $output, rcode => $rcode } }); return ($rcode, $output); } sub find_available_port { my $parameters = shift; my $debug = $parameters->{debug} || 3; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "find_available_port" }); my ($build_rcode, $call) = build_find_available_port_call($parameters); return (1) if ($build_rcode); return call({ call => $call, debug => $debug }); } sub find_ws_processes { my $parameters = shift; my $debug = $parameters->{debug} || 3; my $ps_name = $parameters->{ps_name} // "websockify"; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "find_ws_processes" }); my $ps_call = "$pgrep -a '$ps_name' | $sed -E 's/^([[:digit:]]+).*${ps_name}[[:space:]:]+([[:digit:]]+)[[:space:]:]+([[:digit:]]+).*\$/\\1,\\2,\\3/'"; my ($rcode, $output) = call({ call => $ps_call, debug => $debug }); return (1) if ($rcode); my $result = { pids => {}, sources => {}, targets => {} }; foreach my $line (split(/\n/, $output)) { chomp($line); $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => { ws_line => $line } }); my ($pid, $sport, $tport) = split(/,/, $line); my $process = { pid => $pid, sport => $sport, tport => $tport }; set_ws_process({ debug => $debug, process => $process, processes => $result }); } return (0, $result); } sub find_tp_processes { my $parameters = shift; my $debug = $parameters->{debug} || 3; my $ps_name = $parameters->{ps_name} // "anvil-manage-tunnel"; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "find_tp_processes" }); my $ps_call = "$pgrep -a '$ps_name' | $sed - E 's/^([[:digit:]]+).*--target[[:space:]]+([^[:space:]]+).*--ctl-path[[:space:]]+([^[:space:]]+).*--tunnel-ls-path[[:space:]]+([^[:space:]]+).*\$/\\1,\\2,\\3,\\4/'"; my ($rcode, $output) = call({ call => $ps_call, debug => $debug }); return (1) if ($rcode); my $result = { pids => {}, targets => {} }; foreach my $line (split(/\n/, $output)) { chomp($line); $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => { tp_line => $line } }); my ($pid, $target, $ctl_path, $tunnel_ls_path) = split(/,/, $line); my $process = { ctl_path => $ctl_path, pid => $pid, target => $target, tunnel_ls_path => $tunnel_ls_path }; set_tp_process({ debug => $debug, process => $process, processes => $result }); } return (0, $result); } sub find_tunnel { my $parameters = shift; my $debug = $parameters->{debug} || 3; my $svr_uuid = $parameters->{svr_uuid}; my $tunnel_ls_path = $parameters->{tunnel_ls_path}; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "find_tunnel" }); return (1) if ( (not defined $svr_uuid) || (not defined $tunnel_ls_path) ); my $call = "$sed -En 's/^${svr_uuid}.*:([[:digit:]]+):.*:([[:digit:]]+)\$/\\1,\\2/p' '$tunnel_ls_path'"; my ($rcode, $output) = call({ call => $call, debug => $debug }); return (1) if ($rcode); chomp($output); my ($lport, $rport) = split(/,/, $output); return (0, { lport => $lport, rport => $rport }); } sub get_strikers { my $query = " SELECT host_name, host_uuid FROM hosts WHERE host_status = 'online' AND host_type = 'striker' ;"; my $rows = $anvil->Database->query({ query => $query, source => $THIS_FILE, line => __LINE__ }); my $strikers = { names => {}, uuids => {} }; foreach my $row (@{$rows}) { my $host_name = $row->[0]; my $host_uuid = $row->[1]; $strikers->{uuids}{$host_uuid} = { name => $host_name, uuid => $host_uuid }; $strikers->{names}{$host_name} = $host_uuid; } return (0, $strikers); } sub get_tunnel_variable { my $parameters = shift; my $svr_uuid = $parameters->{svr_uuid}; my $tp_target_uuid = $parameters->{tp_target_uuid}; my $variable_name = build_tunnel_variable_name($svr_uuid, $tp_target_uuid); my $query = " SELECT variable_value FROM variables WHERE variable_name = ".$anvil->Database->quote($variable_name)." AND variable_source_table = 'hosts' AND variable_source_uuid = ".$anvil->Database->quote($tp_target_uuid)." ;"; my $rows = $anvil->Database->query({ query => $query, source => $THIS_FILE, line => __LINE__ }); return (1) if (not @{$rows}); my $end_port = $rows->[0]->[0]; return (0, int($end_port)); } sub is_int { return $_[0] =~ /^\d+$/; } sub is_uuid_v4 { return $_[0] =~ /[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}/; } sub set_process { my $parameters = shift; my $debug = $parameters->{debug} || 3; my $handle_delete = $parameters->{handle_delete}; my $handle_set = $parameters->{handle_set}; my $pid = $parameters->{pid}; my $process = $parameters->{process}; my $processes = $parameters->{processes}; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $process, prefix => "process" }); return (1) if (not defined $processes); if (defined $process) { $pid = $process->{pid}; $handle_set->($pid, $process, $processes); } elsif (defined $pid) { $process = $processes->{pids}{$pid}; $handle_delete->($pid, $process, $processes); } return (0); } sub set_tp_process { my $parameters = shift; $parameters->{handle_delete} = sub { my ($pid, $process, $processes) = @_; my $target = $process->{target}; delete $processes->{pids}{$pid}; delete $processes->{targets}{$target}; }; $parameters->{handle_set} = sub { my ($pid, $process, $processes) = @_; my $target = $process->{target}; $processes->{pids}{$pid} = $process; $processes->{targets}{$target} = $pid; }; return set_process($parameters); } sub set_tunnel_variable { my $parameters = shift; my $debug = $parameters->{debug} || 3; my $end_port = $parameters->{end_port}; my $svr_uuid = $parameters->{svr_uuid}; my $tp_target_uuid = $parameters->{tp_target_uuid}; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "set_tunnel_variable" }); my ($variable_uuid) = $anvil->Database->insert_or_update_variables({ file => $THIS_FILE, line => __LINE__, variable_name => build_tunnel_variable_name($svr_uuid, $tp_target_uuid), variable_source_table => "hosts", variable_source_uuid => $tp_target_uuid, variable_value => $end_port, }); return (1) if (not is_uuid_v4($variable_uuid)); return (0); } sub set_ws_process { my $parameters = shift; $parameters->{handle_delete} = sub { my ($pid, $process, $processes) = @_; my $sport = $process->{sport}; my $tport = $process->{tport}; delete $processes->{pids}{$pid}; delete $processes->{sources}{$sport}; delete $processes->{targets}{$tport}; }; $parameters->{handle_set} = sub { my ($pid, $process, $processes) = @_; my $sport = $process->{sport}; my $tport = $process->{tport}; $processes->{pids}{$pid} = $process; $processes->{sources}{$sport} = $pid; $processes->{targets}{$tport} = $pid; }; return set_process($parameters); } sub start_pipe { my $parameters = shift; my $debug = $parameters->{debug} || 3; my $svr_uuid = $parameters->{svr_uuid}; my $svr_vnc_port = $parameters->{svr_vnc_port}; return (1) if (not is_uuid_v4($svr_uuid)); my $common_params = { debug => $debug }; my $rcode; # If we don't have the server's VNC port, find it in its qemu-kvm process. if ( (not defined $svr_vnc_port) || (not is_int($svr_vnc_port)) ) { ($rcode, my $svr_processes) = $anvil->Server->find_processes($common_params); my $svr_process = $svr_processes->{uuids}{$svr_uuid}; my $svr_vnc_alive = $svr_process->{vnc_alive}; return (1) if (not $svr_vnc_alive); $svr_vnc_port = $svr_process->{vnc_port}; } ($rcode, my $ws_processes) = find_ws_processes($common_params); return ($rcode) if ($rcode); ($rcode, my $ws_pid) = start_ws({ svr_vnc_port => $svr_vnc_port, ws_processes => $ws_processes, %$common_params }); return ($rcode) if ($rcode); ($rcode, my $hosts) = get_strikers($common_params); return ($rcode) if ($rcode); ($rcode, my $tp_processes) = find_tp_processes($common_params); return ($rcode) if ($rcode); my $ws_process = $ws_processes->{pids}{$ws_pid}; foreach my $host_uuid (keys %{$hosts->{uuids}}) { my $host_name = $hosts->{uuids}{$host_uuid}{name}; ($rcode, my $tp_pid) = start_tp({ tp_processes => $tp_processes, tp_target => $host_name, %$common_params }); next if ($rcode); my $tp_process = $tp_processes->{pids}{$tp_pid}; ($rcode, my $tunnel) = start_tunnel({ svr_uuid => $svr_uuid, tp_ctl_path => $tp_process->{ctl_path}, tp_target => $host_name, tp_target_uuid => $host_uuid, ws_sport => $ws_process->{sport}, %$common_params, }); next if ($rcode); ($rcode) = set_tunnel_variable($tunnel); if ($rcode) { stop_tunnel({ lport => $ws_process->{sport}, rport => $tunnel->{end_port}, tp_ctl_path => $tp_process->{ctl_path}, %$common_params, }); } } return (0); } sub start_tp { my $parameters = shift; my $debug = $parameters->{debug} || 3; my $tp_ctl_path = $parameters->{tp_ctl_path}; my $tp_processes = $parameters->{tp_processes}; my $tp_target = $parameters->{tp_target}; my $tp_tunnel_ls_path = $parameters->{tp_tunnel_ls_path}; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "start_tp" }); return (1) if ( (not defined $tp_processes) || (not defined $tp_target) ); my $tp_ctl_name = "sshctl-${tp_target}"; $tp_ctl_path //= "~/.libnet-openssh-perl/${tp_ctl_name}"; $tp_tunnel_ls_path //= $anvil->data->{path}{'directories'}{'tmp'}."/${tp_ctl_name}-tunnel-ls"; my $existing_tp_pid = $tp_processes->{targets}{$tp_target}; return (0, $existing_tp_pid) if (defined $existing_tp_pid); my $tp_call = "$manage_tunnel --debug $debug --target $tp_target --ctl-path '$tp_ctl_path' --tunnel-ls-path '$tp_tunnel_ls_path' & $echo pid:\$!"; my ($start_rcode, $start_output) = call({ call => $tp_call, debug => $debug }); return (1) if ($start_rcode); my ($tp_pid) = $start_output =~ /pid:(\d+)$/; my $tp_process = { ctl_path => $tp_ctl_path, pid => $tp_pid, target => $tp_target, tunnel_ls_path => $tp_tunnel_ls_path }; set_tp_process({ debug => $debug, process => $tp_process, processes => $tp_processes }); return (0, $tp_pid); } sub start_tunnel { my $parameters = shift; my $debug = $parameters->{debug}; my $svr_uuid = $parameters->{svr_uuid}; my $tp_ctl_path = $parameters->{tp_ctl_path}; my $tp_target = $parameters->{tp_target}; my $tp_target_uuid = $parameters->{tp_target_uuid}; my $ws_sport = $parameters->{ws_sport}; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "start_tunnel" }); return (1) if ( (not defined $svr_uuid) || (not defined $tp_target) || (not defined $tp_target_uuid) || (not defined $tp_ctl_path) || ($tp_ctl_path eq "") || (not -e $tp_ctl_path) || (not defined $ws_sport) ); my $build_rcode; my $sh_call; # ----- Try to find a usable port on the target host. ($build_rcode, $sh_call) = build_find_available_port_call({ start => $ws_sport }); return (1) if ($build_rcode); $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => { find_available_port_call => $sh_call } }); my ($tunnel_rport, $find_error, $find_rcode) = $anvil->Remote->call({ no_cache => 1, ossh_opts => [ ctl_path => $tp_ctl_path, external_master => 1 ], shell_call => $sh_call, target => "0.0.0.0", }); $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => { find_error => $find_error, find_rcode => $find_rcode, tunnel_rport => $tunnel_rport, } }); return (1) if ($find_rcode); # ----- ($build_rcode, $sh_call) = build_tunnel_call({ ctl_path => $tp_ctl_path, debug => $debug, lport => $ws_sport, rport => $tunnel_rport, svr_uuid => $svr_uuid, }); return (1) if ($build_rcode); my ($start_rcode) = call({ call => $sh_call, debug => $debug }); return (1) if ($start_rcode); return (0, { debug => $debug, end_port => $tunnel_rport, svr_uuid => $svr_uuid, tp_target_uuid => $tp_target_uuid }); } sub start_ws { my $parameters = shift; my $debug = $parameters->{debug} || 3; my $svr_vnc_port = $parameters->{svr_vnc_port}; my $ws_processes = $parameters->{ws_processes}; my $ws_sport_offset = $parameters->{ws_sport_offset} || 10000; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "start_ws" }); return (1) if ( (not defined $ws_processes) || (not defined $svr_vnc_port) || (not is_int($svr_vnc_port)) || (not is_int($ws_sport_offset)) ); my $existing_ws_pid = $ws_processes->{targets}{$svr_vnc_port}; return (0, $existing_ws_pid) if (defined $existing_ws_pid); my ($find_rcode, $ws_sport) = find_available_port({ debug => $debug, start => int($svr_vnc_port) + int($ws_sport_offset) }); return (1) if ($find_rcode); my $ws_call = "$websockify $ws_sport :$svr_vnc_port &>/dev/null & $echo pid:\$!"; my ($start_rcode, $start_output) = call({ call => $ws_call, debug => $debug }); return (1) if ($start_rcode); my ($ws_pid) = $start_output =~ /pid:(\d+)$/; my $ws_process = { pid => $ws_pid, sport => $ws_sport, tport => $svr_vnc_port }; set_ws_process({ debug => $debug, process => $ws_process, processes => $ws_processes }); return (0, $ws_pid); } sub stop_pipe { my $parameters = shift; my $debug = $parameters->{debug} || 3; my $svr_uuid = $parameters->{svr_uuid}; my $svr_vnc_port = $parameters->{svr_vnc_port}; return (1) if (not is_uuid_v4($svr_uuid)); my $common_params = { debug => $debug }; my $rcode; # If we don't have the server's VNC port, find it in its qemu-kvm process. if ( (not defined $svr_vnc_port) || (not is_int($svr_vnc_port)) ) { ($rcode, my $svr_processes) = $anvil->Server->find_processes($common_params); my $svr_process = $svr_processes->{uuids}{$svr_uuid}; my $svr_vnc_alive = $svr_process->{vnc_alive}; return (1) if (not $svr_vnc_alive); $svr_vnc_port = $svr_process->{vnc_port}; } ($rcode, my $ws_processes) = find_ws_processes($common_params); return ($rcode) if ($rcode); my $ws_pid = $ws_processes->{targets}{$svr_vnc_port}; stop_ws({ ws_pid => $ws_pid, ws_processes => $ws_processes }); ($rcode, my $hosts) = get_strikers($common_params); return ($rcode) if ($rcode); ($rcode, my $tp_processes) = find_tp_processes($common_params); return ($rcode) if ($rcode); foreach my $host_uuid (keys %{$hosts->{uuids}}) { my $host_name = $hosts->{uuids}{$host_uuid}{name}; my $tp_pid = $tp_processes->{targets}{$host_name}; my $tp_process = $tp_processes->{pids}{$tp_pid}; ($rcode, my $tunnel_ports) = find_tunnel({ svr_uuid => $svr_uuid, tunnel_ls_path => $tp_process->{tunnel_ls_path}, %$common_params, }); next if ($rcode); stop_tunnel({ %$tunnel_ports, tp_ctl_path => $tp_process->{ctl_path}, %$common_params }); } return (0); } sub stop_tp { my $parameters = shift; my $debug = $parameters->{debug} || 3; my $tp_pid = $parameters->{tp_pid}; my $tp_processes = $parameters->{tp_processes}; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "stop_tp" }); call({ debug => $debug, call => "$kill $tp_pid || $kill -9 $tp_pid" }); set_tp_process({ debug => $debug, pid => $tp_pid, processes => $tp_processes }); } sub stop_tunnel { my $parameters = shift; my $debug = $parameters->{debug} || 3; my $lport = $parameters->{lport}; my $rport = $parameters->{rport}; my $tp_ctl_path = $parameters->{tp_ctl_path}; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "stop_tunnel" }); my ($build_rcode, $call) = build_tunnel_call({ ctl_cmd => "cancel", ctl_path => $tp_ctl_path, debug => $debug, lport => $lport, rport => $rport, }); return (1) if ($build_rcode); my ($stop_rcode) = call({ call => $call, debug => $debug }); return (1) if ($stop_rcode); return (0); } sub stop_ws { my $parameters = shift; my $debug = $parameters->{debug} || 3; my $ws_pid = $parameters->{ws_pid}; my $ws_processes = $parameters->{ws_processes}; $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "stop_ws" }); call({ debug => $debug, call => "$kill $ws_pid || $kill -9 $ws_pid" }); set_ws_process({ debug => $debug, pid => $ws_pid, processes => $ws_processes }); }