More.pm 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026
  1. package Net::OpenSSH::More;
  2. #ABSTRACT: Net::OpenSSH submodule with many useful features
  3. use strict;
  4. use warnings;
  5. use parent 'Net::OpenSSH';
  6. use Data::UUID ();
  7. use Expect ();
  8. use File::HomeDir ();
  9. use File::Temp ();
  10. use Fcntl ();
  11. use IO::Pty ();
  12. use IO::Socket::INET ();
  13. use IO::Socket::INET6 ();
  14. use IO::Stty ();
  15. use List::Util qw{first};
  16. use Net::DNS::Resolver ();
  17. use Net::IP ();
  18. use Time::HiRes ();
  19. use Term::ANSIColor ();
  20. =head1 NAME
  21. Net::OpenSSH::More
  22. =head1 DESCRIPTION
  23. Submodule of Net::OpenSSH that contains many methods that were
  24. otherwise left "as an exercise to the reader" in the parent module.
  25. Highlights:
  26. =over 4
  27. =item
  28. Persistent terminal via expect for very fast execution, less forking.
  29. =item
  30. Usage of File::Temp and auto-cleanup to prevent lingering ctl_path cruft.
  31. =item
  32. Ability to manipulate incoming text while streaming the output of commands.
  33. =item
  34. Run perl subroutine refs you write locally but execute remotely.
  35. =item
  36. Many shortcut methods for common system administration tasks
  37. =item
  38. Registration method for commands to run upon DESTROY/before disconnect.
  39. =item
  40. Automatic reconnection ability upon connection loss
  41. =back
  42. =head1 SYNOPSIS
  43. use Net::OpenSSH::More;
  44. my $ssh = Net::OpenSSH::More->new(
  45. 'host' => 'some.host.test',
  46. 'port' => 69420,
  47. 'user' => 'azurediamond',
  48. 'password' => 'hunter2',
  49. );
  50. ...
  51. =head1 SEE ALSO
  52. Net::OpenSSH
  53. Net::OpenSSH::More::Linux
  54. =cut
  55. my %defaults = (
  56. 'user' => $ENV{'USER'} || getpwuid($>),
  57. 'port' => 22,
  58. 'use_persistent_shell' => 0,
  59. 'output_prefix' => '',
  60. 'home' => File::HomeDir->my_home,
  61. 'retry_interval' => 6,
  62. 'retry_max' => 10,
  63. );
  64. our %cache;
  65. our $disable_destructor = 0;
  66. ###################
  67. # PRIVATE METHODS #
  68. ###################
  69. my $die_no_trace = sub {
  70. my ( $full_msg, $summary ) = @_;
  71. $summary ||= 'FATAL';
  72. my $carp = $INC{'Carp/Always.pm'} ? '' : ' - Use Carp::Always for full trace.';
  73. die "[$summary] ${full_msg}${carp}";
  74. };
  75. my $check_local_perms = sub {
  76. my ( $path, $expected_mode, $is_dir ) = @_;
  77. $is_dir //= 0;
  78. my @stat = stat($path);
  79. $die_no_trace->(qq{"$path" must be a directory that exists}) unless !$is_dir ^ -d _;
  80. $die_no_trace->(qq{"$path" must be a file that exists}) unless $is_dir ^ -f _;
  81. $die_no_trace->(qq{"$path" could not be read}) unless -r _;
  82. my $actual_mode = $stat[2] & 07777;
  83. $die_no_trace->( sprintf( qq{Permissions on "$path" are not correct: got=0%o, expected=0%o}, $actual_mode, $expected_mode ) ) unless $expected_mode eq $actual_mode;
  84. return 1;
  85. };
  86. my $resolve_login_method = sub {
  87. my ($opts) = @_;
  88. my $chosen = first { $opts->{$_} } qw{key_path password};
  89. $chosen //= '';
  90. undef $chosen if $chosen eq 'key_path' && !$check_local_perms->( $opts->{'key_path'}, 0600 );
  91. return $chosen if $chosen;
  92. return 'SSH_AUTH_SOCK' if $ENV{'SSH_AUTH_SOCK'};
  93. my $fallback_path = "$opts->{'home'}/.ssh/id";
  94. my $key_type = first { -s "${fallback_path}_$_" } qw{ed25519 ecdsa rsa dsa};
  95. $opts->{'key_path'} = "${fallback_path}_${key_type}" if defined $key_type;
  96. $die_no_trace->('No key_path or password specified and no active SSH agent; cannot connect') if !$opts->{'key_path'};
  97. $check_local_perms->( $opts->{'key_path'}, 0600 ) if $opts->{'key_path'};
  98. return $opts->{'key_path'};
  99. };
  100. my $get_dns_record_from_hostname = sub {
  101. my ( $hostname, $record_type ) = @_;
  102. $record_type ||= 'A';
  103. my $reply = Net::DNS::Resolver->new()->search( $hostname, $record_type );
  104. return unless $reply;
  105. return { map { $_->type() => $_->address() } grep { $_->type eq $record_type } ( $reply->answer() ) };
  106. };
  107. # Knock on the server till it responds, or doesn't. Try both ipv4 and ipv6.
  108. my $ping = sub {
  109. my ($opts) = @_;
  110. my $timeout = 30;
  111. my ( $host_info, $ip, $r_type );
  112. if ( my $ip_obj = Net::IP->new( $opts->{'host'} ) ) {
  113. $r_type = $ip_obj->ip_is_ipv4 ? 'A' : 'AAAA';
  114. $ip = $opts->{'host'};
  115. }
  116. else {
  117. my $host_info = first { $get_dns_record_from_hostname->( $opts->{'host'}, $_ ) } qw{A AAAA};
  118. ($r_type) = keys(%$host_info);
  119. if ( !$host_info->{$r_type} ) {
  120. require Data::Dumper;
  121. die "Can't determine IP type. " . Data::Dumper::Dumper($host_info);
  122. }
  123. $ip = $host_info->{$r_type};
  124. }
  125. my %family_map = ( 'A' => 'INET', 'AAAA' => 'INET6' );
  126. my $start = time;
  127. while ( ( time - $start ) <= $timeout ) {
  128. return 1 if "IO::Socket::$family_map{$r_type}"->new(
  129. 'PeerAddr' => $ip,
  130. 'PeerPort' => $opts->{'port'},
  131. 'Proto' => 'tcp',
  132. 'Timeout' => $timeout,
  133. );
  134. diag( { '_opts' => $opts }, "[DEBUG] Waiting for response on $ip:$opts->{'port'} ($r_type)..." ) if $opts->{'debug'};
  135. select undef, undef, undef, 0.5; # there's no need to try more than 2 times per second
  136. }
  137. return 0;
  138. };
  139. my $init_ssh = sub {
  140. my ( $class, $opts ) = @_;
  141. # Always clear the cache if possible when we get here.
  142. if ( $opts->{'_cache_index'} ) {
  143. local $disable_destructor = 1;
  144. undef $cache{ $opts->{'_cache_index'} };
  145. }
  146. # Try not to have disallowed ENV chars. For now just transliterate . into _
  147. # XXX TODO This will be bad with some usernames/domains.
  148. # Maybe need to run host through punycode decoder, etc.?
  149. if ( !$opts->{'_host_sock_key'} ) {
  150. $opts->{'_host_sock_key'} = "NET_OPENSSH_MASTER_$opts->{'host'}_$opts->{'user'}";
  151. $opts->{'_host_sock_key'} =~ tr/./_/;
  152. }
  153. # Make temp dir go out of scope with this object for ctl paths, etc.
  154. # Leave no trace!
  155. $opts->{'_tmp_obj'} = File::Temp->newdir() if !$opts->{'_tmp_obj'};
  156. my $tmp_dir = $opts->{'_tmp_obj'}->dirname();
  157. diag( { '_opts' => $opts }, "Temp dir: $tmp_dir" ) if $opts->{'debug'};
  158. my $temp_fh;
  159. # Use an existing connection if possible, otherwise make one
  160. if ( $ENV{ $opts->{'_host_sock_key'} } && -e $ENV{ $opts->{'_host_sock_key'} } ) {
  161. $opts->{'external_master'} = 1;
  162. $opts->{'ctl_path'} = $ENV{ $opts->{'_host_sock_key'} };
  163. }
  164. else {
  165. if ( !$opts->{'debug'} ) {
  166. open( $temp_fh, ">", "$tmp_dir/STDERR" ) or $die_no_trace->("Can't open $tmp_dir/STDERR for writing: $!");
  167. $opts->{'master_stderr_fh'} = $temp_fh;
  168. }
  169. $opts->{'ctl_dir'} = $tmp_dir;
  170. $opts->{'strict_mode'} = 0;
  171. $opts->{'master_opts'} = [
  172. '-o' => 'StrictHostKeyChecking=no',
  173. '-o' => 'GSSAPIAuthentication=no',
  174. '-o' => 'UserKnownHostsFile=/dev/null',
  175. '-o' => 'ConnectTimeout=180',
  176. '-o' => 'TCPKeepAlive=no',
  177. ];
  178. push @{ $opts->{'master_opts'} }, '-v' if $opts->{'debug'};
  179. if ( $opts->{'key_path'} ) {
  180. push @{ $opts->{'master_opts'} }, '-o', 'IdentityAgent=none';
  181. }
  182. # Attempt to use the SSH agent if possible. This won't hurt if you use -k or -P.
  183. # Even if your sock doesn't work to get you in, you may want it to do something on the remote host.
  184. # Of course, you may want to disable this with no_agent if your system is stupidly configured
  185. # with lockout after 3 tries and you have 4 keys in agent.
  186. # Anyways, don't just kill the sock for your bash session, restore it in DESTROY
  187. $opts->{'_restore_auth_sock'} = delete $ENV{SSH_AUTH_SOCK} if $opts->{'no_agent'};
  188. $opts->{'forward_agent'} = 1 if $ENV{'SSH_AUTH_SOCK'};
  189. }
  190. my $status = 0;
  191. my $self;
  192. foreach my $attempt ( 1 .. $opts->{'retry_max'} ) {
  193. local $@;
  194. my $up = $ping->($opts);
  195. if ( !$up ) {
  196. $die_no_trace->("$opts->{'host'} is down!") if $opts->{die_on_drop};
  197. diag( { '_opts' => $opts }, "Waiting for host to bring up sshd, attempt $attempt..." );
  198. next;
  199. }
  200. # Now, per the POD of Net::OpenSSH, new will NEVER DIE, so just trust it.
  201. my @base_module_opts =
  202. qw{host user port password passphrase key_path gateway proxy_command batch_mode ctl_dir ctl_path ssh_cmd scp_cmd rsync_cmd remote_shell timeout kill_ssh_on_timeout strict_mode async connect master_opts default_ssh_opts forward_agent forward_X11 default_stdin_fh default_stdout_fh default_stderr_fh default_stdin_file default_stdout_file default_stderr_file master_stdout_fh master_sdterr_fh master_stdout_discard master_stderr_discard expand_vars vars external_master default_encoding default_stream_encoding default_argument_encoding password_prompt login_handler master_setpgrp master_pty_force};
  203. my $class4super = "Net::OpenSSH::More";
  204. # Subclassing here is a bit tricky, especially *after* you have gone down more than one layer.
  205. # Ultimately we only ever want the constructor for Net::OpenSSH, so start there and then
  206. # Re-bless into subclass if that's relevant.
  207. $self = $class4super->SUPER::new( map { $_ => $opts->{$_} } grep { $opts->{$_} } @base_module_opts );
  208. my $error = $self->error;
  209. next unless ref $self eq 'Net::OpenSSH::More' && !$error;
  210. bless $self, $class if ref $self ne $class;
  211. if ( $temp_fh && -s $temp_fh ) {
  212. seek( $temp_fh, 0, Fcntl::SEEK_SET );
  213. local $/;
  214. $error .= " " . readline($temp_fh);
  215. }
  216. if ($error) {
  217. $die_no_trace->("Bad password passed, will not retry SSH connection: $error.") if ( $error =~ m{bad password} && $opts->{'password'} );
  218. $die_no_trace->("Bad key, will not retry SSH connection: $error.") if ( $error =~ m{master process exited unexpectedly} && $opts->{'key_path'} );
  219. $die_no_trace->("Bad credentials, will not retry SSH connection: $error.") if ( $error =~ m{Permission denied} );
  220. }
  221. if ( defined $self->error && $self->error ne "0" && $attempt == 1 ) {
  222. $self->diag( "SSH Connection could not be established to " . $self->{'host'} . " with the error:", $error, 'Will Retry 10 times.' );
  223. }
  224. if ( $status = $self->check_master() ) {
  225. $self->diag( "Successfully established connection to " . $self->{'host'} . " on attempt #$attempt." ) if $attempt gt 1;
  226. last;
  227. }
  228. sleep $opts->{'retry_interval'};
  229. }
  230. $die_no_trace->("Failed to establish SSH connection after $opts->{'retry_max'} attempts. Stopping here.") if ( !$status );
  231. # Setup connection caching if needed
  232. if ( !$opts->{'no_cache'} && !$opts->{'_host_sock_key'} ) {
  233. $self->{'master_pid'} = $self->disown_master();
  234. $ENV{ $opts->{'_host_sock_key'} } = $self->get_ctl_path();
  235. }
  236. #Allow the user to unlink the host sock if we need to pop the cache for some reason
  237. $self->{'host_sock'} = $ENV{ $opts->{'_host_sock_key'} };
  238. return $self;
  239. };
  240. my $connection_check = sub {
  241. my ($self) = @_;
  242. return 1 if $self->check_master;
  243. local $@;
  244. local $disable_destructor = 1;
  245. eval { $self = $init_ssh->( __PACKAGE__, $self->{'_opts'} ) };
  246. return $@ ? 0 : 1;
  247. };
  248. # Try calling the function.
  249. # If it fails, then call $connection_check to reconnect if needed.
  250. #
  251. # The goal is to avoid calling $connection_check
  252. # unless something goes wrong since it adds about
  253. # 450ms to each ssh command.
  254. #
  255. # If the control socket has gone away, call
  256. # $connection_check ahead of time to reconnect it.
  257. my $call_ssh_reinit_if_check_fails = sub {
  258. my ( $self, $func, @args ) = @_;
  259. $connection_check->($self) if !-S $self->{'_ctl_path'};
  260. local $@;
  261. my @ret = eval { $self->$func(@args) };
  262. my $ssh_error = $@ || $self->error;
  263. warn "[WARN] $ssh_error" if $ssh_error;
  264. return @ret if !$ssh_error;
  265. $connection_check->($self);
  266. return ( $self->$func(@args) );
  267. };
  268. my $post_connect = sub {
  269. my ( $self, $opts ) = @_;
  270. $self->{'persistent_shell'}->close() if $self->{'persistent_shell'};
  271. undef $self->{'persistent_shell'};
  272. return;
  273. };
  274. my $trim = sub {
  275. my ($string) = @_;
  276. return '' unless length $string;
  277. $string =~ s/^\s+//;
  278. $string =~ s/\s+$//;
  279. return $string;
  280. };
  281. my $send = sub {
  282. my ( $self, $line_reader, @command ) = @_;
  283. $self->diag( "[DEBUG][$self->{'_opts'}{'host'}] EXEC " . join( " ", @command ) ) if $self->{'_opts'}{'debug'};
  284. my ( $pty, $err, $pid ) = $call_ssh_reinit_if_check_fails->( $self, 'open3pty', @command );
  285. $die_no_trace->("Net::OpenSSH::open3pty failed: $err") if ( !defined $pid || $self->error() );
  286. $self->{'_out'} = "";
  287. $line_reader = sub {
  288. my ( $self, $out, $stash_param ) = @_;
  289. $out =~ s/[\r\n]{1,2}$//;
  290. $self->{$stash_param} .= "$out\n";
  291. return;
  292. }
  293. if ref $line_reader ne 'CODE';
  294. # TODO make this async so you can stream STDERR *in order*
  295. # with STDOUT as well
  296. # That said, most only care about error if command fails, so...
  297. my $out;
  298. $line_reader->( $self, $out, '_out' ) while $out = $pty->getline;
  299. $pty->close;
  300. # only populate error if there's an error #
  301. $self->{'_err'} = '';
  302. $line_reader->( $self, $out, '_err' ) while $out = $err->getline;
  303. $err->close;
  304. waitpid( $pid, 0 );
  305. return $? >> 8;
  306. };
  307. my $TERMINATOR = "\r\n";
  308. my $send_persistent_cmd = sub {
  309. my ( $self, $command, $uuid ) = @_;
  310. $uuid //= Data::UUID->new()->create_str();
  311. $command = join( ' ', @$command );
  312. my $actual_cmd = "UUID='$uuid'; echo \"BEGIN \$UUID\"; $command; echo \"___\$?___\"; echo; echo \"EOF \$UUID\"";
  313. $self->diag("[DEBUG][$self->{'_opts'}{'host'}] EXEC $actual_cmd") if $self->{'_opts'}{'debug'};
  314. #Use command on bash to ignore stuff like aliases so that we have a minimum level of PEBKAC errors due to aliasing cp to cp -i, etc.
  315. $self->{'expect'}->print("${actual_cmd}${TERMINATOR}");
  316. # Rather than take the approach of cPanel, which commands then polls async,
  317. # it is more straightforward to echo unique strings before and after the command.
  318. # This made getting the return code somewhat more complicated, as you can see below.
  319. # That said, it also makes you not have to worry about doing things asynchronously.
  320. $self->{'expect'}->expect( $self->{'_opts'}{'expect_timeout'}, '-re', qr/BEGIN $uuid/m );
  321. $self->{'expect'}->expect( $self->{'_opts'}{'expect_timeout'}, '-re', qr/EOF $uuid/m ); # If nothing is printed in timeout, give up
  322. # Get the actual output, remove terminal grunk
  323. my $message = $trim->( $self->{'expect'}->before() );
  324. $message =~ s/[\r\n]{1,2}$//; # Remove 'secret newline' control chars
  325. $message =~ s/\x{d}//g; # More control chars
  326. $message = Term::ANSIColor::colorstrip($message); # Strip colors
  327. # Find the exit code
  328. my ($code) = $message =~ m/___(\d*)___$/;
  329. unless ( defined $code ) {
  330. # Tell the user if they've made a boo-boo
  331. my $possible_err = $trim->( $self->{'expect'}->before() );
  332. $possible_err =~ s/\s//g;
  333. $die_no_trace->("Runaway multi-line string detected. Please adjust the command passed.") if $possible_err =~ m/\>/;
  334. $die_no_trace->(
  335. "Could not determine exit code!
  336. It timed out (went $self->{'_opts'}{'expect_timeout'}s without printing anything).
  337. Run command outside of the persistent terminal please."
  338. );
  339. }
  340. $message =~ s/___(\d*)___$//g;
  341. return ( $message, $code );
  342. };
  343. my $do_persistent_command = sub {
  344. my ( $self, $command, $no_stderr ) = @_;
  345. if ( !$self->{'persistent_shell'} ) {
  346. my ( $pty, $pid ) = $call_ssh_reinit_if_check_fails->( $self, 'open2pty', $self->{'_remote_shell'} );
  347. die "Got no pty back from open2pty: " . $self->error if !$pty;
  348. # You might think that the below settings are important.
  349. # In most cases, they are not.
  350. $pty->set_raw();
  351. $pty->stty( 'raw', 'icrnl', '-echo' );
  352. $pty->slave->stty( 'raw', 'icrnl', '-echo' );
  353. #Hook in expect
  354. $self->diag("[DEBUG][$self->{'_opts'}{'host'}] INIT expect on for PTY with pid $pid") if $self->{'_opts'}{'debug'};
  355. $self->{'expect'} = Expect->init($pty);
  356. $self->{'expect'}->restart_timeout_upon_receive(1); #Logabandon by default
  357. # XXX WARNING bashisms. That said, I'm not sure how to better do this yet portably.
  358. my $expect_env_cmd = "export PS1=''; export TERM='dumb'; unset HISTFILE; export FOE='configured'; stty raw icrnl -echo; unalias -a; echo \"EOF=\$FOE\"";
  359. $self->diag("[DEBUG][$self->{'_opts'}{'host'}] EXEC $expect_env_cmd") if $self->{'_opts'}{'debug'};
  360. $self->{'expect'}->print("${expect_env_cmd}${TERMINATOR}");
  361. $self->{'expect'}->expect( $self->{'_opts'}{'expect_timeout'}, '-re', qr/EOF=configured/ );
  362. $self->{'expect'}->clear_accum();
  363. #cache
  364. $self->{'persistent_shell'} = $pty;
  365. $self->{'persistent_pid'} = $pid;
  366. }
  367. #execute the command
  368. my $uuid = Data::UUID->new()->create_str();
  369. push @$command, '2>', "/tmp/stderr_$uuid.out" unless $no_stderr;
  370. my ( $oot, $code ) = $send_persistent_cmd->( $self, $command, $uuid );
  371. $self->{'_out'} = $oot;
  372. unless ($no_stderr) {
  373. #Grab stderr
  374. ( $self->{'_err'} ) = $send_persistent_cmd->( $self, [ '/usr/bin/cat', "/tmp/stderr_$uuid.out" ] );
  375. #Clean up
  376. $send_persistent_cmd->( $self, [ '/usr/bin/rm', '-f', "/tmp/stderr_$uuid.out" ] );
  377. }
  378. return int($code);
  379. };
  380. #######################
  381. # END PRIVATE METHODS #
  382. #######################
  383. =head1 METHODS
  384. =head2 new
  385. Instantiate the object, establish the connection. Note here that I'm not allowing
  386. a connection string like the parent module, and instead exploding these out into
  387. opts to pass to the constructor. This is because we want to index certain things
  388. under the hood by user, etc. and I *do not* want to use a regexp to pick out
  389. your username, host, port, etc. when this problem is solved much more easily
  390. by forcing that separation on the caller's end.
  391. ACCEPTS:
  392. =over 4
  393. =item
  394. %opts - <HASH> A hash of key value pairs corresponding to the what you would normally pass in to Net::OpenSSH,
  395. along with the following keys:
  396. =over 4
  397. =item
  398. use_persistent_shell - Whether or not to setup Expect to watch a persistent TTY. Less stable, but faster.
  399. =item
  400. expect_timeout - When the above is active, how long should we wait before your program prints something
  401. before bailing out?
  402. =item
  403. no_agent - Pass in a truthy value to disable the SSH agent. By default the agent is enabled.
  404. =item
  405. die_on_drop - If, for some reason, the connection drops, just die instead of attempting reconnection.
  406. =item
  407. output_prefix - If given, is what we will tack onto the beginning of any output via diag method.
  408. useful for streaming output to say, a TAP consumer (test) via passing in '# ' as prefix.
  409. =item
  410. debug - Pass in a truthy value to enable certain diag statements I've added in the module and pass -v to ssh.
  411. =item
  412. home - STRING corresponding to an absolute path to something that "looks like" a homedir. Defaults to the user's homedir.
  413. useful in cases where you say, want to load SSH keys from a different path without changing assumptions about where
  414. keys exist in a homedir on your average OpenSSH using system.
  415. =item
  416. no_cache - Pass in a truthy value to disable caching the connection and object, indexed by host string.
  417. useful if for some reason you need many separate connections to test something. Make sure your MAX_SESSIONS is set sanely
  418. in sshd_config if you use this extensively.
  419. =item
  420. retry_interval - In the case that sshd is not up on the remote host, how long to wait while before reattempting connection.
  421. defaults to 6s. We retry $RETRY_MAX times, so this means waiting a little over a minute for SSH to come up by default.
  422. your situation requires longer intervals, pass in something longer.
  423. =item
  424. retry_max - Number of times to retry when a connection fails. Defaults to 10.
  425. =back
  426. =back
  427. RETURNS a Net::OpenSSH::More object.
  428. =head3 A note on Authentication order
  429. We attempt to authenticate using the following details, and in this order:
  430. 1) Use supplied key_path.
  431. 2) Use supplied password.
  432. 3) Use existing SSH agent (SSH_AUTH_SOCK environment variable)
  433. 4) Use keys that may exist in $HOME/.ssh - id_ed25519, id_ecdsa, id_rsa and id_dsa (in that order).
  434. If all methods therein fail, we will die, as nothing will likely work at that point.
  435. It is important to be aware of this if your remove host has something like fail2ban or cPHulkd
  436. enabled which monitors and blocks access based on failed login attempts. If this is you,
  437. ensure that you have not configured things in a way as to accidentally lock yourself out
  438. of the remote host just because you fatfingered a connection detail in the constructor.
  439. =cut
  440. sub new {
  441. my ( $class, %opts ) = @_;
  442. $opts{'host'} = '127.0.0.1' if !$opts{'host'} || $opts{'host'} eq 'localhost';
  443. $opts{'remote_shell'} ||= 'bash'; # prevent stupid defaults
  444. $opts{'expect_timeout'} //= 30; # If your program goes over 30s without printing...
  445. # Set defaults, check if we can return early
  446. %opts = ( %defaults, %opts );
  447. $opts{'_cache_index'} = "$opts{'user'}_$opts{'host'}_$opts{'port'}";
  448. return $cache{ $opts{'_cache_index'} } unless $opts{'no_cache'} || !$cache{ $opts{'_cache_index'} };
  449. # Figure out how we're gonna login
  450. $opts{'_login_method'} = $resolve_login_method->( \%opts );
  451. # check permissions on base files if we got here
  452. $check_local_perms->( "$opts{'home'}/.ssh", 0700, 1 ) if -e "$opts{'home'}/.ssh";
  453. $check_local_perms->( "$opts{'home'}/.ssh/config", 0600 ) if -e "$opts{'home'}/.ssh/config";
  454. # Make the connection
  455. my $self = $init_ssh->( $class, \%opts );
  456. $cache{ $opts{'_cache_index'} } = $self unless $opts{'no_cache'};
  457. # Stash opts for later
  458. $self->{'_opts'} = \%opts;
  459. # Establish persistent shell, etc.
  460. $post_connect->( $self, \%opts );
  461. return $self;
  462. }
  463. =head2 use_persistent_shell
  464. Pass "defined but falsy/truthy" to this to enable using the persistent shell or deactivate its' use.
  465. Returns either the value you just set or the value it last had (if arg is not defined).
  466. =cut
  467. sub use_persistent_shell {
  468. my ( $self, $use_shell ) = @_;
  469. return $self->{'_opts'}{'use_persistent_shell'} if !defined($use_shell);
  470. return $self->{'_opts'}{'use_persistent_shell'} = $use_shell;
  471. }
  472. =head2 copy
  473. Copies $SOURCE file on the remote machine to $DEST on the remote machine.
  474. If you want to sync/copy files from remote to local or vice/versa, use
  475. the sftp accessor (Net::SFTP::Foreign) instead.
  476. Dies in this module, as this varies on different platforms (GNU/LINUX, Windows, etc.)
  477. =cut
  478. sub copy {
  479. die "Unimplemented, use a subclass of this perhaps?";
  480. }
  481. =head2 B<backup_files (FILES)>
  482. Backs up files which you wish to later restore to their original state. If the file does
  483. not currently exist then the method will still store a reference for later file deletion.
  484. This may seem strange at first, but think of it in the context of preserving 'state' before
  485. a test or scripted action is run. If no file existed prior to action, the way to restore
  486. that state would be to delete the added file(s).
  487. NOTE: Since copying files on the remote system to another location on the remote system
  488. is in fact not something implemented by Net::SFTP::Foreign, this is necessarily going
  489. to be a "non-portable" method -- use the Linux.pm subclass of this if you want to be able
  490. to actually backup files without dying, or subclass your own for Windows, however they
  491. choose to implement `copy` with their newfangled(?) SSH daemon.
  492. C<FILES> - LIST - File(s) to backup.
  493. C<STASH> - BOOL - mv files on backup instead of cp. This will make sure FILES arg path no
  494. longer exists at all so a fresh FILE can be written during run.
  495. my $file = '/path/to/file.txt';
  496. $ssh->backup_files($file);
  497. my @files = ( '/path/to/file.txt', '/path/to/file2.txt' );
  498. $ssh->backup_files(@files);
  499. =cut
  500. sub backup_files {
  501. my ( $self, @files ) = @_;
  502. # For each file passed in
  503. foreach my $file (@files) {
  504. # If the file hasn't already been backed up
  505. if ( !defined $self->{'file_backups'}{$file} ) {
  506. # and the file exists
  507. if ( $self->sftp->test_e($file) ) {
  508. # then back it up
  509. $self->{'file_backups'}{$file} = time;
  510. my $bkup = $file . '.' . $self->{'file_backups'}{$file};
  511. $self->diag("[INFO] Backing up '$file' to '$bkup'");
  512. $self->copy( $file, $bkup ); # XXX Probably not that portable, maybe move to Linux.pm somehow?
  513. # otherwise if the file to be backed up doesn't exist
  514. }
  515. else {
  516. # then just note that a file may need to be deleted later
  517. $self->{'file_backups'}{$file} = '';
  518. }
  519. }
  520. }
  521. return;
  522. }
  523. =head2 B<restore_files (FILES)>
  524. Restores specific file(s) backed up using backup_files(), or all the backup files if none
  525. are specified, to their previous state.
  526. If the file in question DID NOT exist when backup_files was last invoked for the file,
  527. then the file will instead be deleted, as that was the state of the file previous to
  528. actions taken in your test or script.
  529. C<FILES> - (Optional) - LIST - File(s) to restore.
  530. my $file = '/path/to/file.txt';
  531. $ssh->backup_files($file);
  532. $ssh->restore_files();
  533. =cut
  534. sub restore_files {
  535. my ( $self, @files ) = @_;
  536. # If no files were passed in then grab all files that have been backed up
  537. @files = keys( %{ $self->{'file_backups'} } ) if !@files;
  538. # foreach file
  539. foreach my $file (@files) {
  540. # that has been marked as modified
  541. if ( defined $self->{'file_backups'}{$file} ) {
  542. # if a backup exists
  543. if ( $self->{'file_backups'}{$file} ) {
  544. # then restore the backup
  545. my $bkup = $file . '.' . $self->{'file_backups'}{$file};
  546. if ( $self->sftp->test_e($bkup) ) {
  547. $self->diag("[INFO] Restoring backup '$file' from '$bkup'");
  548. $self->sftp->rename( $bkup, $file, 'overwrite' => 1 );
  549. }
  550. # otherwise no backup exists we just need to delete the modified file
  551. }
  552. else {
  553. $self->diag("[INFO] Deleting '$file' to restore system state (beforehand the file didn't exist)");
  554. $self->sftp->remove($file);
  555. }
  556. }
  557. delete $self->{'file_backups'}{$file};
  558. }
  559. return;
  560. }
  561. =head2 DESTROY
  562. Noted in POD only because of some behavior differences between the
  563. parent module and this. The following actions are taken *before*
  564. the parent's destructor kicks in:
  565. * Return early if you aren't the PID which created the object.
  566. * Restore any files backed up with backup_files earlier.
  567. =cut
  568. sub DESTROY {
  569. my ($self) = @_;
  570. return if !$self->{'_perl_pid'} || $$ != $self->{'_perl_pid'} || $disable_destructor;
  571. $self->restore_files();
  572. $ENV{SSH_AUTH_SOCK} = $self->{'_opts'}{'_restore_auth_sock'} if $self->{'_opts'}{'_restore_auth_sock'};
  573. $self->{'persistent_shell'}->close() if $self->{'persistent_shell'};
  574. return $self->SUPER::DESTROY();
  575. }
  576. =head2 diag
  577. Print a diagnostic message to STDOUT.
  578. Optionally prefixed by what you passed in as $opts{'output_prefix'} in the constructor.
  579. I use this in several places when $opts{'debug'} is passed to the constructor.
  580. ACCEPTS LIST of messages.
  581. RETURNS undef.
  582. =cut
  583. sub diag {
  584. my ( $self, @msgs ) = @_;
  585. print STDOUT "$self->{'_opts'}{'output_prefix'}$_\n" for @msgs;
  586. return;
  587. }
  588. =head2 cmd
  589. Execute specified command via SSH. If first arg is HASHREF, then it uses that as options.
  590. Command is specifed as a LIST, as that's the easiest way to ensure escaping is done correctly.
  591. $opts HASHREF:
  592. C<no_stderr> - Boolean - Whether or not to discard STDERR.
  593. C<use_operistent_shell> - Boolean - Whether or not to use the persistent shell.
  594. C<command> - LIST of components combined together to make a shell command.
  595. Returns LIST STDOUT, STDERR, and exit code from executed command.
  596. my ($out,$err,$ret) = $ssh->cmd(qw{ip addr show});
  597. If use_persistent_shell was truthy in the constructor (or you override via opts HR),
  598. then commands are executed in a persistent Expect session to cut down on forks,
  599. and in general be more efficient.
  600. However, some things can hang this up.
  601. Unterminated Heredoc & strings, for instance.
  602. Also, long running commands that emit no output will time out.
  603. Also, be careful with changing directory;
  604. this can cause unexpected side-effects in your code.
  605. Changing shell with chsh will also be ignored;
  606. the persistent shell is what you started with no matter what.
  607. In those cases, use_persistent_shell should be called to disable that before calling this.
  608. Also note that persistent mode basically *requires* you to use bash.
  609. I am not yet aware of how to make this better yet.
  610. If the 'debug' opt to the constructor is set, every command executed hereby will be printed.
  611. If no_stderr is passed, stderr will not be gathered (it takes writing/reading to a file, which is additional time cost).
  612. BUGS:
  613. In no_persist mode, stderr and stdout are merged, making the $err parameter returned less than useful.
  614. =cut
  615. sub cmd {
  616. my ($self) = shift;
  617. my $opts = ref $_[0] eq 'HASH' ? shift : {};
  618. my @command = @_;
  619. $die_no_trace->( 'No command specified', 'PEBCAK' ) if !@command;
  620. my $ret;
  621. $opts->{'use_persistent_shell'} = $self->{'_opts'}{'use_persistent_shell'} if !exists $opts->{'use_persistent_shell'};
  622. if ( $opts->{'use_persistent_shell'} ) {
  623. $ret = $do_persistent_command->( $self, \@command, $opts->{'no_stderr'} );
  624. }
  625. else {
  626. $ret = $send->( $self, undef, @command );
  627. }
  628. chomp( my $out = $self->{'_out'} );
  629. my $err = $self->error || '';
  630. $self->{'last_exit_code'} = $ret;
  631. return ( $out, $err, $ret );
  632. }
  633. =head2 cmd_exit_code
  634. Same thing as cmd but only returns the exit code.
  635. =cut
  636. sub cmd_exit_code {
  637. my ( $self, @args ) = @_;
  638. return ( $self->cmd(@args) )[2];
  639. }
  640. sub sftp {
  641. my ($self) = @_;
  642. unless ( defined $self->{'_sftp'} ) {
  643. $self->{'_sftp'} = $self->SUPER::sftp();
  644. die 'Unable to establish SFTP connection to remote host: ' . $self->error() unless defined $self->{'_sftp'};
  645. }
  646. return $self->{'_sftp'};
  647. }
  648. =head3 B<write (FILE,CONTENT,[MOD],[OWN])>
  649. Write a file.
  650. C<FILE> - Absolute path to file.
  651. C<CONTENT> - Content to write to file.
  652. C<MOD> - File mode.
  653. C<OWN> - File owner. Defaults to the user you connected as.
  654. C<GRP> - File group. Defaults to OWN.
  655. Returns true if all actions are successful, otherwise warn/die about the error.
  656. $ssh->write($filename,$content,'600','root');
  657. =cut
  658. sub write {
  659. my ( $self, $file, $content, $mode, $owner, $group ) = @_;
  660. die '[PARAMETER] No file specified' if !defined $file;
  661. die '[PARAMETER] File content not specified' if !defined $content;
  662. my %opts;
  663. $opts{'perm'} = $mode if $mode;
  664. my $ret = $self->sftp()->put_content( $content, $file, %opts );
  665. warn "[WARN] Write failed: " . $self->sftp()->error() if !$ret;
  666. if ( defined $owner || defined $group ) {
  667. $owner //= $self->{'_opts'}{'user'};
  668. $group //= $owner;
  669. $ret = $self->sftp()->chown( $file, $owner, $group );
  670. warn "[WARN] Couldn't chown $file" if $ret;
  671. }
  672. return $ret;
  673. }
  674. =head3 B<eval_full( options )>
  675. Run Perl code on the remote system and return the results.
  676. interpreter defaults to /usr/bin/perl.
  677. B<Input>
  678. Input options are supplied as a hash with the following keys:
  679. code - A coderef or string to execute on the remote system.
  680. args - An optional arrayref of arguments to the code.
  681. exe - Path to perl executable. Optional.
  682. B<Output>
  683. The output from eval_full() is based on the return value of the input
  684. coderef. Return context is preserved for the coderef.
  685. All error states will generate exceptions.
  686. B<Caveats>
  687. A coderef supplied to this function will be serialized by B::Deparse
  688. and recreated on the remote server. This method of moving the code does
  689. not support closing over variables, and any needed modules must
  690. be loaded inside the coderef with C<require>.
  691. B<Example>
  692. my $greeting_message = $ssh->eval_full( code => sub { return "Hello $_[0]";}, args => [$name] );
  693. =cut
  694. sub eval_full {
  695. my ( $self, %options ) = @_;
  696. my $code = $options{code};
  697. my $args = $options{args} // [];
  698. my $exe = $options{exe} || '/usr/bin/perl';
  699. require Storable;
  700. local $Storable::Deparse = 1;
  701. my ( $in_fh, $out_fh, undef, $pid ) = $call_ssh_reinit_if_check_fails->(
  702. $self,
  703. 'open_ex',
  704. { stdin_pipe => 1, stdout_pipe => 1, stderr_to_stdout => 1 },
  705. q{export PERLCODE='use Storable;$Storable::Eval=1;my $input;while ($input .= <STDIN>) { if ($input =~ /\d+START_STORABLE(.*)STOP_STORABLE\d+/) { my @result = eval { my $in_hr = Storable::thaw(pack("H*", $1)); if ( ref $in_hr->{code} ) { return $in_hr->{wantarray} ? $in_hr->{code}->(@{$in_hr->{args}}) : scalar $in_hr->{code}->(@{$in_hr->{args}});} return $in_hr->{wantarray} ? eval $in_hr->{code} : scalar eval $in_hr->{code};}; print $$ . "START_STORABLE" . unpack("H*", Storable::freeze( { data => \@result, error => "$@" })) . "STOP_STORABLE" . $$ . "\n";exit;}}'; }
  706. . $exe
  707. . q{ -e "$PERLCODE";}
  708. );
  709. die "Failed to connect: $!" unless ($pid);
  710. print $in_fh $$ . "START_STORABLE" . unpack( "H*", Storable::freeze( { code => $code, args => $args, wantarray => wantarray() } ) ) . "STOP_STORABLE" . $$ . "\n";
  711. close $in_fh;
  712. my $output = '';
  713. while ( $out_fh->sysread( $output, 4096, length($output) ) > 0 ) {
  714. 1;
  715. }
  716. close $out_fh;
  717. waitpid( $pid, 0 );
  718. my $result = { error => "Unable to deserialize output from remote_eval: $output" };
  719. if ( $output =~ /\d+START_STORABLE(.*)STOP_STORABLE\d+/ ) {
  720. $result = Storable::thaw( pack( "H*", $1 ) );
  721. }
  722. die $result->{error} if ( $result->{error} );
  723. return wantarray ? @{ $result->{data} } : $result->{data}[0];
  724. }
  725. =head3 cmd_stream
  726. Pretty much the same as running cmd() with one important caveat --
  727. all output is formatted with the configured prefix and *streams* to STDOUT.
  728. Useful for remote test harness building.
  729. Returns (exit_code), as in this context that should be all you care about.
  730. You may be asking, "well then why not use system?" That does not support
  731. the prefixing I'm doing here. Essentially we provide a custom line reader
  732. to 'send' which sends the output to STDOUT via 'diag' as well as doing
  733. the "default" behavior (append the line to the relevant output vars).
  734. NOTE: This uses send() exclusively, and will never invoke the persistent shell,
  735. so if you want that, don't use this.
  736. =cut
  737. sub cmd_stream {
  738. my ( $self, @cmd ) = @_;
  739. my $line_reader = sub {
  740. my ( $self, $out, $stash_param ) = @_;
  741. $out =~ s/[\r\n]{1,2}$//;
  742. $self->diag($out);
  743. $self->{$stash_param} .= "$out\n";
  744. return;
  745. };
  746. return $send->( $self, $line_reader, @cmd );
  747. }
  748. =head1 SPECIAL THANKS
  749. cPanel, L.L.C. - in particularly the QA department (which the authors once were in).
  750. Many of the ideas for this module originated out of lessons learned from our time
  751. writing a ssh based remote teststuite for testing cPanel & WHM.
  752. Chris Eades - For the original module this evolved from at cPanel over the years.
  753. bdraco (Nick Koston) - For optimization ideas and the general process needed for expect & persistent shell.
  754. J.D. Lightsey - For the somewhat crazy looking but nonetheless very useful eval_full subroutine used
  755. to execute subroutine references from the orchestrating server on the remote host's perl.
  756. Brian M. Carlson - For the highly useful sftp shortcut method that caches Net::SFTP::Foreign.
  757. Rikus Goodell - For shell escaping expertise
  758. =head1 IN MEMORY OF
  759. Paul Trost
  760. Dan Stewart
  761. =cut
  762. 1;