Mercurial > pub > Minions
diff bin/minions.pl @ 0:cd5df456ee84
Initial working version of minions.pl ans sysyemd units
author | cin |
---|---|
date | Tue, 15 Nov 2016 01:33:09 +0300 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/minions.pl Tue Nov 15 01:33:09 2016 +0300 @@ -0,0 +1,379 @@ +#!/usr/bin/perl -T +use strict; +use warnings; + +package Hive; + +package main; +use Pod::Usage; +use Getopt::Long qw(:config auto_help); +use Sys::Virt; +use Sys::Virt::Domain; + +our $VERSION = 0.1; +our $CONFIG_FILE = '/etc/minions/hive.yaml'; +our $STATE_FILE = '/var/lib/minions/session.yaml'; +our $TIMEOUT = 300; +our $PARALLEL = 0; +our $RETRY_INTERVAL = 60; + +my $tasks = 2; +my @pending; + +my @uri = qw(qemu:///system lxc:///); + +my %commands = ( + stop => \&doStop, + start => \&doStart, + help => \&doHelp +); + +my $cmd = shift @ARGV; + +&{ $commands{ lc( $cmd || '' ) } || \&doHelp }(@ARGV); + +exit 0; + +sub doHelp { + print <<END_HELP; +Minions v.$VERSION manages libvirt domains... + stop [domain[ stop-method]] + Stops the specified libvirt domain with the specified method. If the + domain isn't specified then stops all domains on all configured + connections and saves the list of stopped domains to + /var/lib/minions/session.yaml file. + + start [domain] + Starts the specified domain, if the domain is ommited restores the + previous session from /var/lib/minions/session.yaml and deletes this + file. + help + Prints this help + + domain must be written in format {connection}.{domain} where {connection} is + the one of configured connections from /etc/minions/hive.yaml and {domain} + is the name of the libvirt domain. +END_HELP +} + +sub doStart { + my $hive = Hive->new($CONFIG_FILE); + + if ( @_ > 0 ) { + if ( my ( $cn, $dn ) = ( $_[0] =~ m/(\w+)\.(\w+)/ ) ) { + $hive->startDomain( $cn, $dn ); + } + else { + die "Invalid parameter: {connection}.{domain} format is required"; + } + } + else { + $hive->start(); + } + $hive->waitPending(); +} + +sub doStop { + my $hive = Hive->new($CONFIG_FILE); + + if ( @_ > 0 ) { + if ( my ( $cn, $dn ) = ( $_[0] =~ m/(\w+)\.(\w+)/ ) ) { + $hive->stopDomain( $cn, $dn ); + } + else { + die "Invalid parameter: {connection}.{domain} format is required"; + } + } + else { + $hive->stop(); + } + + $hive->waitPending(); +} + +package Hive; +use fields qw(config vmms _pending); +use YAML::XS qw(DumpFile LoadFile Dump); +use Sys::Virt; +use Sys::Virt::Domain; +use File::Spec; + +BEGIN { + no strict 'refs'; + *{ __PACKAGE__ . "::$_" } = \*{"Sys::Virt::Domain::$_"} + for qw(STATE_SHUTOFF LIST_PERSISTENT LIST_ACTIVE); +} + +sub new { + my Hive $this = fields::new(shift); + $this->init(@_); + return $this; +} + +sub init { + my Hive $this = shift; + my $file = shift; + + my $config = $this->{config} = LoadFile($file); + + while ( my ( $name, $info ) = each %{ $config->{vmms} || {} } ) { + eval { + die "Invalid connection info $name" + unless ref($info) and $info->{uri}; + + $this->trace_info("Connection '$name': $info->{uri}"); + $this->{vmms}{$name}{stop} = lc( $info->{stop} || 'shutdown' ); + + die "Unsupported stop method: $info->{stop}" + unless grep $_ eq $this->{vmms}{$name}{stop}, + qw(suspend shutdown); + + $this->{vmms}{$name}{instance} = + Sys::Virt->new( uri => $info->{uri} ); + }; + $this->trace_error("Failed to connect '$name': $@") if $@; + } +} + +sub stop { + my Hive $this = shift; + + my @pending; + + my %stopped; + + while ( my ( $name, $vmm ) = each %{ $this->{vmms} || {} } ) { + next unless $vmm->{instance}; + + $this->trace_info("Do $vmm->{stop} for all domains on $name"); + + my @domains = + $vmm->{instance}->list_all_domains( LIST_PERSISTENT | LIST_ACTIVE ); + $stopped{$name} = [ map $_->get_name(), @domains ]; + + $this->trace_info( "\t-" . $_->get_name() ) + and $this->_safeStop( $_, $vmm->{stop} ) + for @domains; + } + + my $state = $this->{config}{sate_file} || $STATE_FILE; + + my $dir = ( File::Spec->splitpath($state) )[1]; + mkdir $dir unless -e $dir; + + DumpFile( $state, \%stopped ); +} + +sub start { + my Hive $this = shift; + + my $state = $this->{config}{sate_file} || $STATE_FILE; + my $session = -f $state ? LoadFile($state) : {}; + + while ( my ( $name, $machines ) = each %{$session} ) { + my $vmm = $this->{vmms}{$name} + or $this->trace_error( + "Failed to resotre session for '$name': no such connection") + and next; + + $this->trace_info("Restoring domains on '$name'"); + for my $m ( @{ $machines || [] } ) { + my $d = $vmm->{instance}->get_domain_by_name($m) + or $this->trace_error("\t-$m not found") + and next; + + eval { + $this->trace_info("\t-$m"); + $this->_safeStart($d); + }; + $this->trace_error("$@") if $@; + } + } + + unlink $state if -f $state; +} + +sub startDomain { + my Hive $this = shift; + my ( $cn, $dn ) = @_; + + $this->trace_info("Start $cn.$dn"); + my $con = $this->{vmms}{$cn} + or die "Connection '$cn' doesn't exists"; + + my $dom = $con->{instance}->get_domain_by_name($dn) + or die "Domain $dn isn't found in '$cn'"; + + return $this->_safeStart($dom); +} + +sub _resolveDomain { + my Hive $this = shift; + my ( $cn, $dn ) = @_; + + my $con = $this->{vmms}{$cn} + or die "Connection '$cn' doesn't exists"; + + my $dom = $con->{instance}->get_domain_by_name($dn) + or die "Domain $dn isn't found in '$cn'"; + return $dom; +} + +sub _safeStart { + my Hive $this = shift; + my $dom = shift; + + eval { + unless ( $dom->is_active() ) { + $dom->create(); + } + else { + $this->trace_info( + "The domain " . $dom->get_name() . " already active" ); + } + }; + if ($@) { + die "$@" unless $dom->is_active(); + $this->trace_info("$@"); + } +} + +sub stopDomain { + my Hive $this = shift; + my ( $cn, $dn, $method ) = @_; + + $this->trace_info("Stop $cn.$dn"); + my $dom = $this->_resolveDomain( $cn, $dn ); + + # if stop method is not specified use the default one + $method = $this->{vmms}{$cn}{stop} || 'shutdown' + unless $method; + + return $this->_safeStop( $dom, $method ); +} + +sub _safeStop { + my Hive $this = shift; + my ( $dom, $method ) = @_; + + eval { + if ( $method eq 'shutdown' ) { + push @{ $this->{_pending} }, Shutdown->new($dom); + } + elsif ( $method eq 'suspend' ) { + $dom->managed_save(); + } + }; + $this->trace_error( "failed to $method " . $dom->get_name() . ": $@" ) + if $@; +} + +sub waitPending { + my Hive $this = shift; + + my $timeout = $this->{config}{timeout} || $TIMEOUT; + my $parallel = $this->{config}{parallel} || $PARALLEL; + my $retry = $this->{config}{retryInterval} || $RETRY_INTERVAL; + + my @pending = @{ $this->{_pending} || [] }; + $this->{_pending} = []; + + my $spins = 0; + + $this->trace_info("Waiting for operations to complete") + if @pending; + + while (@pending) { + my @queue; + my $slots = $parallel; + $spins++; + foreach my $task (@pending) { + my $name = $task->getName(); + my $duration = $task->getDuration(); + + if ( $task->isComplete() ) { + $this->trace_info("\t- $name stopped in $duration s"); + } + elsif ( $duration > $timeout ) { + $this->trace_info( + "\t- $name destroyed due timeout after $duration s"); + $task->terminate(); + } + else { + $task->start() + if not $task->isStarted() + and ( $parallel == 0 || --$slots >= 0 ); + $this->trace_info("\tretry $name after $duration s") + and $task->signal() + if $retry and $spins % $retry == 0; + push @queue, $task; + } + } + sleep(1) if @pending = @queue; + } +} + +sub trace_info { + shift; + print @_, "\n"; +} + +sub trace_error { + shift; + print STDERR @_, "\n"; +} + +package Shutdown; +use fields qw(_domain _startTime _started); + +BEGIN { + no strict 'refs'; + *{ __PACKAGE__ . "::$_" } = \*{"Sys::Virt::Domain::$_"} + for qw(STATE_SHUTOFF LIST_PERSISTENT LIST_ACTIVE); +} + +sub new { + my Shutdown $self = fields::new(shift); + $self->{_domain} = shift; + return $self; +} + +sub isComplete { + my Shutdown $this = shift; + return $this->{_domain}->get_info()->{state} == STATE_SHUTOFF; +} + +sub getName { + my Shutdown $this = shift; + return $this->{_domain}->get_name(); +} + +sub getDuration { + my Shutdown $this = shift; + return ( $this->{_startTime} ? time - $this->{_startTime} : 0 ); +} + +sub terminate { + my Shutdown $this = shift; + return eval { $this->{_domain}->destroy() }; +} + +sub signal { + my Shutdown $this = shift; + + eval { $this->{_domain}->shutdown() }; +} + +sub start { + my Shutdown $this = shift; + + $this->{_started} = 1; + $this->{_startTime} = time; + + $this->signal(); +} + +sub isStarted { + my Shutdown $this = shift; + return $this->{_started}; +}