5 Commits 9a1c7b6264 ... 185ce56ee2

Auteur SHA1 Message Date
  Andy Baugh 185ce56ee2 Forgot I'm in caveman land il y a 1 mois
  Andy Baugh 918c187e52 Die, don't confess il y a 1 mois
  Andy Baugh e4899cace1 WIP on copy/backup files il y a 1 mois
  Andy Baugh e502b2ee64 Sin tax il y a 1 mois
  Andy Baugh 30b44a15e1 Can't recall all the changes here but they are good I'm sure il y a 1 mois
4 fichiers modifiés avec 195 ajouts et 9 suppressions
  1. 2 1
      README.md
  2. 170 6
      lib/Net/OpenSSH/More.pm
  3. 22 1
      lib/Net/OpenSSH/More/Linux.pm
  4. 1 1
      t/Net-OpenSSH-More-Linux.t

+ 2 - 1
README.md

@@ -2,6 +2,7 @@ Net::OpenSSH::More
 ==================
 ==================
 A submodule of Net::OpenSSH focused on giving you some more tools
 A submodule of Net::OpenSSH focused on giving you some more tools
 for executing common usage patterns of the (quite useful) parent
 for executing common usage patterns of the (quite useful) parent
-module.
+module, but which are basically left "as an exercise for the reader"
+in the parent's POD.
 
 
 See module POD for more details, and HAPPY HACKING!
 See module POD for more details, and HAPPY HACKING!

+ 170 - 6
lib/Net/OpenSSH/More.pm

@@ -36,7 +36,6 @@ Highlights:
 * Many shortcut methods for common system administration tasks
 * Many shortcut methods for common system administration tasks
 * Registration method for commands to run upon DESTROY/before disconnect.
 * Registration method for commands to run upon DESTROY/before disconnect.
 * Automatic reconnection ability upon connection loss
 * Automatic reconnection ability upon connection loss
-* Easy SFTP accessor for file uploads/downloads.
 
 
 =head1 SYNOPSIS
 =head1 SYNOPSIS
 
 
@@ -335,7 +334,8 @@ my $send = sub {
       }
       }
       if ref $line_reader ne 'CODE';
       if ref $line_reader ne 'CODE';
 
 
-    # TODO make this async so you can stream STDERR as well
+    # TODO make this async so you can stream STDERR *in order*
+    # with STDOUT as well
     # That said, most only care about error if command fails, so...
     # That said, most only care about error if command fails, so...
     my $out;
     my $out;
     $line_reader->( $self, $out, '_out' ) while $out = $pty->getline;
     $line_reader->( $self, $out, '_out' ) while $out = $pty->getline;
@@ -542,18 +542,150 @@ sub use_persistent_shell {
     return $self->{'_opts'}{'use_persistent_shell'} = $use_shell;
     return $self->{'_opts'}{'use_persistent_shell'} = $use_shell;
 }
 }
 
 
+=head2 copy
+
+Copies $SOURCE file on the remote machine to $DEST on the remote machine.
+If you want to sync/copy files from remote to local or vice/versa, use
+the sftp accessor (Net::SFTP::Foreign) instead.
+
+Dies in this module, as this varies on different platforms (GNU/LINUX, Windows, etc.)
+
+=cut
+
+sub copy {
+    die "Unimplemented, use a subclass of this perhaps?";
+}
+
+=head2 B<backup_files (FILES)>
+
+Backs up files which you wish to later restore to their original state. If the file does
+not currently exist then the method will still store a reference for later file deletion.
+This may seem strange at first, but think of it in the context of preserving 'state' before
+a test or scripted action is run. If no file existed prior to action, the way to restore
+that state would be to delete the added file(s).
+
+NOTE: Since copying files on the remote system to another location on the remote system
+is in fact not something implemented by Net::SFTP::Foreign, this is necessarily going
+to be a "non-portable" method -- use the Linux.pm subclass of this if you want to be able
+to actually backup files without dying, or subclass your own for Windows, however they
+choose to implement `copy` with their newfangled(?) SSH daemon.
+
+C<FILES> - ARRAY_REF - File(s) to backup.
+
+C<STASH> - BOOL - mv files on backup instead of cp.  This will make sure FILES arg path no
+                  longer exists at all so a fresh FILE can be written during run.
+
+my $file = '/path/to/file.txt';
+$ssh->backup_files([$file]);
+
+my @files = ( '/path/to/file.txt', '/path/to/file2.txt' );
+$ssh->backup_files(\@files);
+
+=cut
+
+sub backup_files {
+    my ( $self, $files, $stash ) = @_;
+
+    die "[INPUT]: ARRAYREF required by backup_files()." if ( ref $files ne 'ARRAY' );
+
+    # For each file passed in
+    foreach my $file (@$files) {
+
+        # If the file hasn't already been backed up
+        if ( !defined $self->{'file_backups'}{$file} ) {
+
+            # and the file exists
+            if ( $self->sftp->test_e($file) ) {
+
+                # then back it up
+                $self->{'file_backups'}{$file} = time;
+                my $bkup = $file . '.' . $self->{'file_backups'}{$file};
+                if ($stash) {
+                    $self->diag("[INFO] Moving '$file' to '$bkup'");
+                    $self->sftp->rename( $file, $bkup );
+                }
+                else {
+                    $self->diag("[INFO] Backing up '$file' to '$bkup'");
+                    $self->copy( $file, $bkup );    # XXX Probably not that portable, maybe move to Linux.pm somehow?
+                }
+
+                # otherwise if the file to be backed up doesn't exist
+            }
+            else {
+                # then just note that a file may need to be deleted later
+                $self->{'file_backups'}{$file} = '';
+            }
+        }
+    }
+    return;
+}
+
+=head2 B<restore_files (FILES)>
+
+Restores specific file(s) backed up using backup_files(), or all the backup files if none
+are specified, to their previous state.
+
+If the file in question DID NOT exist when backup_files was last invoked for the file,
+then the file will instead be deleted, as that was the state of the file previous to
+actions taken in your test or script.
+
+C<FILES> - (Optional) - ARRAY_REF - File(s) to restore.
+
+my $file = '/path/to/file.txt';
+$ssh->backup_files([$file]);
+$ssh->restore_files();
+
+=cut
+
+sub restore_files {
+    my ( $self, $files ) = @_;
+
+    # If no files were passed in then grab all files that have been backed up
+    $files //= [ keys %{ $self->{'file_backups'} } ];
+    die "[INPUT]: ARRAYREF required by restore_files()." if ( ref $files ne 'ARRAY' );
+
+    # foreach file
+    foreach my $file (@$files) {
+
+        # that has been marked as modified
+        if ( defined $self->{'file_backups'}{$file} ) {
+
+            # if a backup exists
+            if ( $self->{'file_backups'}{$file} ) {
+
+                # then restore the backup
+                my $bkup = $file . '.' . $self->{'file_backups'}{$file};
+                if ( $self->sftp->test_e($bkup) ) {
+                    $self->diag("[INFO] Restoring backup '$file' from '$bkup'");
+                    $self->sftp->rename( $bkup, $file, 'overwrite' => 1 );
+                }
+
+                # otherwise no backup exists we just need to delete the modified file
+            }
+            else {
+                $self->diag("[INFO] Deleting '$file' to restore system state (beforehand the file didn't exist)");
+                $self->sftp->remove($file);
+            }
+        }
+        delete $self->{'file_backups'}{$file};
+    }
+    return;
+}
+
 =head2 DESTROY
 =head2 DESTROY
 
 
 Noted in POD only because of some behavior differences between the
 Noted in POD only because of some behavior differences between the
 parent module and this. The following actions are taken *before*
 parent module and this. The following actions are taken *before*
 the parent's destructor kicks in:
 the parent's destructor kicks in:
 * Return early if you aren't the PID which created the object.
 * Return early if you aren't the PID which created the object.
+* Restore any files backed up with backup_files earlier.
 
 
 =cut
 =cut
 
 
 sub DESTROY {
 sub DESTROY {
     my ($self) = @_;
     my ($self) = @_;
-    return                                                       if !$self->{'_perl_pid'} || $$ != $self->{'_perl_pid'} || $disable_destructor;
+    return if !$self->{'_perl_pid'} || $$ != $self->{'_perl_pid'} || $disable_destructor;
+    $self->restore_files();
     $ENV{SSH_AUTH_SOCK} = $self->{'_opts'}{'_restore_auth_sock'} if $self->{'_opts'}{'_restore_auth_sock'};
     $ENV{SSH_AUTH_SOCK} = $self->{'_opts'}{'_restore_auth_sock'} if $self->{'_opts'}{'_restore_auth_sock'};
     $self->{'persistent_shell'}->close()                         if $self->{'persistent_shell'};
     $self->{'persistent_shell'}->close()                         if $self->{'persistent_shell'};
 
 
@@ -585,6 +717,7 @@ Command is specifed as a LIST, as that's the easiest way to ensure escaping is d
 
 
 $opts HASHREF:
 $opts HASHREF:
 C<no_stderr> - Boolean - Whether or not to discard STDERR.
 C<no_stderr> - Boolean - Whether or not to discard STDERR.
+C<use_operistent_shell> - Boolean - Whether or not to use the persistent shell.
 
 
 C<command> - LIST of components combined together to make a shell command.
 C<command> - LIST of components combined together to make a shell command.
 
 
@@ -592,7 +725,7 @@ Returns LIST STDOUT, STDERR, and exit code from executed command.
 
 
     my ($out,$err,$ret) = $ssh->cmd(qw{ip addr show});
     my ($out,$err,$ret) = $ssh->cmd(qw{ip addr show});
 
 
-If use_persistent_shell was truthy in the constructor,
+If use_persistent_shell was truthy in the constructor (or you override via opts HR),
 then commands are executed in a persistent Expect session to cut down on forks,
 then commands are executed in a persistent Expect session to cut down on forks,
 and in general be more efficient.
 and in general be more efficient.
 
 
@@ -625,7 +758,8 @@ sub cmd {
     $die_no_trace->( 'No command specified', 'PEBCAK' ) if !@command;
     $die_no_trace->( 'No command specified', 'PEBCAK' ) if !@command;
 
 
     my $ret;
     my $ret;
-    if ( $self->{'_opts'}{'use_persistent_shell'} ) {
+    $opts->{'use_persistent_shell'} = $self->{'_opts'}{'use_persistent_shell'} if !exists $opts->{'use_persistent_shell'};
+    if ( $opts->{'use_persistent_shell'} ) {
         $ret = $do_persistent_command->( $self, \@command, $opts->{'no_stderr'} );
         $ret = $do_persistent_command->( $self, \@command, $opts->{'no_stderr'} );
     }
     }
     else {
     else {
@@ -669,7 +803,7 @@ C<MOD> - File mode.
 C<OWN> - File owner. Defaults to the user you connected as.
 C<OWN> - File owner. Defaults to the user you connected as.
 C<GRP> - File group. Defaults to OWN.
 C<GRP> - File group. Defaults to OWN.
 
 
-Returns true if all actions are successful, otherwise confess the error.
+Returns true if all actions are successful, otherwise warn/die about the error.
 
 
     $ssh->write($filename,$content,'600','root');
     $ssh->write($filename,$content,'600','root');
 
 
@@ -768,6 +902,36 @@ sub eval_full {
     return wantarray ? @{ $result->{data} } : $result->{data}[0];
     return wantarray ? @{ $result->{data} } : $result->{data}[0];
 }
 }
 
 
+=head3 cmd_stream
+
+Pretty much the same as running cmd() with one important caveat --
+all output is formatted with the configured prefix and *streams* to STDOUT.
+Useful for remote test harness building.
+Returns (exit_code), as in this context that should be all you care about.
+
+You may be asking, "well then why not use system?" That does not support
+the prefixing I'm doing here. Essentially we provide a custom line reader
+to 'send' which sends the output to STDOUT via 'diag' as well as doing
+the "default" behavior (append the line to the relevant output vars).
+
+NOTE: This uses send() exclusively, and will never invoke the persistent shell,
+so if you want that, don't use this.
+
+=cut
+
+sub cmd_stream {
+    my ( $self, @cmd ) = @_;
+    my $line_reader = sub {
+        my ( $self, $out, $stash_param ) = @_;
+        $out =~ s/[\r\n]{1,2}$//;
+        $self->diag($out);
+        $self->{$stash_param} .= "$out\n";
+
+        return;
+    };
+    return $send->( $self, $line_reader, @cmd );
+}
+
 =head1 AUTHORS
 =head1 AUTHORS
 
 
 Thomas Andrew "Andy" Baugh <andy@troglodyne.net>
 Thomas Andrew "Andy" Baugh <andy@troglodyne.net>

+ 22 - 1
lib/Net/OpenSSH/More/Linux.pm

@@ -7,9 +7,19 @@ use parent 'Net::OpenSSH::More';
 
 
 use File::Slurper ();
 use File::Slurper ();
 
 
+=head1 NAME
+
+Net::OpenSSH::More::Linux
+
+=head1 DESCRIPTION
+
+This module contains useful methods to complement the parent's when in use on
+all linux environments.
+
 =head1 ASSUMPTIONS
 =head1 ASSUMPTIONS
 
 
 This module assumes that both the local and remote machine are some variant of GNU/Linux.
 This module assumes that both the local and remote machine are some variant of GNU/Linux.
+Don't use this if that's not the case.
 
 
 =cut
 =cut
 
 
@@ -52,7 +62,7 @@ sub get_primary_adapter {
             File::Slurper::read_text('/proc/net/route');
             File::Slurper::read_text('/proc/net/route');
         }
         }
         else {
         else {
-            $self->cmd( "cat /proc/net/route" );
+            $self->cmd("cat /proc/net/route");
         }
         }
     };
     };
     foreach my $line ( split( /\n/, $proc_route_path ) ) {
     foreach my $line ( split( /\n/, $proc_route_path ) ) {
@@ -103,4 +113,15 @@ sub get_local_ips {
     );
     );
 }
 }
 
 
+=head2 copy
+
+Effectively the same thing as `cp $SOURCE $DEST` on the remote server.
+
+=cut
+
+sub copy {
+    my ( $self, $SOURCE, $DEST ) = @_;
+    return $self->send("cp -a $SOURCE $DEST");
+}
+
 1;
 1;

+ 1 - 1
t/Net-OpenSSH-More-Linux.t

@@ -35,7 +35,7 @@ subtest_streamed "Common tests using mocks" => sub {
         'DESTROY'      => undef,
         'DESTROY'      => undef,
     );
     );
     my $obj = Net::OpenSSH::More::Linux->new( 'host' => 'localhost', retry_max => 1 );
     my $obj = Net::OpenSSH::More::Linux->new( 'host' => 'localhost', retry_max => 1 );
-    is( ref $obj,           'Net::OpenSSH::More::Linux', "Got right ref type for object upon instantiation" );
+    is( ref $obj, 'Net::OpenSSH::More::Linux', "Got right ref type for object upon instantiation" );
 };
 };
 
 
 done_testing();
 done_testing();