#!/usr/bin/env perl # # This file is part of StorageDisplay # # This software is copyright (c) 2014-2023 by Vincent Danjean. # # This is free software; you can redistribute it and/or modify it under # the same terms as the Perl 5 programming language system itself. # # PODNAME: storage2dot # ABSTRACT: analyse and generate a graphical view of a machine storage use strict; use warnings; our $VERSION = '2.02'; # VERSION # delay module loading so that local data collect can be done # without extra modules # use StorageDisplay; use StorageDisplay::Collect; my $cleanup_readmode=0; sub collect_from_remote { my $remote = shift; my $content=''; eval { require Net::OpenSSH; Net::OpenSSH->import; require Term::ReadKey; Term::ReadKey->import; } or die "Cannot load required modules (Net::OpenSSH and/or Term::ReadKey) for remote data collect: $!\n"; END { if ($cleanup_readmode) { # in case of bug, always restore normal mode ReadMode('normal'); } } my $ssh = Net::OpenSSH->new($remote); $ssh->error and die "Couldn't establish SSH connection: ". $ssh->error; my ($in, $out, $pid) = $ssh->open2( #'cat', 'perl', '--', '-', ); my $fdperlmod; open($fdperlmod, '<', $INC{'StorageDisplay/Collect.pm'}) or die "Cannot open ".INC{'StorageDisplay/Collect.pm'}.": $!\n"; #use Sys::Syscall; #Sys::Syscall::sendfile($in, $fdperlmod); { while(defined(my $line=<$fdperlmod>)) { last if $line =~ m/^__END__\s*$/; print $in $line; } close $fdperlmod; } #print $in "StorageDisplay::Collect::dump_collect;\n"; my @args = (@_, 'LocalBySSH'); my $cmd = "StorageDisplay::Collect::dump_collect('".join("','", @args)."');\n"; print STDERR 'Running through SSH: ',$cmd; print $in $cmd; print $in "__END__\n"; flush $in; use IO::Select; use POSIX ":sys_wait_h"; my $sel = IO::Select->new(\*STDIN, $out); my $timeout = 1; $cleanup_readmode=1; ReadMode('noecho'); my ($in_closed,$out_closed) = (0,0); while(1) { $!=0; my @ready = $sel->can_read($timeout); if ($!) { die "Error with select: $!\n"; } if (scalar(@ready)) { foreach my $fd (@ready) { if ($fd == $out) { my $line=<$out>; if (defined($line)) { $content .= $line; } else { $sel->remove($out); close $out; $out_closed=1; } } else { my $line=<STDIN>; if (print $in $line) { flush $in; } else { $sel->remove(\*STDIN); close $in; $in_closed=1; } } } } else { my $res = waitpid($pid, WNOHANG); if ($res==-1) { die "Some error occurred ".($? >> 8).": $!\n"; } if ($res) { if (!$in_closed) { $sel->remove(\*STDIN); close $in; } ReadMode('normal'); last; } #print STDERR "timeout for $pid\n"; } } if (!$out_closed) { while (defined(my $line=<$out>)) { $content .= $out; } $sel->remove($out); close $out; } ReadMode('normal'); $cleanup_readmode=0; return $content; } use Getopt::Long; use Pod::Usage; use Data::Dumper; $Data::Dumper::Sortkeys = 1; $Data::Dumper::Purity = 1; #use Carp::Always; my $remote; my $data; my $output; my $collect; my $recordfile; my $replayfile; my $verbose; my $help; my $man; GetOptions ("d|data=s" => \$data, # string "r|remote=s" => \$remote, # string "o|output=s" => \$output, # string "c|collect-only" => \$collect, # flag "record-file=s" => \$recordfile, # string "replay-file=s" => \$replayfile, # string "verbose" => \$verbose, # flag "h|help" => \$help, # flag "man" => \$man, # flag ) or pod2usage(2); sub main() { pod2usage(-exitval => 0, -verbose => 1) if $help; pod2usage(-exitval => 0, -verbose => 2) if $man; if (defined($data) && ( defined($remote) || defined($recordfile) || defined($replayfile) )) { die "E: --data cannot be used with --remote, --record, nor --replay\n"; } if (defined($replayfile) && ( defined($remote) || defined($recordfile) )) { die "E: --replay cannot be used with --remote, nor --record\n"; } if (!$collect) { require StorageDisplay or die "Cannot load the StorageDisplay module to handle collected data: $!\n"; } my $infos; if ($replayfile) { require StorageDisplay::Collect::CMD::Replay or die "Replay requested, but unable to load the StorageDisplay::Collect::CMD::Replay module: $!\n"; my $dh; open($dh, "<", $replayfile) or die "Cannot open '$replayfile': $!" ; my $replay=join('', <$dh>); my $replaydata; close($dh); { my $VAR1; eval($replay); ## no critic (ProhibitStringyEval) #print STDERR "c: $content\n"; $replaydata = $VAR1; } $infos = StorageDisplay::Collect->new( 'Replay', 'replay-data' => $replaydata)->collect(); } my $contents; my @recorder; if (defined($recordfile)) { @recorder = ('Proxy::Recorder', 'recorder-reader'); } if (defined($data)) { my $dh; open($dh, "<", $data) or die "Cannot open '$data': $!" ; $contents=join('', <$dh>); close($dh); } elsif (defined($remote)) { $contents = collect_from_remote($remote, @recorder); } elsif (not defined($infos)) { $infos = StorageDisplay::Collect->new(@recorder, 'Local')->collect(); } # data are in $contents (if got through Data::Dumper) or directly in $infos if (defined($contents)) { # moving data from $contents to $infos { my $VAR1; eval($contents); ## no critic (ProhibitStringyEval) #print STDERR "c: $content\n"; $infos = $VAR1; } } if (defined($recordfile)) { if (! exists($infos->{'recorder'})) { print STDERR "W: skpping recording: no records!\n"; } else { my $dh; open($dh, ">", $recordfile) or die "Cannot open '$data': $!"; print $dh Dumper($infos->{'recorder'}); close($dh); } } delete($infos->{'recorder'}); my $oldout; if (defined($output)) { # dzil do not want Two-argument "open" # so, commented-out as we do not use it # if this change, a way to write this would have to be found # open(my $oldout, ">&STDOUT") or die "Can't dup STDOUT: $!"; open(STDOUT, '>', $output) or die "Can't redirect STDOUT to $output: $!"; } if ($collect) { print Dumper($infos); return; } my $st=StorageDisplay->new('infos' => $infos); $st->createElems(); $st->display; } main __END__ =pod =encoding UTF-8 =head1 NAME storage2dot - analyse and generate a graphical view of a machine storage =head1 VERSION version 2.02 =head1 SYNOPSIS B<storage2dot [OPTIONS]> Options: --remote|-r MACHINE collect data on MACHINE (through SSH) --collect-only|-c generate plain data instead of dot file --data|-d FILE use FILE as data source --output|-o FILE write output into FILE --record-file FILE record shell commandsinto FILE [for tests] --replay-file FILE collect data from FILE [for tests] --help|-h brief documentation --man full documentation This program can be used to collect data about the storage state from local or remote machines (through SSH) and use them to generate a DOT graphic representing them. =head1 OPTIONS =over 8 =item B<--remote MACHINE> Collect storage data on MACHINE (through SSH). By default, local storage data are collected (without SSH). =item B<--collect-only> By default, a DOT file is generated from the storage data. With this option, the program do not create the DOT file but only output the raw collected data for later analyze. =item B<--data FILE> In order to generate the DOT file, use the provided data. B<FILE> must have been created with the help of the previous option. No new data are collected when this option is used. =item B<--output FILE> Write generated data (DOT by default) into B<FILE> instead of the standard output. =item B<--record-file FILE> Write shell commands (and their output) that are used to collect data into B<FILE>. This is mainly used for reproducibility during tests. =item B<--replay-file FILE> Use information from B<FILE> instead of running real shell commands in order to collect data. B<FILE> must be This is mainly used for reproducibility during tests. =item B<--help> Print a brief help message and exits. =item B<--man> Prints the manual page and exits. =back =head1 EXAMPLES =over 8 =item B<storage2dot -o state.dot> Generate a DOT file representing the state of the storage on the local machine. =item B<storage2dot -r host -o state.dot> Generate a DOT file representing the state of the storage on the remote B<host> machine. Only perl (and its standard modules) are required on the remote machine. Of course, a SSH account is also required. =item B<storage2dot -c -o state.data> Just collect data on current machine without generating a DOT file. Only perl (and its standard modules) are required on the current machine. =item B<storage2dot --data state.data -o state.dot> Generate a DOT file representing the state of the storage recorded in the state.data file. Extra perl modules are required for this command. =item B<dot -Tpdf state.dot >>B< state.pdf> Generate a PDF from the DOT file using dot(1). =back =head1 AUTHOR Vincent Danjean <Vincent.Danjean@ens-lyon.org> =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2014-2023 by Vincent Danjean. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut