| 3 | rodolico | 1 | #!/usr/bin/env perl
 | 
        
           |  |  | 2 |   | 
        
           |  |  | 3 | # All functions related to maniplating a specific node
 | 
        
           |  |  | 4 | # part of havirt.
 | 
        
           |  |  | 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 |   | 
        
           | 3 | rodolico | 24 | # v0.0.1 20240602 RWR
 | 
        
           |  |  | 25 | # Initial setup
 | 
        
           | 26 | rodolico | 26 | #
 | 
        
           |  |  | 27 | # v1.2.0 20240826 RWR
 | 
        
           |  |  | 28 | # Added some code to migrate domains if node placed in maintenance mode
 | 
        
           |  |  | 29 | # Added a lot of 'verbose' print lines, and modified for new flag structure
 | 
        
           |  |  | 30 | #
 | 
        
           | 38 | rodolico | 31 | # v1.2.1 20240828 RWR
 | 
        
           |  |  | 32 | # Fixed node maintenance, adding scanning and fixing some bugs with
 | 
        
           |  |  | 33 | # the migration code
 | 
        
           | 3 | rodolico | 34 |   | 
        
           |  |  | 35 | package node;
 | 
        
           |  |  | 36 |   | 
        
           |  |  | 37 | use warnings;
 | 
        
           |  |  | 38 | use strict;  
 | 
        
           |  |  | 39 |   | 
        
           | 4 | rodolico | 40 | # define the version number
 | 
        
           |  |  | 41 | # see https://metacpan.org/pod/release/JPEACOCK/version-0.97/lib/version.pod
 | 
        
           |  |  | 42 | use version;
 | 
        
           | 38 | rodolico | 43 | our $VERSION = version->declare("1.2.1");
 | 
        
           | 3 | rodolico | 44 |   | 
        
           | 4 | rodolico | 45 |   | 
        
           |  |  | 46 | use Data::Dumper;
 | 
        
           |  |  | 47 |   | 
        
           | 3 | rodolico | 48 | use Exporter;
 | 
        
           |  |  | 49 |   | 
        
           |  |  | 50 | our @ISA = qw( Exporter );
 | 
        
           |  |  | 51 | our @EXPORT = qw( 
 | 
        
           | 5 | rodolico | 52 |                   &node &list &update &scan &add
 | 
        
           | 3 | rodolico | 53 |                 );
 | 
        
           |  |  | 54 |   | 
        
           | 4 | rodolico | 55 | # Converts from output of node info to a key we want to use
 | 
        
           |  |  | 56 | my %conversion = ( 
 | 
        
           |  |  | 57 |   'CPU frequency' => 'clock',
 | 
        
           |  |  | 58 |   'CPU model' => 'cpu_model',
 | 
        
           |  |  | 59 |   'CPU socket(s)' => 'cpu_socket',
 | 
        
           |  |  | 60 |   'CPU(s)' => 'cpu_count',
 | 
        
           |  |  | 61 |   'Core(s) per socket' => 'cpu_cores',
 | 
        
           |  |  | 62 |   'Memory size' => 'memory',
 | 
        
           |  |  | 63 |   'NUMA cell(s)' => 'numa_cells',
 | 
        
           |  |  | 64 |   'Thread(s) per core' => 'threads_per_core'
 | 
        
           |  |  | 65 | );
 | 
        
           | 3 | rodolico | 66 |   | 
        
           | 9 | rodolico | 67 | # show a help screen
 | 
        
           |  |  | 68 | sub help {
 | 
        
           |  |  | 69 |    my @return;
 | 
        
           |  |  | 70 |    push @return, "node update [nodename|-t nodename]";
 | 
        
           |  |  | 71 |    push @return, "\tUpdates capabilities on one or more nodes, default is all nodes";
 | 
        
           |  |  | 72 |    push @return, "node list [--format|-f screen|tsv]";
 | 
        
           |  |  | 73 |    push @return, "\tLists all nodes with some statistics about them as screen or tsv (default screen)";
 | 
        
           |  |  | 74 |    push @return, "node scan [nodename|-t nodename]";
 | 
        
           |  |  | 75 |    push @return, "\tUpdates list of domains on one or more existing nodes, default is all nodes";
 | 
        
           | 29 | rodolico | 76 |    push @return, "node maintenance nodename [on|off --target=targetNode]";
 | 
        
           | 17 | rodolico | 77 |    push @return, "\ton - set maintenance flag; no domains can be started/migrated to node";
 | 
        
           | 29 | rodolico | 78 |    push @return, "\t     target must be set with the --target flag";
 | 
        
           | 17 | rodolico | 79 |    push @return, "\toff - Allows domains to be migrated/started on node";
 | 
        
           |  |  | 80 |    push @return, "\tnothing - displays current maintenance flag";
 | 
        
           |  |  | 81 |    push @return, "\tNote: a node with any domains running can not have maintenance mode turned on";
 | 
        
           | 9 | rodolico | 82 |    return join( "\n", @return ) . "\n";
 | 
        
           |  |  | 83 | }
 | 
        
           |  |  | 84 |   | 
        
           |  |  | 85 |   | 
        
           | 5 | rodolico | 86 | # lists hardware capabilities of all nodes (virsh nodeinfo)
 | 
        
           | 4 | rodolico | 87 | sub list {
 | 
        
           |  |  | 88 |    my @header;
 | 
        
           |  |  | 89 |    my @data;
 | 
        
           |  |  | 90 |    my $return;
 | 
        
           | 12 | rodolico | 91 |    &main::readDB();
 | 
        
           |  |  | 92 |    foreach my $node ( sort keys %{$main::statusDB->{'node'}} ) {
 | 
        
           | 4 | rodolico | 93 |       unless ( @header ) {
 | 
        
           |  |  | 94 |          # just grab the keys for headers
 | 
        
           | 12 | rodolico | 95 |          @header = sort keys %{ $main::statusDB->{'node'}->{$node} };
 | 
        
           | 4 | rodolico | 96 |          # put Node at the beginning
 | 
        
           |  |  | 97 |          unshift ( @header, 'Node' );
 | 
        
           |  |  | 98 |       }
 | 
        
           |  |  | 99 |       my @line;
 | 
        
           |  |  | 100 |       push @line, $node;
 | 
        
           | 12 | rodolico | 101 |       foreach my $column (sort keys %{ $main::statusDB->{'node'}->{$node} }) {
 | 
        
           |  |  | 102 |          push @line, $main::statusDB->{'node'}->{$node}->{$column};
 | 
        
           | 4 | rodolico | 103 |       }
 | 
        
           |  |  | 104 |       push (@data, \@line );
 | 
        
           |  |  | 105 |    }
 | 
        
           | 7 | rodolico | 106 |    return &main::report( \@header, \@data );
 | 
        
           | 4 | rodolico | 107 | }
 | 
        
           |  |  | 108 |   | 
        
           | 5 | rodolico | 109 | # Get information about a node. Really only needs to be done when a node is
 | 
        
           |  |  | 110 | # first defined, or if there is a hardware upgrade
 | 
        
           | 9 | rodolico | 111 | # reads information off of the stack (@_), but will add to that if --target
 | 
        
           |  |  | 112 | # was defined
 | 
        
           | 4 | rodolico | 113 | sub update {
 | 
        
           | 12 | rodolico | 114 |    &main::readDB( 1 ); # open and lock so we can write to it later
 | 
        
           | 26 | rodolico | 115 |    my @return;
 | 
        
           | 18 | rodolico | 116 |    my @requiredFields = ( 'maintenance' );
 | 
        
           | 4 | rodolico | 117 |    my @targets;
 | 
        
           | 25 | rodolico | 118 |    if ( $main::config->{'flags'}->{'target'} ) {
 | 
        
           |  |  | 119 |       push @_, $main::config->{'flags'}->{'target'};
 | 
        
           | 4 | rodolico | 120 |    }
 | 
        
           | 12 | rodolico | 121 |    @_ = keys %{$main::statusDB->{'node'}} unless @_;
 | 
        
           | 9 | rodolico | 122 |    while ( my $nodename = shift  ) {
 | 
        
           | 26 | rodolico | 123 |       print "Updating $nodename\n" if $main::config->{'flags'}->{'debug'} || $main::config->{'flags'}->{'verbose'};
 | 
        
           | 25 | rodolico | 124 |       my $command = &main::makeCommand($nodename, "virsh nodeinfo" );
 | 
        
           | 26 | rodolico | 125 |       if ( $main::config->{'flags'}->{'dryrun'} ) {
 | 
        
           |  |  | 126 |          push @return, $command;
 | 
        
           |  |  | 127 |       } else {  
 | 
        
           | 41 | rodolico | 128 |          my $return= `$command`;
 | 
        
           | 26 | rodolico | 129 |          print "Output of [$command] is\n" . $return if $main::config->{'flags'}->{'debug'};
 | 
        
           |  |  | 130 |          my @nodeinfo = split( "\n", $return );
 | 
        
           |  |  | 131 |          for ( my $i = 0; $i < @nodeinfo; $i++ ) {
 | 
        
           |  |  | 132 |             my ($key, $value) = split( /:\s+/, $nodeinfo[$i] );
 | 
        
           |  |  | 133 |             if ( $value =~ m/^(\d+)\s+[a-z]+$/i ) {
 | 
        
           |  |  | 134 |                $value = $1;
 | 
        
           |  |  | 135 |             }
 | 
        
           |  |  | 136 |             $key = $conversion{$key} if exists( $conversion{$key} );
 | 
        
           |  |  | 137 |             $main::statusDB->{'node'}->{$nodename}->{$key} = $value;
 | 
        
           |  |  | 138 |          } # for
 | 
        
           |  |  | 139 |          foreach my $field ( @requiredFields ) {
 | 
        
           |  |  | 140 |             $main::statusDB->{'node'}->{$nodename}->{$field} = '' 
 | 
        
           |  |  | 141 |                unless defined ( $main::statusDB->{'node'}->{$nodename}->{$field} );
 | 
        
           |  |  | 142 |          } # foreach
 | 
        
           |  |  | 143 |       }
 | 
        
           | 4 | rodolico | 144 |    } # while
 | 
        
           | 25 | rodolico | 145 |    print "main::statusDB->{'node'} state after update\n" . Dumper( $main::statusDB->{'node'} ) if $main::config->{'flags'}->{'debug'};
 | 
        
           | 12 | rodolico | 146 |    &main::writeDB();
 | 
        
           | 26 | rodolico | 147 |    return "Node has been updated\n" . join( "\n", @return ) . "\n";
 | 
        
           | 4 | rodolico | 148 | }      
 | 
        
           |  |  | 149 |   | 
        
           | 5 | rodolico | 150 |   | 
        
           |  |  | 151 | # check one or more nodes and determine which domains are running on them.
 | 
        
           |  |  | 152 | # defaults to everything in the node database, but the -t can have it run on only one
 | 
        
           |  |  | 153 | # this is the function that should be run every few minutes on one of the servers
 | 
        
           | 4 | rodolico | 154 | sub scan {
 | 
        
           | 15 | rodolico | 155 |    return &main::scan(@_);
 | 
        
           | 5 | rodolico | 156 | }
 | 
        
           | 4 | rodolico | 157 |   | 
        
           | 3 | rodolico | 158 |   | 
        
           | 5 | rodolico | 159 | # add a new node. This is the same as doing an update on a node that doesn't exist.
 | 
        
           |  |  | 160 | sub add {
 | 
        
           | 12 | rodolico | 161 |    &update( @_ );
 | 
        
           | 3 | rodolico | 162 | }
 | 
        
           |  |  | 163 |   | 
        
           | 25 | rodolico | 164 | # put node in maintenance mode
 | 
        
           |  |  | 165 | # if there are running domains on it, migrate them off first
 | 
        
           | 38 | rodolico | 166 | # If we migrate, we must then do a force scan, which locks
 | 
        
           |  |  | 167 | # the database. So, we must read the database shared first, then
 | 
        
           |  |  | 168 | # only read exclusive when we are actually changing the maintenance
 | 
        
           |  |  | 169 | # flag. The solution here is a kludge, but it at least works
 | 
        
           |  |  | 170 | # we do the exclusive read only just before we change then write
 | 
        
           | 17 | rodolico | 171 | sub maintenance {
 | 
        
           |  |  | 172 |    my ( $node, $action ) = @_;
 | 
        
           | 38 | rodolico | 173 |    &main::readDB();
 | 
        
           | 25 | rodolico | 174 |    my @return;
 | 
        
           | 17 | rodolico | 175 |    if ( $action ) {
 | 
        
           | 38 | rodolico | 176 |       print "Found action [$action] in node.pm:maintenance\n" if $main::config->{'flags'}->{'debug'} > 1;
 | 
        
           | 25 | rodolico | 177 |       if ( lc ( $action ) eq 'on' ) {
 | 
        
           | 38 | rodolico | 178 |          if ( keys %{$main::statusDB->{'nodePopulation'}->{$node}->{'running'} } ) {
 | 
        
           | 25 | rodolico | 179 |             # we've requested maintenance mode, but there are domains running on the node
 | 
        
           | 38 | rodolico | 180 |             print "Found domains on $node in node.pm:maintenance\n" if $main::config->{'flags'}->{'debug'} > 1;
 | 
        
           |  |  | 181 |             print "Trying to migrate domains off of $node before doing maintenance\n" if $main::config->{'flags'}->{'verbose'} || $main::config->{'flags'}->{'debug'} > 1;
 | 
        
           | 25 | rodolico | 182 |             push @return, &migrateAllDomains( $node, $main::config->{'flags'}->{'target'}, keys %{$main::statusDB->{'nodePopulation'}->{$node}->{'running'}} );
 | 
        
           | 38 | rodolico | 183 |             print "Finished migration attempt, forcing a scan\n" if $main::config->{'flags'}->{'debug'} > 1;
 | 
        
           |  |  | 184 |             &main::forceScan();
 | 
        
           | 25 | rodolico | 185 |          }
 | 
        
           | 38 | rodolico | 186 |          if ( keys %{ $main::statusDB->{'nodePopulation'}->{$node}->{'running'} } ) {
 | 
        
           |  |  | 187 |             print "Still found running domains on $node, aborting\n" if $main::config->{'flags'}->{'debug'} > 1;
 | 
        
           | 25 | rodolico | 188 |             push @return,  "Can not mark $node in maintenance mode with running domains";
 | 
        
           |  |  | 189 |          } else {
 | 
        
           | 38 | rodolico | 190 |             print "Marking $node as under maintenance\n" if $main::config->{'flags'}->{'verbose'} || $main::config->{'flags'}->{'debug'};
 | 
        
           |  |  | 191 |             &main::readDB(1);
 | 
        
           | 25 | rodolico | 192 |             $main::statusDB->{'node'}->{$node}->{'maintenance'} = 1;
 | 
        
           | 38 | rodolico | 193 |             &main::writeDB();
 | 
        
           | 25 | rodolico | 194 |          }
 | 
        
           |  |  | 195 |       } else {
 | 
        
           | 38 | rodolico | 196 |          print "Marking $node as Online\n" if $main::config->{'flags'}->{'verbose'} || $main::config->{'flags'}->{'debug'};
 | 
        
           |  |  | 197 |          &main::readDB(1);
 | 
        
           | 26 | rodolico | 198 |          $main::statusDB->{'node'}->{$node}->{'maintenance'} = 0;
 | 
        
           | 38 | rodolico | 199 |          &main::writeDB();
 | 
        
           | 25 | rodolico | 200 |       }
 | 
        
           | 17 | rodolico | 201 |    }
 | 
        
           | 38 | rodolico | 202 |    &main::readDB();
 | 
        
           |  |  | 203 |    print Dumper( $main::statusDB ) if $main::config->{'flags'}->{'debug'} > 2;
 | 
        
           |  |  | 204 | #   return "Maintenance set to " . ( $main::statusDB->{'node'}->{$node}->{'maintenance'} ) . "\n" .
 | 
        
           | 25 | rodolico | 205 |    return "Maintenance set to " . ( $main::statusDB->{'node'}->{$node}->{'maintenance'} ? 'On' : 'Off' ) . "\n" .
 | 
        
           |  |  | 206 |           ( @return ? join( "\n", @return ) . "\n" : '');
 | 
        
           | 17 | rodolico | 207 | }
 | 
        
           |  |  | 208 |   | 
        
           | 25 | rodolico | 209 |   | 
        
           |  |  | 210 | # migrate domains from node $from to node $to
 | 
        
           |  |  | 211 | # the rest of the stack is a list of domains to migrate
 | 
        
           |  |  | 212 | sub migrateAllDomains {
 | 
        
           |  |  | 213 |    my $from = shift;
 | 
        
           |  |  | 214 |    my $to = shift;
 | 
        
           |  |  | 215 |    print "In node.pm:migrateAllDomains, migrating\n" . join( "\n", @_ ) . "\nto $to\n"
 | 
        
           |  |  | 216 |       if ( $main::config->{'flags'}->{'debug'} );
 | 
        
           |  |  | 217 |    my @commands;
 | 
        
           | 26 | rodolico | 218 |    print "Checking for available resources on $to before migrating\n"  if $main::config->{'flags'}->{'verbose'};
 | 
        
           | 25 | rodolico | 219 |    if ( my $error = &main::validateResources( $to, @_ ) ) {
 | 
        
           |  |  | 220 |       return "We can not migrate all of the domains on $from to $to\n$error\n";
 | 
        
           |  |  | 221 |    }
 | 
        
           |  |  | 222 |    while ( my $domain = shift ) {
 | 
        
           |  |  | 223 |       push @commands, &main::migrate( $domain, $to );
 | 
        
           |  |  | 224 |    }
 | 
        
           |  |  | 225 |    chomp @commands;
 | 
        
           |  |  | 226 |    return join( "\n", @commands ) . "\n";
 | 
        
           |  |  | 227 | }
 | 
        
           |  |  | 228 |   | 
        
           |  |  | 229 |   |