| 3 | rodolico | 1 | #!/usr/bin/env perl
 | 
        
           |  |  | 2 |   | 
        
           |  |  | 3 | # Common library for havirt. Basically, just a place to put things which may be used by any
 | 
        
           | 4 | rodolico | 4 | # part of havirt. More for organizations purposes.
 | 
        
           | 3 | rodolico | 5 |   | 
        
           | 4 | rodolico | 6 | # Copyright 2024 Daily Data, Inc.
 | 
        
           |  |  | 7 | # 
 | 
        
           |  |  | 8 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following 
 | 
        
           |  |  | 9 | # conditions are met:
 | 
        
           |  |  | 10 | #
 | 
        
           |  |  | 11 | #   Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 | 
        
           |  |  | 12 | #   Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer 
 | 
        
           |  |  | 13 | #   in the documentation and/or other materials provided with the distribution.
 | 
        
           |  |  | 14 | #   Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived
 | 
        
           |  |  | 15 | #   from this software without specific prior written permission.
 | 
        
           |  |  | 16 | # 
 | 
        
           |  |  | 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 | 
        
           |  |  | 18 | # NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 | 
        
           |  |  | 19 | # THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 | 
        
           |  |  | 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 | 
        
           |  |  | 21 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 | 
        
           |  |  | 22 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 | 
        
           |  |  | 23 |   | 
        
           |  |  | 24 |   | 
        
           | 3 | rodolico | 25 | # v0.0.1 20240602 RWR
 | 
        
           |  |  | 26 | # Initial setup
 | 
        
           |  |  | 27 |   | 
        
           |  |  | 28 | package havirt;
 | 
        
           |  |  | 29 |   | 
        
           |  |  | 30 | use warnings;
 | 
        
           |  |  | 31 | use strict;  
 | 
        
           |  |  | 32 |   | 
        
           |  |  | 33 | use Data::Dumper qw(Dumper); # Import the Dumper() subroutine
 | 
        
           |  |  | 34 |   | 
        
           | 4 | rodolico | 35 | # define the version number
 | 
        
           |  |  | 36 | # see https://metacpan.org/pod/release/JPEACOCK/version-0.97/lib/version.pod
 | 
        
           |  |  | 37 | use version;
 | 
        
           | 19 | rodolico | 38 | our $VERSION = version->declare("1.0.0");
 | 
        
           | 4 | rodolico | 39 |   | 
        
           |  |  | 40 |   | 
        
           | 3 | rodolico | 41 | use Exporter;
 | 
        
           |  |  | 42 |   | 
        
           |  |  | 43 | our @ISA = qw( Exporter );
 | 
        
           |  |  | 44 | our @EXPORT = qw( 
 | 
        
           |  |  | 45 |                   &readDB &writeDB
 | 
        
           | 15 | rodolico | 46 |                   &report &scan
 | 
        
           |  |  | 47 |                   &makeCommand &forceScan
 | 
        
           |  |  | 48 |                   &executeAndWait
 | 
        
           | 18 | rodolico | 49 |                   &findDomain
 | 
        
           |  |  | 50 |                   &diffArray
 | 
        
           | 3 | rodolico | 51 |                 );
 | 
        
           |  |  | 52 |   | 
        
           | 12 | rodolico | 53 | # read a DB file (just a YAML)
 | 
        
           |  |  | 54 | # if $lock is set, will create a "lock" file so other processes will
 | 
        
           |  |  | 55 | # not try to write to it. Using custom code as flock is automagically
 | 
        
           |  |  | 56 | # release when the file is read
 | 
        
           | 3 | rodolico | 57 |   | 
        
           |  |  | 58 | sub readDB {
 | 
        
           | 12 | rodolico | 59 |    my $lock = shift;
 | 
        
           |  |  | 60 |    my $lockFileName = "$main::statusDBName.lock";
 | 
        
           |  |  | 61 |    my $lockTime = 5; # maximum time to wait for lock to clear
 | 
        
           |  |  | 62 |    # wait for lock to clear if it exists, if we are wanting a lock
 | 
        
           |  |  | 63 |    # and we have tried it for $locktime iterations
 | 
        
           |  |  | 64 |    while ( $lock && -f $lockFileName && $lockTime-- ) {
 | 
        
           |  |  | 65 |       sleep 1; # wait one second, then try again
 | 
        
           |  |  | 66 |    }
 | 
        
           |  |  | 67 |    if ( $lock ) {
 | 
        
           |  |  | 68 |       die "Something has $main::statusDBName locked, aborting\n" if -f $lockFileName;
 | 
        
           |  |  | 69 |       `touch $lockFileName`;
 | 
        
           |  |  | 70 |    }
 | 
        
           | 3 | rodolico | 71 |    my $yaml = YAML::Tiny->new( {} );
 | 
        
           | 12 | rodolico | 72 |    if ( -f $main::statusDBName ) {
 | 
        
           |  |  | 73 |       $yaml = YAML::Tiny->read( $main::statusDBName );
 | 
        
           | 3 | rodolico | 74 |    }
 | 
        
           | 12 | rodolico | 75 |    $main::statusDB = $yaml->[0];
 | 
        
           | 3 | rodolico | 76 | }
 | 
        
           |  |  | 77 |   | 
        
           |  |  | 78 | sub writeDB {
 | 
        
           | 12 | rodolico | 79 |    my $yaml = YAML::Tiny->new( $main::statusDB );
 | 
        
           |  |  | 80 |    $yaml->write( $main::statusDBName );
 | 
        
           |  |  | 81 |    unlink "$main::statusDBName.lock" if -f "$main::statusDBName.lock"; # release any lock we might have on it
 | 
        
           | 3 | rodolico | 82 | }
 | 
        
           |  |  | 83 |   | 
        
           | 4 | rodolico | 84 | sub report {
 | 
        
           |  |  | 85 |    if ( $main::reportFormat eq 'tsv' ) {
 | 
        
           |  |  | 86 |       return &report_tsv( @_ );
 | 
        
           |  |  | 87 |    } else {
 | 
        
           |  |  | 88 |       return &report_screen( @_ );
 | 
        
           |  |  | 89 |    }
 | 
        
           |  |  | 90 | }
 | 
        
           |  |  | 91 |   | 
        
           | 3 | rodolico | 92 | sub report_tsv {
 | 
        
           |  |  | 93 |    my ( $header, $data ) = @_;
 | 
        
           |  |  | 94 |    my @output;
 | 
        
           |  |  | 95 |    push @output, join( "\t", @$header );
 | 
        
           |  |  | 96 |    for( my $line = 0; $line < @$data; $line++ ) {
 | 
        
           |  |  | 97 |       push @output, join( "\t", @{$data->[$line]} );
 | 
        
           |  |  | 98 |    } # for
 | 
        
           |  |  | 99 |    return join( "\n", @output ) . "\n";
 | 
        
           |  |  | 100 | }
 | 
        
           |  |  | 101 |   | 
        
           |  |  | 102 | sub report_screen {
 | 
        
           |  |  | 103 |    my ( $header, $data ) = @_;
 | 
        
           |  |  | 104 |    my @output;
 | 
        
           |  |  | 105 |    my @widths;
 | 
        
           |  |  | 106 |    my $column;
 | 
        
           |  |  | 107 |    my $row;
 | 
        
           |  |  | 108 |    # First, initialize by using the length of the headers
 | 
        
           |  |  | 109 |    for ( $column = 0; $column < @$header; $column++ ) {
 | 
        
           |  |  | 110 |       @widths[$column] = length( $header->[$column] );
 | 
        
           |  |  | 111 |    }
 | 
        
           |  |  | 112 |    # now, go through all data in each row, for each column, and increment the width if it is larger
 | 
        
           |  |  | 113 |    for ( $row = 0; $row < @$data; $row++ ) {
 | 
        
           |  |  | 114 |       for ( $column = 0; $column < @$header; $column++ ) {
 | 
        
           |  |  | 115 |          $widths[$column] = length( $data->[$row][$column] ) 
 | 
        
           |  |  | 116 |             if length( $data->[$row][$column] ) > $widths[$column];
 | 
        
           |  |  | 117 |       } # for column
 | 
        
           |  |  | 118 |    } # for row
 | 
        
           |  |  | 119 |    # actually do the print now
 | 
        
           |  |  | 120 |    my @format;
 | 
        
           |  |  | 121 |    for ( $column = 0; $column < @widths; $column++ ) {
 | 
        
           |  |  | 122 |       push ( @format, '%' . $widths[$column] . 's' );
 | 
        
           |  |  | 123 |    }
 | 
        
           |  |  | 124 |    my $format = join( ' ', @format ) . "\n";
 | 
        
           |  |  | 125 |    my $output = sprintf( $format, @$header );
 | 
        
           |  |  | 126 |    for ( $row = 0; $row < @$data; $row++ ) {
 | 
        
           |  |  | 127 |       $output .= sprintf( $format, @{$data->[$row]} );
 | 
        
           |  |  | 128 |    } # for row
 | 
        
           |  |  | 129 |    return $output;
 | 
        
           |  |  | 130 | }
 | 
        
           | 10 | rodolico | 131 |   | 
        
           | 15 | rodolico | 132 | # scans a node to determine which domains are running on it
 | 
        
           |  |  | 133 | sub getDomainsOnNode {
 | 
        
           |  |  | 134 |    my $node = shift;
 | 
        
           |  |  | 135 |    my @nodeList = grep { /^\s*\d/ } `ssh $node 'virsh list'`;
 | 
        
           |  |  | 136 |    for ( my $i = 0; $i < @nodeList; $i++ ) {
 | 
        
           |  |  | 137 |       if ( $nodeList[$i] =~ m/\s*\d+\s*([^ ]+)/ ) {
 | 
        
           |  |  | 138 |          $nodeList[$i] = $1;
 | 
        
           |  |  | 139 |       }
 | 
        
           |  |  | 140 |    }
 | 
        
           |  |  | 141 |    my %hash = map{ $_ => time } @nodeList;
 | 
        
           |  |  | 142 |    return \%hash;
 | 
        
           |  |  | 143 | }
 | 
        
           |  |  | 144 |   | 
        
           | 18 | rodolico | 145 | # find node a domain is on
 | 
        
           |  |  | 146 | # first parameter is the domain name
 | 
        
           |  |  | 147 | # rest of @_ is list of nodes to search
 | 
        
           |  |  | 148 | # if no nodes passed in, will search all known nodes
 | 
        
           |  |  | 149 | # returns first node found with the domain, or an empty string if not found
 | 
        
           |  |  | 150 | # possibly not being used??
 | 
        
           |  |  | 151 | sub findDomain {
 | 
        
           |  |  | 152 |    my $domainName = shift;
 | 
        
           |  |  | 153 |    my @node = @_;
 | 
        
           |  |  | 154 |    my $foundNode = '';
 | 
        
           |  |  | 155 |    &readDB();
 | 
        
           |  |  | 156 |    unless ( @node ) {
 | 
        
           |  |  | 157 |       @node = keys %{$main::statusDB->{'node'} };
 | 
        
           |  |  | 158 |       print "findDomain, nodes = " . join( "\t", @node ) . "\n" if $main::DEBUG > 1;
 | 
        
           |  |  | 159 |    }
 | 
        
           |  |  | 160 |    foreach my $thisNode ( @node ) {
 | 
        
           |  |  | 161 |       my $output = `ssh $thisNode 'virsh list'`;
 | 
        
           |  |  | 162 |       print "findDomain, $thisNode list =\n" . $output . "\n" if $main::DEBUG > 1;;
 | 
        
           |  |  | 163 |       return $thisNode if ( $output =~ m/$domainName/ );
 | 
        
           |  |  | 164 |    }
 | 
        
           |  |  | 165 |    return '';
 | 
        
           |  |  | 166 | }
 | 
        
           | 15 | rodolico | 167 |   | 
        
           |  |  | 168 | # check one or more nodes and determine which domains are running on them.
 | 
        
           |  |  | 169 | # defaults to everything in the node database, but the -t can have it run on only one
 | 
        
           |  |  | 170 | # this is the function that should be run every few minutes on one of the servers
 | 
        
           |  |  | 171 | sub scan {
 | 
        
           |  |  | 172 |    if ( -f $main::lastScanFileName && ! $main::force ) {
 | 
        
           |  |  | 173 |       my $lastScan = time - ( stat( $main::lastScanFileName ) ) [9];
 | 
        
           |  |  | 174 |       return "Scan was run $lastScan seconds ago\n" unless $lastScan > $main::minScanTimes;
 | 
        
           |  |  | 175 |    }
 | 
        
           |  |  | 176 |    `touch $main::lastScanFileName`;
 | 
        
           |  |  | 177 |    &main::readDB(1);
 | 
        
           |  |  | 178 |    print Dumper( $main::statusDB->{'nodePopulation'} ) if $main::DEBUG > 2;
 | 
        
           |  |  | 179 |    my @targets;
 | 
        
           |  |  | 180 |    if ( $main::targetNode ) {
 | 
        
           |  |  | 181 |       push @targets, $main::targetNode;
 | 
        
           |  |  | 182 |    } else {
 | 
        
           |  |  | 183 |       @targets = keys %{$main::statusDB->{'node'}};
 | 
        
           |  |  | 184 |    }
 | 
        
           |  |  | 185 |    print "Scanning " . join( "\n", @targets ) . "\n" if $main::DEBUG;
 | 
        
           |  |  | 186 |    foreach my $node (@targets) {
 | 
        
           |  |  | 187 |       $main::statusDB->{'nodePopulation'}->{$node}->{'running'} = &getDomainsOnNode( $node );
 | 
        
           |  |  | 188 |       $main::statusDB->{'nodePopulation'}->{$node}->{'lastchecked'} = time;
 | 
        
           |  |  | 189 |       foreach my $domain ( keys %{$main::statusDB->{'nodePopulation'}->{$node}->{'running'}} ) {
 | 
        
           |  |  | 190 |          # make sure there is an entry for all of these domains
 | 
        
           |  |  | 191 |          $main::statusDB->{'virt'}->{$domain} = {} unless exists( $main::statusDB->{'virt'}->{$domain} );
 | 
        
           |  |  | 192 |       }
 | 
        
           |  |  | 193 |       print Dumper( $main::statusDB->{'nodePopulation'}->{$node} ) if $main::DEBUG > 2;
 | 
        
           |  |  | 194 |    }
 | 
        
           |  |  | 195 |    &main::writeDB();
 | 
        
           |  |  | 196 |    return "Node(s) updated\n";
 | 
        
           |  |  | 197 | }
 | 
        
           |  |  | 198 |   | 
        
           | 18 | rodolico | 199 | # makes the command that will be run on a node
 | 
        
           |  |  | 200 | # Created as a sub so we can change format easily
 | 
        
           | 15 | rodolico | 201 | sub makeCommand {
 | 
        
           |  |  | 202 |    my ( $node, $command ) = @_;
 | 
        
           |  |  | 203 |    return "ssh $node '$command'";
 | 
        
           |  |  | 204 | }
 | 
        
           |  |  | 205 |   | 
        
           | 18 | rodolico | 206 | # force a node scan, even if time has not expired
 | 
        
           | 15 | rodolico | 207 | sub forceScan {
 | 
        
           |  |  | 208 |    my $save = $main::force;
 | 
        
           |  |  | 209 |    $main::force = 1;
 | 
        
           |  |  | 210 |    &main::scan();
 | 
        
           |  |  | 211 |    $main::force = $save;
 | 
        
           |  |  | 212 | }
 | 
        
           |  |  | 213 |   | 
        
           |  |  | 214 |   | 
        
           |  |  | 215 | # executes command $command, then repeatedly runs virsh list
 | 
        
           |  |  | 216 | # on $scanNode, grep'ing for $scanDomain
 | 
        
           |  |  | 217 | # $condition is 1 (true) or 0 (false)
 | 
        
           |  |  | 218 | sub executeAndWait {
 | 
        
           |  |  | 219 |    my ( $command, $scanNode, $scanDomain, $condition ) = @_;
 | 
        
           |  |  | 220 |    my $waitSeconds = 5; # number of seconds to wait before checking again
 | 
        
           |  |  | 221 |    my $maxIterations = 60 / $waitSeconds; # maximum number of tries
 | 
        
           |  |  | 222 |    print "Running [$command], then waiting $waitSeconds to check if complete\n" if $main::DEBUG;
 | 
        
           |  |  | 223 |    `$command`;
 | 
        
           |  |  | 224 |    my $waitCommand = &makeCommand( $scanNode, "virsh list | grep $scanDomain" );
 | 
        
           |  |  | 225 |    my $output = '';
 | 
        
           |  |  | 226 |    do {
 | 
        
           |  |  | 227 |       return 0 unless ( $maxIterations-- ); # we've waited too long, so probably not working
 | 
        
           |  |  | 228 |       print '. ';
 | 
        
           |  |  | 229 |       sleep 5;
 | 
        
           |  |  | 230 |       $output = `$waitCommand`;
 | 
        
           |  |  | 231 |       print "[$waitCommand] returned [$output]\n" if $main::DEBUG > 1;
 | 
        
           |  |  | 232 |    } until ( $condition ? $output : !$output );
 | 
        
           |  |  | 233 |    return 1; # made it successful
 | 
        
           |  |  | 234 | } 
 | 
        
           |  |  | 235 |   | 
        
           | 18 | rodolico | 236 | # find the differences between two arrays (passed by reference)
 | 
        
           |  |  | 237 | # first sorts the array, then walks through them one by one
 | 
        
           |  |  | 238 | # @$arr1 MUST be larger than @$arr2
 | 
        
           |  |  | 239 | sub diffArray {
 | 
        
           |  |  | 240 |    my ( $arr1, $arr2 ) = @_;
 | 
        
           |  |  | 241 |    my @result;
 | 
        
           |  |  | 242 |   | 
        
           |  |  | 243 |    @$arr1 = sort @$arr1;
 | 
        
           |  |  | 244 |    @$arr2 = sort @$arr2;
 | 
        
           |  |  | 245 |    my $i=0;
 | 
        
           |  |  | 246 |    my $j=0;
 | 
        
           |  |  | 247 |   | 
        
           |  |  | 248 |    while ( $i < @$arr1 ) {
 | 
        
           |  |  | 249 |       if ( $arr1->[$i] eq $arr2->[$j] ) {
 | 
        
           |  |  | 250 |          $i++;
 | 
        
           |  |  | 251 |          $j++;
 | 
        
           |  |  | 252 |       } elsif ( $arr1->[$i] lt $arr2->[$j] ) {
 | 
        
           |  |  | 253 |          push @result, $arr1->[$i];
 | 
        
           |  |  | 254 |          $i++;
 | 
        
           |  |  | 255 |       } else {
 | 
        
           |  |  | 256 |          push @result, $arr2->[$j];
 | 
        
           |  |  | 257 |          $j++;
 | 
        
           |  |  | 258 |       }
 | 
        
           |  |  | 259 |    }
 | 
        
           |  |  | 260 |    return \@result;
 | 
        
           |  |  | 261 | }
 |