Subversion Repositories havirt

Rev

Rev 19 | Rev 26 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 19 Rev 25
Line 28... Line 28...
28
package havirt;
28
package havirt;
29
 
29
 
30
use warnings;
30
use warnings;
31
use strict;  
31
use strict;  
32
 
32
 
-
 
33
BEGIN {
-
 
34
   use FindBin;
-
 
35
   use File::Spec;
-
 
36
   # use libraries from the directory this script is in
-
 
37
   use Cwd 'abs_path';
-
 
38
   use File::Basename;
-
 
39
   use lib dirname( abs_path( __FILE__ ) );
-
 
40
}
-
 
41
 
33
use Data::Dumper qw(Dumper); # Import the Dumper() subroutine
42
use Data::Dumper qw(Dumper); # Import the Dumper() subroutine
34
 
43
 
35
# define the version number
44
# define the version number
36
# see https://metacpan.org/pod/release/JPEACOCK/version-0.97/lib/version.pod
45
# see https://metacpan.org/pod/release/JPEACOCK/version-0.97/lib/version.pod
37
use version;
46
use version;
Line 40... Line 49...
40
 
49
 
41
use Exporter;
50
use Exporter;
42
 
51
 
43
our @ISA = qw( Exporter );
52
our @ISA = qw( Exporter );
44
our @EXPORT = qw( 
53
our @EXPORT = qw( 
-
 
54
                  &readDB
45
                  &readDB &writeDB
55
                  &writeDB
46
                  &report &scan
56
                  &report
-
 
57
                  &scan
47
                  &makeCommand &forceScan
58
                  &makeCommand
-
 
59
                  &forceScan
48
                  &executeAndWait
60
                  &executeAndWait
49
                  &findDomain
61
                  &findDomain
50
                  &diffArray
62
                  &diffArray
-
 
63
                  &makeConfig
-
 
64
                  &readConfig
-
 
65
                  &getAvailableResources
-
 
66
                  &resource
-
 
67
                  &validateResources
-
 
68
                  &migrate
51
                );
69
                );
52
 
70
 
53
# read a DB file (just a YAML)
71
# read a DB file (just a YAML)
54
# if $lock is set, will create a "lock" file so other processes will
72
# 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
73
# not try to write to it. Using custom code as flock is automagically
56
# release when the file is read
74
# release when the file is read
57
 
75
 
58
sub readDB {
76
sub readDB {
59
   my $lock = shift;
77
   my $lock = shift;
60
   my $lockFileName = "$main::statusDBName.lock";
78
   my $lockFileName = "$main::config->{'status db filename'}.lock";
61
   my $lockTime = 5; # maximum time to wait for lock to clear
79
   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
80
   # wait for lock to clear if it exists, if we are wanting a lock
63
   # and we have tried it for $locktime iterations
81
   # and we have tried it for $locktime iterations
64
   while ( $lock && -f $lockFileName && $lockTime-- ) {
82
   while ( $lock && -f $lockFileName && $lockTime-- ) {
65
      sleep 1; # wait one second, then try again
83
      sleep 1; # wait one second, then try again
66
   }
84
   }
67
   if ( $lock ) {
85
   if ( $lock ) {
68
      die "Something has $main::statusDBName locked, aborting\n" if -f $lockFileName;
86
      die "Something has $main::config->{'status db filename'} locked, aborting\n" if -f $lockFileName;
69
      `touch $lockFileName`;
87
      `touch $lockFileName`;
70
   }
88
   }
71
   my $yaml = YAML::Tiny->new( {} );
89
   my $yaml = YAML::Tiny->new( {} );
72
   if ( -f $main::statusDBName ) {
90
   if ( -f $main::config->{'status db filename'} ) {
73
      $yaml = YAML::Tiny->read( $main::statusDBName );
91
      $yaml = YAML::Tiny->read( $main::config->{'status db filename'} );
74
   }
92
   }
75
   $main::statusDB = $yaml->[0];
93
   $main::statusDB = $yaml->[0];
76
}
94
}
77
 
95
 
78
sub writeDB {
96
sub writeDB {
79
   my $yaml = YAML::Tiny->new( $main::statusDB );
97
   my $yaml = YAML::Tiny->new( $main::statusDB );
80
   $yaml->write( $main::statusDBName );
98
   $yaml->write( $main::config->{'status db filename'} );
81
   unlink "$main::statusDBName.lock" if -f "$main::statusDBName.lock"; # release any lock we might have on it
99
   unlink "$main::config->{'status db filename'}.lock" if -f "$main::config->{'status db filename'}.lock"; # release any lock we might have on it
82
}
100
}
83
 
101
 
84
sub report {
102
sub report {
85
   if ( $main::reportFormat eq 'tsv' ) {
103
   if ( $main::config->{'flags'}->{'format'} eq 'tsv' ) {
86
      return &report_tsv( @_ );
104
      return &report_tsv( @_ );
87
   } else {
105
   } else {
88
      return &report_screen( @_ );
106
      return &report_screen( @_ );
89
   }
107
   }
90
}
108
}
Line 130... Line 148...
130
}
148
}
131
 
149
 
132
# scans a node to determine which domains are running on it
150
# scans a node to determine which domains are running on it
133
sub getDomainsOnNode {
151
sub getDomainsOnNode {
134
   my $node = shift;
152
   my $node = shift;
-
 
153
   my $command = &main::makeCommand( $node, 'virsh list' );
-
 
154
   print "havirt.pm:getDomainsOnNode, command is $command\n" if $main::config->{'flags'}->{'debug'} > 2;
135
   my @nodeList = grep { /^\s*\d/ } `ssh $node 'virsh list'`;
155
   my @nodeList = grep { /^\s*\d/ } `$command`;
136
   for ( my $i = 0; $i < @nodeList; $i++ ) {
156
   for ( my $i = 0; $i < @nodeList; $i++ ) {
137
      if ( $nodeList[$i] =~ m/\s*\d+\s*([^ ]+)/ ) {
157
      if ( $nodeList[$i] =~ m/\s*\d+\s*([^ ]+)/ ) {
138
         $nodeList[$i] = $1;
158
         $nodeList[$i] = $1;
139
      }
159
      }
140
   }
160
   }
Line 153... Line 173...
153
   my @node = @_;
173
   my @node = @_;
154
   my $foundNode = '';
174
   my $foundNode = '';
155
   &readDB();
175
   &readDB();
156
   unless ( @node ) {
176
   unless ( @node ) {
157
      @node = keys %{$main::statusDB->{'node'} };
177
      @node = keys %{$main::statusDB->{'node'} };
158
      print "findDomain, nodes = " . join( "\t", @node ) . "\n" if $main::DEBUG > 1;
178
      print "findDomain, nodes = " . join( "\t", @node ) . "\n" if $main::config->{'flags'}->{'debug'} > 1;
159
   }
179
   }
160
   foreach my $thisNode ( @node ) {
180
   foreach my $thisNode ( @node ) {
161
      my $output = `ssh $thisNode 'virsh list'`;
181
      my $command = &main::makeCommand( $thisNode, 'virsh list' );
-
 
182
      my $output = `$command`;
162
      print "findDomain, $thisNode list =\n" . $output . "\n" if $main::DEBUG > 1;;
183
      print "findDomain, $thisNode list =\n" . $output . "\n" if $main::config->{'flags'}->{'debug'} > 1;;
163
      return $thisNode if ( $output =~ m/$domainName/ );
184
      return $thisNode if ( $output =~ m/$domainName/ );
164
   }
185
   }
165
   return '';
186
   return '';
166
}
187
}
167
 
188
 
168
# check one or more nodes and determine which domains are running on them.
189
# 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
190
# 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
191
# this is the function that should be run every few minutes on one of the servers
171
sub scan {
192
sub scan {
-
 
193
   my @targets = @_;
172
   if ( -f $main::lastScanFileName && ! $main::force ) {
194
   if ( -f $main::config->{'last scan filename'} && ! $main::config->{'flags'}->{'yes'} ) {
173
      my $lastScan = time - ( stat( $main::lastScanFileName ) ) [9];
195
      my $lastScan = time - ( stat( $main::config->{'last scan filename'} ) ) [9];
174
      return "Scan was run $lastScan seconds ago\n" unless $lastScan > $main::minScanTimes;
196
      return "Scan was run $lastScan seconds ago\n" unless $lastScan > $main::config->{'minum scan time'};
175
   }
197
   }
176
   `touch $main::lastScanFileName`;
198
   `touch $main::config->{'last scan filename'}`;
177
   &main::readDB(1);
199
   &main::readDB(1);
178
   print Dumper( $main::statusDB->{'nodePopulation'} ) if $main::DEBUG > 2;
200
   print Dumper( $main::statusDB->{'nodePopulation'} ) if $main::config->{'flags'}->{'debug'} > 2;
179
   my @targets;
-
 
180
   if ( $main::targetNode ) {
201
   if ( $main::config->{'flags'}->{'target'} ) {
181
      push @targets, $main::targetNode;
202
      push @targets, $main::config->{'flags'}->{'target'};
182
   } else {
-
 
183
      @targets = keys %{$main::statusDB->{'node'}};
-
 
184
   }
203
   }
-
 
204
   @targets = keys %{$main::statusDB->{'node'}} unless @targets;
185
   print "Scanning " . join( "\n", @targets ) . "\n" if $main::DEBUG;
205
   print "Scanning " . join( "\n", @targets ) . "\n" if $main::config->{'flags'}->{'debug'};
186
   foreach my $node (@targets) {
206
   foreach my $node (@targets) {
187
      $main::statusDB->{'nodePopulation'}->{$node}->{'running'} = &getDomainsOnNode( $node );
207
      $main::statusDB->{'nodePopulation'}->{$node}->{'running'} = &getDomainsOnNode( $node );
188
      $main::statusDB->{'nodePopulation'}->{$node}->{'lastchecked'} = time;
208
      $main::statusDB->{'nodePopulation'}->{$node}->{'lastchecked'} = time;
189
      foreach my $domain ( keys %{$main::statusDB->{'nodePopulation'}->{$node}->{'running'}} ) {
209
      foreach my $domain ( keys %{$main::statusDB->{'nodePopulation'}->{$node}->{'running'}} ) {
190
         # make sure there is an entry for all of these domains
210
         # make sure there is an entry for all of these domains
191
         $main::statusDB->{'virt'}->{$domain} = {} unless exists( $main::statusDB->{'virt'}->{$domain} );
211
         $main::statusDB->{'virt'}->{$domain} = {} unless exists( $main::statusDB->{'virt'}->{$domain} );
192
      }
212
      }
193
      print Dumper( $main::statusDB->{'nodePopulation'}->{$node} ) if $main::DEBUG > 2;
213
      print Dumper( $main::statusDB->{'nodePopulation'}->{$node} ) if $main::config->{'flags'}->{'debug'} > 2;
194
   }
214
   }
195
   &main::writeDB();
215
   &main::writeDB();
196
   return "Node(s) updated\n";
216
   return "Node(s) updated\n";
197
}
217
}
198
 
218
 
199
# makes the command that will be run on a node
219
# makes the command that will be run on a node
200
# Created as a sub so we can change format easily
220
# Created as a sub so we can change format easily
-
 
221
# if node is the node we're on, we don't need to do a remote call
-
 
222
# if node is null, we'll assume we do the command here
-
 
223
# otherwise, we'll do an ssh to the node and run the command there
201
sub makeCommand {
224
sub makeCommand {
202
   my ( $node, $command ) = @_;
225
   my ( $node, $command ) = @_;
-
 
226
   my $me = `hostname`;
-
 
227
   chomp $me;
-
 
228
   if ( ! $node || $node eq $me ) {
-
 
229
      return $command;
-
 
230
   } else {
203
   return "ssh $node '$command'";
231
      return "ssh $node '$command'";
-
 
232
   }
204
}
233
}
205
 
234
 
206
# force a node scan, even if time has not expired
235
# force a node scan, even if time has not expired
207
sub forceScan {
236
sub forceScan {
208
   my $save = $main::force;
237
   my $save = $main::config->{'flags'}->{'yes'};
209
   $main::force = 1;
238
   $main::config->{'flags'}->{'yes'} = 1;
210
   &main::scan();
239
   &main::scan();
211
   $main::force = $save;
240
   $main::config->{'flags'}->{'yes'} = $save;
212
}
241
}
213
 
242
 
214
 
243
 
215
# executes command $command, then repeatedly runs virsh list
244
# executes command $command, then repeatedly runs virsh list
216
# on $scanNode, grep'ing for $scanDomain
245
# on $scanNode, grep'ing for $scanDomain
217
# $condition is 1 (true) or 0 (false)
246
# $condition is 1 (true) or 0 (false)
218
sub executeAndWait {
247
sub executeAndWait {
219
   my ( $command, $scanNode, $scanDomain, $condition ) = @_;
248
   my ( $command, $scanNode, $scanDomain, $condition ) = @_;
220
   my $waitSeconds = 5; # number of seconds to wait before checking again
249
   my $waitSeconds = 5; # number of seconds to wait before checking again
221
   my $maxIterations = 60 / $waitSeconds; # maximum number of tries
250
   my $maxIterations = 60 / $waitSeconds; # maximum number of tries
222
   print "Running [$command], then waiting $waitSeconds to check if complete\n" if $main::DEBUG;
251
   print "Running [$command], then waiting $waitSeconds to check if complete\n" if $main::config->{'flags'}->{'debug'};
223
   `$command`;
252
   `$command`;
224
   my $waitCommand = &makeCommand( $scanNode, "virsh list | grep $scanDomain" );
253
   my $waitCommand = &makeCommand( $scanNode, "virsh list | grep $scanDomain" );
225
   my $output = '';
254
   my $output = '';
226
   do {
255
   do {
227
      return 0 unless ( $maxIterations-- ); # we've waited too long, so probably not working
256
      return 0 unless ( $maxIterations-- ); # we've waited too long, so probably not working
228
      print '. ';
257
      print '. ';
229
      sleep 5;
258
      sleep 1;
230
      $output = `$waitCommand`;
259
      $output = `$waitCommand`;
231
      print "[$waitCommand] returned [$output]\n" if $main::DEBUG > 1;
260
      print "[$waitCommand] returned [$output]\n" if $main::config->{'flags'}->{'debug'} > 1;
232
   } until ( $condition ? $output : !$output );
261
   } until ( $condition ? $output : !$output );
233
   return 1; # made it successful
262
   return 1; # made it successful
234
} 
263
} 
235
 
264
 
236
# find the differences between two arrays (passed by reference)
265
# find the differences between two arrays (passed by reference)
Line 257... Line 286...
257
         $j++;
286
         $j++;
258
      }
287
      }
259
   }
288
   }
260
   return \@result;
289
   return \@result;
261
}
290
}
-
 
291
 
-
 
292
 
-
 
293
# create a config file if one does not exist
-
 
294
sub makeConfig {
-
 
295
   my ( $config, $filename ) = @_;
-
 
296
   $config->{'script dir'} = $FindBin::RealBin;
-
 
297
   $config->{'script name'} = $FindBin::Script;
-
 
298
   $config->{'db dir'} = $config->{'script dir'} . '/var';
-
 
299
   $config->{'conf dir'} = $config->{'script dir'} . '/conf';
-
 
300
   $config->{'status db filename'} = $config->{'db dir'} . '/status.yaml';
-
 
301
   $config->{'last scan filename'} = $config->{'script dir'} . '/var/lastscan';
-
 
302
   $config->{'minum scan time'} = 5 * 60; # five minutes
-
 
303
   $config->{'node reserved memory'} = 8 * 1024 * 1024; # 8 gigabytes
-
 
304
   $config->{'node reserved vcpu' } = 0; # turn off reserved vcpu
-
 
305
   $config->{'flags'}->{'format'} = 'screen';
-
 
306
   $config->{'flags'}->{'yes'} = 0;
-
 
307
   $config->{'flags'}->{'quiet'} = 0;
-
 
308
   $config->{'flags'}->{'target'} = '';
-
 
309
   $config->{'flags'}->{'dryrun'} = 1;
-
 
310
   $config->{'flags'}->{'debug'} = 0;
-
 
311
   $config->{'flags'}->{'help'} = 0;
-
 
312
   $config->{'flags'}->{'version'} = 0;
-
 
313
   my $yaml = YAML::Tiny->new( $config );
-
 
314
   $yaml->write( $filename );
-
 
315
}
-
 
316
 
-
 
317
# read the config file and return it
-
 
318
sub readConfig {
-
 
319
   my $filename = shift;
-
 
320
   my $yaml = YAML::Tiny->new( {} );
-
 
321
   if ( -f $filename ) {
-
 
322
      $yaml = YAML::Tiny->read( $filename );
-
 
323
   }
-
 
324
   return $yaml->[0];
-
 
325
}
-
 
326
 
-
 
327
# find available resource on a node
-
 
328
sub resource {
-
 
329
   my $node = shift;
-
 
330
   die "Can not find node $node in havirt.pm:resource\n"
-
 
331
      unless $main::statusDB->{'node'}->{$node};
-
 
332
   my $return = {
-
 
333
      'memory' => 0,
-
 
334
      'cpu_count' => 0
-
 
335
      };
-
 
336
   foreach my $key ( keys %$return ) {
-
 
337
      $return->{$key} = $main::statusDB->{'node'}->{$node}->{$key}
-
 
338
         if defined $main::statusDB->{'node'}->{$node}->{$key};
-
 
339
   } # foreach
-
 
340
   return $return;
-
 
341
}
-
 
342
 
-
 
343
sub getAvailableResources {
-
 
344
   my $node = shift;
-
 
345
   &readDB();
-
 
346
   die "Can not find node $node in havirt.pm:resource\n"
-
 
347
      unless $main::statusDB->{'node'}->{$node};
-
 
348
   my $totalResources = &resource( $node );
-
 
349
   print Dumper( $totalResources ) if $main::config->{'flags'}->{'debug'};
-
 
350
   foreach my $domain ( keys %{ $main::statusDB->{'nodePopulation'}->{$node}->{'running'} } ) {
-
 
351
      $totalResources->{'memory'} -= $main::statusDB->{'virt'}->{$domain}->{'memory'};
-
 
352
      $totalResources->{'cpu_count'} -= $main::statusDB->{'virt'}->{$domain}->{'vcpu'};
-
 
353
   }
-
 
354
   return $totalResources;
-
 
355
}
-
 
356
 
-
 
357
# validate that node has enough resources for the domains which occupy the
-
 
358
# remainder of the stack
-
 
359
# returns 0 on success, or one or more error messages in a string on failure
-
 
360
sub validateResources {
-
 
361
   my $node = shift;
-
 
362
   &readDB();
-
 
363
   my @return;
-
 
364
   my $nodeResources = &getAvailableResources( $node );
-
 
365
   print "In havirt.pm:validateResources, checking if enough room on $node for\n" . join( "\n", @_ ) . "\n"
-
 
366
      if ( $main::config->{'flags'}->{'debug'} );
-
 
367
   # subtract the reserved memory from the node
-
 
368
   $nodeResources->{'memory'} -= $main::config->{'node reserved memory'};
-
 
369
   $nodeResources->{'cpu_count'} -= $main::config->{'node reserved vcpu'} if $main::config->{'node reserved vcpu'};
-
 
370
   while ( my $domain = shift ) {
-
 
371
      $nodeResources->{'memory'} -= $main::statusDB->{'virt'}->{$domain}->{'memory'};
-
 
372
      $nodeResources->{'cpu_count'} -= $main::statusDB->{'virt'}->{$domain}->{'vcpu'};
-
 
373
   }
-
 
374
   print "In havirt.pm:validateResources, $node will have $nodeResources->{memory} memory and $nodeResources->{cpu_count} vcpu's after task\n"
-
 
375
      if ( $main::config->{'flags'}->{'debug'} > 1 );
-
 
376
   
-
 
377
   push @return, "This action would result in memory of $nodeResources->{memory}" if $nodeResources->{'memory'} <= 0;
-
 
378
   push @return, "This action would result in virtual cpu count of $nodeResources->{cpu_count}" if $nodeResources->{'cpu_count'} <= 0 && $main::config->{'flags'}->{'node reserved vcpu'};
-
 
379
   return @return ? join( "\n", @return ) . "\n" : 0;
-
 
380
}
-
 
381
 
-
 
382
# migrate domain from current node it is on to $target
-
 
383
sub migrate {
-
 
384
   my ( $virt, $target ) = @_;
-
 
385
   my $return;
-
 
386
   my $node;
-
 
387
   # these are replaced by the safer findDomain
-
 
388
   #&main::forceScan();
-
 
389
   #&main::readDB();
-
 
390
   $node = &main::findDomain( $virt );
-
 
391
   print Dumper( $main::statusDB->{'nodePopulation'} ) if $main::config->{'flags'}->{'debug'} > 2;
-
 
392
   die "I can not find $virt on any node\n" unless $node;
-
 
393
   die "Domain $virt in maintenance mode, can not migrate it\n" if $main::statusDB->{'virt'}->{$virt}->{'maintenance'};
-
 
394
   die "Node $target in maintenance mode, can not migrate anything to it\n" if $main::statusDB->{'node'}->{$target}->{'maintenance'};
-
 
395
   die "$virt already on $target\n" if $target eq $node;
-
 
396
   my $command = &main::makeCommand( $node, "virsh migrate --live --persistent --verbose  $virt qemu+ssh://$target/system" );
-
 
397
   if ( $main::config->{'flags'}->{'yes'} ) { # they want us to actually do it
-
 
398
      $return = ( &main::executeAndWait( $command, $node, $virt, 0 ) ? 'Success' : 'Time Out waiting for shutdown');
-
 
399
      &main::forceScan();
-
 
400
   } else {
-
 
401
      $return = $command;
-
 
402
   }
-
 
403
   return "$return\n";
-
 
404
}
-
 
405