# HG changeset patch # User cin # Date 1479162789 -10800 # Node ID cd5df456ee8403848ad903e6c500722d2f600c3d Initial working version of minions.pl ans sysyemd units diff -r 000000000000 -r cd5df456ee84 .includepath --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.includepath Tue Nov 15 01:33:09 2016 +0300 @@ -0,0 +1,5 @@ + + + + + diff -r 000000000000 -r cd5df456ee84 .project --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.project Tue Nov 15 01:33:09 2016 +0300 @@ -0,0 +1,17 @@ + + + Minions + + + + + + org.epic.perleditor.perlbuilder + + + + + + org.epic.perleditor.perlnature + + diff -r 000000000000 -r cd5df456ee84 Changes --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Changes Tue Nov 15 01:33:09 2016 +0300 @@ -0,0 +1,5 @@ +Revision history for Minions + +0.01 Date/time + First version, released on an unsuspecting world. + diff -r 000000000000 -r cd5df456ee84 README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README Tue Nov 15 01:33:09 2016 +0300 @@ -0,0 +1,52 @@ +Minions + +The set of tools to manipulate libvirt domains + + +INSTALLATION + TODO + +USING + TODO + + +LICENSE AND COPYRIGHT + +Copyright (C) 2016 "sergey" + +This program is free software; you can redistribute it and/or modify it +under the terms of the the Artistic License (2.0). You may obtain a +copy of the full license at: + +L + +Any use, modification, and distribution of the Standard or Modified +Versions is governed by this Artistic License. By using, modifying or +distributing the Package, you accept this license. Do not use, modify, +or distribute the Package, if you do not accept this license. + +If your Modified Version has been derived from a Modified Version made +by someone other than you, you are nevertheless required to ensure that +your Modified Version complies with the requirements of this license. + +This license does not grant you the right to use any trademark, service +mark, tradename, or logo of the Copyright Holder. + +This license includes the non-exclusive, worldwide, free-of-charge +patent license to make, have made, use, offer to sell, sell, import and +otherwise transfer the Package with respect to any patent claims +licensable by the Copyright Holder that are necessarily infringed by the +Package. If you institute patent litigation (including a cross-claim or +counterclaim) against any party alleging that the Package constitutes +direct or contributory patent infringement, then this Artistic License +to you shall terminate on the date that such litigation is filed. + +Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER +AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. +THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY +YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR +CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR +CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff -r 000000000000 -r cd5df456ee84 bin/minions.pl --- /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 <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}; +} diff -r 000000000000 -r cd5df456ee84 etc/minions/hive.yaml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/etc/minions/hive.yaml Tue Nov 15 01:33:09 2016 +0300 @@ -0,0 +1,9 @@ +vmms: + qemu: + uri: qemu:///system + stop: suspend + lxc: + uri: lxc:/// + stop: shutdown +timeout: 100 +parallel: 4 \ No newline at end of file diff -r 000000000000 -r cd5df456ee84 usr/lib/systemd/system/libvirt-minion@.service --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/usr/lib/systemd/system/libvirt-minion@.service Tue Nov 15 01:33:09 2016 +0300 @@ -0,0 +1,18 @@ +[Unit] +Description=Suspend/Resume running libvirt %I domain +Requires=libvirtd.service +After=network.target +After=time-sync.target +After=libvirtd.service +Before=libvirt-minions.service + +[Service] +ExecStart=/usr/lib/minions/minions.pl start %I +ExecStop=/usr/lib/minions/minions.pl stop %I +Type=oneshot +RemainAfterExit=yes +StandardOutput=journal+console +TimeoutStopSec=0 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff -r 000000000000 -r cd5df456ee84 usr/lib/systemd/system/libvirt-minions.service --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/usr/lib/systemd/system/libvirt-minions.service Tue Nov 15 01:33:09 2016 +0300 @@ -0,0 +1,17 @@ +[Unit] +Description=Suspend/Resume running libvirt guests +Requires=libvirtd.service +After=network.target +After=time-sync.target +After=libvirtd.service + +[Service] +ExecStart=/usr/lib/minions/minions.pl start +ExecStop=/usr/lib/minions/minions.pl stop +Type=oneshot +RemainAfterExit=yes +StandardOutput=journal+console +TimeoutStopSec=0 + +[Install] +WantedBy=multi-user.target \ No newline at end of file