File Coverage

bin/storage-merge-dots
Criterion Covered Total %
statement 49 227 21.5
branch 4 98 4.0
condition 0 8 0.0
subroutine 13 22 59.0
pod n/a
total 66 355 18.5


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2             #
3             # This file is part of StorageDisplay
4             #
5             # This software is copyright (c) 2014-2023 by Vincent Danjean.
6             #
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             #
10              
11             # PODNAME: storage-merge-dots
12             # ABSTRACT: merge dot files created by storage2dot, adding inter links if possible
13              
14 2     2   10796 use strict;
  2         5  
  2         80  
15 2     2   8 use warnings;
  2         2  
  2         152  
16              
17              
18 2         557307 our $VERSION = '2.06'; # VERSION
19              
20              
21 2     2   972 use StorageDisplay;
  2         60  
  2         235  
22 2     2   1589 use StorageDisplay::Collect;
  2         30  
  2         287  
23              
24             sub collect_from_remote {
25 0     0   0 my $remote = shift;
26 0         0 my $content='';
27 2     2   2383 use Net::OpenSSH;
  2         93713  
  2         147  
28 2     2   1406 use Term::ReadKey;
  2         6306  
  2         805  
29             END {
30 2     2   1017100 ReadMode('normal');
31             }
32 0         0 my $ssh = Net::OpenSSH->new($remote);
33 0 0       0 $ssh->error and
34             die "Couldn't establish SSH connection: ". $ssh->error;
35              
36 0         0 my ($in, $out, $pid) = $ssh->open2(
37             #'cat',
38             'perl', '--', '-',
39             );
40              
41 0         0 my $fdperlmod;
42             open($fdperlmod, '<', $INC{'StorageDisplay/Collect.pm'})
43 0 0       0 or die "Cannot open ".INC{'StorageDisplay/Collect.pm'}.": $!\n";
  0         0  
44             #use Sys::Syscall;
45             #Sys::Syscall::sendfile($in, $fdperlmod);
46             {
47 0         0 while(defined(my $line=<$fdperlmod>)) {
  0         0  
48 0 0       0 last if $line =~ m/^__END__\s*$/;
49 0         0 print $in $line;
50             }
51 0         0 close $fdperlmod;
52             }
53             #print $in "StorageDisplay::Collect::dump_collect;\n";
54 0         0 my @args = (@_, 'LocalBySSH');
55 0         0 my $cmd = "StorageDisplay::Collect::dump_collect('".join("','", @args)."');\n";
56 0         0 print STDERR 'Running through SSH: ',$cmd;
57 0         0 print $in $cmd;
58 0         0 print $in "__END__\n";
59 0         0 flush $in;
60              
61 2     2   1156 use IO::Select;
  2         4400  
  2         167  
62 2     2   21 use POSIX ":sys_wait_h";
  2         14  
  2         33  
63 0         0 my $sel = IO::Select->new(\*STDIN, $out);
64 0         0 my $timeout = 1;
65 0         0 ReadMode('noecho');
66 0         0 my ($in_closed,$out_closed) = (0,0);
67 0         0 while(1) {
68 0         0 $!=0;
69 0         0 my @ready = $sel->can_read($timeout);
70 0 0       0 if ($!) {
71 0         0 die "Error with select: $!\n";
72             }
73 0 0       0 if (scalar(@ready)) {
74 0         0 foreach my $fd (@ready) {
75 0 0       0 if ($fd == $out) {
76 0         0 my $line=<$out>;
77 0 0       0 if (defined($line)) {
78 0         0 $content .= $line;
79             } else {
80 0         0 $sel->remove($out);
81 0         0 close $out;
82 0         0 $out_closed=1;
83             }
84             } else {
85 0         0 my $line=<STDIN>;
86 0 0       0 if (print $in $line) {
87 0         0 flush $in;
88             } else {
89 0         0 $sel->remove(\*STDIN);
90 0         0 close $in;
91 0         0 $in_closed=1;
92             }
93             }
94             }
95             } else {
96 0         0 my $res = waitpid($pid, WNOHANG);
97 0 0       0 if ($res==-1) {
98 0         0 die "Some error occurred ".($? >> 8).": $!\n";
99             }
100 0 0       0 if ($res) {
101 0 0       0 if (!$in_closed) {
102 0         0 $sel->remove(\*STDIN);
103 0         0 close $in;
104             }
105 0         0 ReadMode('normal');
106 0         0 last;
107             }
108             #print STDERR "timeout for $pid\n";
109             }
110             }
111 0 0       0 if (!$out_closed) {
112 0         0 while (defined(my $line=<$out>)) {
113 0         0 $content .= $out;
114             }
115 0         0 $sel->remove($out);
116 0         0 close $out;
117             }
118 0         0 return $content;
119             }
120              
121 2     2   2895 use Getopt::Long;
  2         31963  
  2         13  
122 2     2   2248 use Pod::Usage;
  2         121187  
  2         409  
123 2     2   21 use Data::Dumper;
  2         5  
  2         776012  
124 2         19 $Data::Dumper::Sortkeys = 1;
125 2         7 $Data::Dumper::Purity = 1;
126              
127 2         10 my $help;
128             my $man;
129 2         0 my $verbose;
130 2         8 my $vmnames={};
131              
132 2 50       28 GetOptions ("v|verbose+" => \$verbose, # flag
133             "h|help" => \$help, # flag
134             "man" => \$man, # flag
135             "vm=s" => $vmnames,
136             ) or pod2usage(2);
137              
138 2         2666 my $dotfiles;
139              
140 2         15 my $print_level=0;
141              
142             sub print_info {
143 0 0   0   0 if ($verbose) {
144 0         0 foreach my $l (@_) {
145 0         0 print STDERR "I: ".(' 'x$print_level).$l."\n";
146             }
147             }
148             }
149              
150             sub print_warn {
151 0     0   0 foreach my $l (@_) {
152 0         0 print STDERR "W: ".(' 'x$print_level).$l."\n";
153             }
154             }
155              
156 2         6 my $slinks={};
157 2         8 my $tlinks={};
158              
159             sub add_link {
160 0     0   0 my $self = shift;
161 0         0 my $name = shift;
162 0         0 my $link = shift;
163 0 0       0 if ($link !~ m,([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s*$,) {
164              
165 0         0 return;
166             }
167 0         0 my ($vm, $size, $disk, $sl) = ($1, $2, $3, $4);
168 0         0 my $realdisk = 1;
169 0 0       0 if ($disk =~ m,^[(](.*)[)]$,) {
170 0         0 $disk = $1;
171 0         0 $realdisk = 0;
172             }
173 0 0       0 if (exists($self->{$vm}->{$disk})) {
174 0         0 print_warn "Ignoring duplicate $name link: $link";
175             } else {
176 0         0 $self->{$vm}->{$disk} = {
177             'link' => $sl,
178             'size' => $size,
179             'realdisk' => $realdisk,
180             'name' => $disk,
181             };
182             }
183             }
184              
185             sub add_slink {
186 0     0   0 my $link = shift;
187 0         0 add_link($slinks, "source", $link);
188             }
189              
190             sub add_tlink {
191 0     0   0 my $link = shift;
192 0         0 add_link($tlinks, "target", $link);
193             }
194              
195             sub check_disks {
196 0     0   0 my $hostdisk = shift;
197 0         0 my $vmdisk = shift;
198              
199 0 0       0 return 0 if not exists($vmdisk->{name});
200 0 0       0 return 0 if $hostdisk->{matched};
201 0 0       0 return 0 if $vmdisk->{matched};
202              
203 0 0       0 if ($hostdisk->{size} != $vmdisk->{size}) {
204 0         0 print_info "Rejecting $hostdisk->{name} to $vmdisk->{name}: different size";
205 0         0 return 0;
206             }
207 0         0 return 1;
208             }
209              
210             sub match_links {
211 0     0   0 my $host = shift;
212 0         0 my $vm = shift;
213              
214 0         0 my @matching;
215 0         0 foreach my $hostdname (sort keys %$host) {
216 0         0 my $hostdisk = $host->{$hostdname};
217 0 0       0 next if $hostdisk->{matched};
218 0         0 my $vmdname = $hostdname;
219 0   0     0 my $vmdisk = $vm->{$vmdname} // {};
220 0 0 0     0 if ($hostdisk->{realdisk}
221             && check_disks($hostdisk, $vmdisk)) {
222             # fast path with real diskname (qemu agent was available)
223 0         0 push @matching, [$hostdisk, $vmdisk];
224 0         0 $hostdisk->{matched} = 1;
225 0         0 $vmdisk->{matched} = 1;
226 0         0 next;
227             }
228             # slow path: qemu-agent not available or device renamed after reboot
229             # trying to check with disksize
230 0         0 $vmdisk = undef;
231 0         0 foreach my $vmdisk_try (values %$vm) {
232 0 0       0 if (check_disks($hostdisk, $vmdisk_try)) {
233 0 0       0 if (defined($vmdisk)) {
234 0         0 print_warn "Multiple disks matching, ignoring it";
235 0         0 $vmdisk = undef;
236 0         0 last;
237             }
238 0         0 $vmdisk = $vmdisk_try;
239             }
240             }
241 0 0       0 if (defined($vmdisk)) {
242 0         0 push @matching, [$hostdisk, $vmdisk];
243 0         0 $hostdisk->{matched} = 1;
244 0         0 $vmdisk->{matched} = 1;
245             }
246             }
247 0         0 return @matching;
248             }
249              
250 2         4 my $one_handled;
251             sub handle_dotfile {
252 0     0   0 my $filename = shift;
253              
254 0         0 my $state = 0;
255 0 0       0 open(my $h, '<', $filename) or die "Cannot read $filename\n";
256 0 0       0 my $print = $one_handled ? 0 : 1;
257 0         0 my $header_size = 0;
258 0         0 my $footer_size = 0;
259            
260 0         0 while(defined(my $line = <$h>)) {
261 0         0 chomp($line);
262 0 0       0 if ($state == 0) {
263 0         0 $state = 1;
264             }
265 0 0       0 if ($line =~ m,//\s*HEADER: MACHINE\s*$,) {
    0          
266 0 0       0 if ($state == 1) {
267 0 0       0 print $line, "\n" if not $one_handled;
268 0         0 $header_size++;
269             } else {
270 0         0 print_warn "Ignoring header line after data: $line";
271             }
272             } elsif ($line =~ m,//\s*FOOTER: MACHINE\s*$,) {
273 0 0       0 if ($state < 2) {
    0          
274 0         0 print_warn "Ignoring footer line before data: $line";
275             } elsif ($state == 2) {
276 0         0 $state = 3;
277             }
278 0 0       0 if ($state == 3) {
279 0 0       0 if ($line !~ m,^\s*}\s*//\s*FOOTER: MACHINE\s*$,) {
280 0         0 print_warn "Strange footer line $line";
281             }
282 0         0 $footer_size++;
283             }
284             } else {
285 0 0       0 if ($state < 1) {
    0          
286 0         0 print_warn "Data before header";
287             } elsif ($state == 1) {
288 0         0 $state = 2;
289 0         0 $one_handled++;
290             }
291 0 0       0 if ($state == 2) {
    0          
292 0         0 print $line, "\n";
293 0 0       0 if ($line =~ m,//\s*SOURCE\s+LINK\s*:\s*(.*)$,) {
294 0         0 my $link = $1;
295 0         0 add_slink($link);
296             }
297 0 0       0 if ($line =~ m,//\s*TARGET\s+LINK\s*:\s*(.*)$,) {
298 0         0 my $link = $1;
299 0         0 add_tlink($link);
300             }
301             } elsif ($state == 3) {
302 0         0 print_warn "Ignoring data after footer line: $line";
303             }
304             }
305            
306             }
307 0 0       0 if ($header_size != 2) {
308 0         0 print_warn "Strange header with $header_size lines";
309             }
310 0 0       0 if ($footer_size != 1) {
311 0         0 print_warn "Strange footer with $header_size lines";
312             }
313             }
314              
315             sub main() {
316 2 100   2   34 pod2usage(-exitval => 0, -verbose => 1) if $help;
317 1 50       11 pod2usage(-exitval => 0, -verbose => 2) if $man;
318              
319 0           foreach my $dotfilename (@ARGV) {
320 0           print_info "handling $dotfilename";
321 0           $print_level++;
322 0           handle_dotfile($dotfilename);
323 0           $print_level--;
324             }
325             #use Data::Dumper;
326             #print STDERR Dumper($slinks), "\n";
327             #print STDERR Dumper($tlinks), "\n";
328 0           print_info "handling interlinks";
329 0           $print_level++;
330 0           foreach my $vm (sort keys %{$slinks}) {
  0            
331 0   0       my $hostname = $vmnames->{$vm} // $vm;
332 0           print_info "Looking for $vm ($hostname) VM";
333 0 0         if (exists($tlinks->{$hostname})) {
334 0           $print_level++;
335 0           print_info "Found target";
336 0           my @matches = match_links($slinks->{$vm}, $tlinks->{$hostname});
337 0           foreach (@matches) {
338 0           my ($hd, $vmd) = @{$_};
  0            
339 0           print_info "Linking $hd->{name}\@$vm to $vmd->{name}\@$hostname";
340 0           print $hd->{link}, " -> ", $vmd->{link}, "\n";
341             }
342 0           $print_level--;
343             }
344             }
345 0           $print_level--;
346              
347 0 0         if ($one_handled) {
348 0           print "} // FOOTER: MACHINE\n";
349             }
350             }
351              
352 2         12 main
353              
354             __END__
355              
356             =pod
357              
358             =encoding UTF-8
359              
360             =head1 NAME
361              
362             storage-merge-dots - merge dot files created by storage2dot, adding inter links if possible
363              
364             =head1 VERSION
365              
366             version 2.06
367              
368             =head1 SYNOPSIS
369              
370             B<storage-merge-dots [OPTIONS] DOTFILES...>
371              
372             Options:
373             --vm VMNAME=HOSTNAME associate vmname and hostname
374             --help|-h brief documentation
375             --man full documentation
376              
377             This program can be used to merge several DOT files created by
378             B<storage2dot> and produce (on stdout) a new DOT file with all the
379             machine storage states. If DOT files contains a host and a virtual
380             machine storage state, then they should be linked toguether in the
381             generated DOT file.
382              
383             =head1 OPTIONS
384              
385             =over 8
386              
387             =item B<--vm VMNAME=HOSTNAME>
388              
389             Use this option (possibly several times) in order to associate the
390             B<VMNAME> virtual machine name on the host to the B<HOSTNAME> hostname
391             of the virtual machine. By default, this program considers both are
392             the same.
393              
394             =back
395              
396             =head1 AUTHOR
397              
398             Vincent Danjean <Vincent.Danjean@ens-lyon.org>
399              
400             =head1 COPYRIGHT AND LICENSE
401              
402             This software is copyright (c) 2014-2023 by Vincent Danjean.
403              
404             This is free software; you can redistribute it and/or modify it under
405             the same terms as the Perl 5 programming language system itself.
406              
407             =cut