[pve-devel] [PATCH] add qemu guest agent client

Alexandre Derumier aderumier at odiso.com
Tue Sep 4 09:04:35 CEST 2012


This implement qemu guest agent client.

I Have take the qmp client code and try to adapt it.

We cannot pass id in the quest agent client,so I always execute

{ "execute": "guest-sync", "arguments": { "id": sequenceid } }
before each command

exemple:
sending commands
---------------
{ "execute": "guest-sync", "arguments": { "id": 123456 } }
{"execute":"guest-ping"}

parse result
------------
{ "return": 123456}\n{"return": {}}

I don't know if it's the right way, so please review code.

Signed-off-by: Alexandre Derumier <aderumier at odiso.com>
---
 PVE/Makefile      |    5 +-
 PVE/QGAClient.pm  |  279 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 PVE/QemuServer.pm |   38 +++++++
 3 files changed, 320 insertions(+), 2 deletions(-)
 create mode 100755 PVE/QGAClient.pm

diff --git a/PVE/Makefile b/PVE/Makefile
index 232c881..8a70aeb 100644
--- a/PVE/Makefile
+++ b/PVE/Makefile
@@ -1,11 +1,12 @@
 PERLSOURCE = 			\
 	QemuServer.pm		\
 	QemuMigrate.pm		\
-	QMPClient.pm
+	QMPClient.pm		\
+	QGAClient.pm
 
 .PHONY: install
 install:
 	install -d ${DESTDIR}${PERLDIR}/PVE
 	install -m 0644 ${PERLSOURCE} ${DESTDIR}${PERLDIR}/PVE/
 	make -C VZDump install
-	make -C API2 install
\ No newline at end of file
+	make -C API2 install
diff --git a/PVE/QGAClient.pm b/PVE/QGAClient.pm
new file mode 100755
index 0000000..8880b5a
--- /dev/null
+++ b/PVE/QGAClient.pm
@@ -0,0 +1,279 @@
+package PVE::QGAClient;
+
+use strict;
+use PVE::QemuServer;
+use IO::Multiplex;
+use POSIX qw(EINTR EAGAIN);
+use JSON;
+use Time::HiRes qw(usleep gettimeofday tv_interval);
+
+use Data::Dumper;
+
+# Qemu Guest Agent client.
+#
+# This implementation uses IO::Multiplex (libio-multiplex-perl) and
+# allows you to issue quest agent commands to different VMs in parallel.
+
+# Note: kvm can onyl handle 1 connection, so we close connections asap
+
+sub new {
+    my ($class, $eventcb) = @_;
+
+    my $mux = new IO::Multiplex;
+
+    my $self = bless {
+	mux => $mux,
+	fhs => {}, # $vmid => fh
+	fhs_lookup => {}, # $fh => $vmid
+	queue => {},
+	current => {},
+	errors => {},
+    }, $class;
+
+    $self->{eventcb} = $eventcb if $eventcb;
+
+    $mux->set_callback_object($self);
+
+    return $self;
+}
+
+# add a single command to the queue for later execution 
+# with queue_execute()
+sub queue_cmd {
+    my ($self, $vmid, $callback, $execute, %params) = @_;
+
+    my $cmd = {};
+    $cmd->{execute} = $execute;
+    $cmd->{arguments} = \%params;
+    $cmd->{callback} = $callback;
+
+    push @{$self->{queue}->{$vmid}}, $cmd;
+}
+
+# execute a single command
+sub cmd {
+    my ($self, $vmid, $cmd, $timeout) = @_;
+
+    my $result;
+
+    my $callback = sub {
+	my ($vmid, $resp) = @_;
+	$result = $resp->{'return'};
+    };
+
+    die "no command specified" if !($cmd &&  $cmd->{execute});
+
+    $cmd->{callback} = $callback;
+    $cmd->{arguments} = {} if !defined($cmd->{arguments});
+
+    $self->{queue}->{$vmid} = [ $cmd ];
+
+    if (!$timeout) {
+	    $timeout = 3; # default
+    }
+
+    $self->queue_execute($timeout);
+
+    my $cmdstr = $cmd->{execute} || '';
+    die "VM $vmid qmp command '$cmdstr' failed - $self->{errors}->{$vmid}"
+	if defined($self->{errors}->{$vmid});    
+
+    return $result;
+};
+
+my $cmdid_seq = 0;
+my $next_cmdid = sub {
+    $cmdid_seq++;
+    return "$cmdid_seq";
+};
+
+my $close_connection = sub {
+    my ($self, $vmid) = @_;
+	    
+    my $fh = $self->{fhs}->{$vmid};
+    return if !$fh;
+ 
+    delete $self->{fhs}->{$vmid};
+    delete $self->{fhs_lookup}->{$fh};
+
+    $self->{mux}->close($fh);
+};
+
+my $open_connection = sub {
+    my ($self, $vmid) = @_;
+
+    my $sname = PVE::QemuServer::qga_socket($vmid);
+
+    my $fh;
+    my $starttime = [gettimeofday];
+    my $count = 0;
+    for (;;) {
+	$count++;
+	$fh = IO::Socket::UNIX->new(Peer => $sname, Blocking => 0, Timeout => 1);
+	last if $fh;
+	if ($! != EINTR && $! != EAGAIN) {
+	    die "unable to connect to VM $vmid socket - $!\n";
+	}
+	my $elapsed = tv_interval($starttime, [gettimeofday]);
+	if ($elapsed > 1) {
+	    die "unable to connect to VM $vmid socket - timeout after $count retries\n";
+	}
+	usleep(100000);
+    }
+
+    $self->{fhs}->{$vmid} = $fh;
+    $self->{fhs_lookup}->{$fh} = $vmid;
+    $self->{mux}->add($fh);
+ 
+    return $fh;
+};
+
+my $check_queue = sub {
+    my ($self) = @_;
+
+    my $running = 0;
+	
+    foreach my $vmid (keys %{$self->{queue}}) {
+	my $fh = $self->{fhs}->{$vmid};
+	next if !$fh;
+
+	if ($self->{errors}->{$vmid}) {
+	    &$close_connection($self, $vmid);
+	    next;
+	}
+
+	if ($self->{current}->{$vmid}) { # command running, waiting for response
+	    $running++;
+	    next;
+	}
+
+	if (!scalar(@{$self->{queue}->{$vmid}})) { # no more commands for the VM
+	    &$close_connection($self, $vmid);
+	    next;
+	}
+
+	eval {
+
+	    my $cmd = $self->{current}->{$vmid} = shift @{$self->{queue}->{$vmid}};
+	    $cmd->{id} = &$next_cmdid();
+
+	    my $qmpcmdid =to_json({
+                execute => 'guest-sync',
+                arguments => { id => int($cmd->{id}) } });
+
+	    my $qmpcmd = to_json({
+		execute => $cmd->{execute},
+		arguments => $cmd->{arguments}});
+
+	    $self->{mux}->write($fh, $qmpcmdid.$qmpcmd);
+	};
+	if (my $err = $@) {
+	    $self->{errors}->{$vmid} = $err;
+	} else {
+	    $running++;
+	}
+    }
+
+    $self->{mux}->endloop() if !$running;
+
+    return $running;
+};
+
+# execute all queued command
+sub queue_execute {
+    my ($self, $timeout) = @_;
+
+    $timeout = 3 if !$timeout;
+
+    $self->{current} = {};
+    $self->{errors} = {};
+
+    # open all necessary connections
+    foreach my $vmid (keys %{$self->{queue}}) {
+	next if !scalar(@{$self->{queue}->{$vmid}}); # no commands for the VM
+
+	eval {
+	    my $fh = &$open_connection($self, $vmid);
+	    $self->{mux}->set_timeout($fh, $timeout);
+	};
+	if (my $err = $@) {
+	    warn $err;
+	    $self->{errors}->{$vmid} = $err;
+	}
+    }
+
+    my $running;
+    for (;;) {
+
+	$running = &$check_queue($self);
+
+	last if !$running;
+
+	$self->{mux}->loop;
+    }
+
+    # make sure we close everything
+    foreach my $vmid (keys %{$self->{fhs}}) {
+	&$close_connection($self, $vmid);
+    }
+
+    $self->{queue} = $self->{current} = $self->{fhs} = $self->{fhs_lookup} = {};
+}
+
+# mux_input is called when input is available on one of
+# the descriptors.
+sub mux_input {
+    my ($self, $mux, $fh, $input) = @_;
+
+    return if $$input !~ m/}\n(.+)}\n$/;
+    my $raw = $$input;
+    # Remove the input from the input buffer.
+    $$input = '';
+    my $vmid = $self->{fhs_lookup}->{$fh};
+    if (!$vmid) {
+	warn "internal error - unable to lookup vmid";
+	return;
+    }
+    eval {
+	my @jsons = split("\n", $raw);
+
+	my $obj = from_json($jsons[0]);
+
+	my $curcmd = $self->{current}->{$vmid};
+	die "unable to lookup current command for VM $vmid\n" if (!$curcmd);
+
+        my $cmdid = $obj->{return};
+        die "received responsed without command id\n" if !$cmdid;
+
+    	if ($curcmd->{id} ne $cmdid) {
+	    die "got wrong command id '$cmdid' (expected $curcmd->{id})\n";
+    	}
+
+    	delete $self->{current}->{$vmid};
+
+ 	$obj = from_json($jsons[1]);
+
+	if (my $callback = $curcmd->{callback}) {
+  	    &$callback($vmid, $obj);
+	}
+
+    };
+    if (my $err = $@) {
+	$self->{errors}->{$vmid} = $err;
+    }
+
+    &$check_queue($self);
+}
+
+# This gets called every second to update player info, etc...
+sub mux_timeout {
+    my ($self, $mux, $fh) = @_;
+
+    if (my $vmid = $self->{fhs_lookup}->{$fh}) {
+	$self->{errors}->{$vmid} = "got timeout\n";
+    }
+
+    &$check_queue($self);
+}
+
+1;
diff --git a/PVE/QemuServer.pm b/PVE/QemuServer.pm
index bb0be42..c412283 100644
--- a/PVE/QemuServer.pm
+++ b/PVE/QemuServer.pm
@@ -26,6 +26,7 @@ use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file
 use PVE::INotify;
 use PVE::ProcFSTools;
 use PVE::QMPClient;
+use PVE::QGAClient;
 use Time::HiRes qw(gettimeofday);
 
 my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
@@ -2840,6 +2841,13 @@ sub vm_start {
     });
 }
 
+sub vm_qga_cmd {
+    my ($vmid, $execute, %params) = @_;
+
+    my $cmd = { execute => $execute, arguments => \%params };
+    vm_qga_command($vmid, $cmd);
+}
+
 sub vm_mon_cmd {
     my ($vmid, $execute, %params) = @_;
 
@@ -2888,6 +2896,36 @@ sub vm_qmp_command {
     return $res;
 }
 
+sub vm_qga_command {
+    my ($vmid, $cmd) = @_;
+
+    my $res;
+
+    my $timeout;
+    if ($cmd->{arguments} && $cmd->{arguments}->{timeout}) {
+        $timeout = $cmd->{arguments}->{timeout};
+        delete $cmd->{arguments}->{timeout};
+    }
+
+    eval {
+        die "VM $vmid not running\n" if !check_running($vmid);
+        my $sname = PVE::QemuServer::qga_socket($vmid);
+        if (-e $sname) {
+            my $qgaclient = PVE::QGAClient->new();
+
+            $res = $qgaclient->cmd($vmid, $cmd, $timeout);
+        } else {
+            die "unable to open qga socket\n";
+        }
+    };
+    if (my $err = $@) {
+        syslog("err", "VM $vmid qga command failed - $err");
+        die $err;
+    }
+
+    return $res;
+}
+
 sub vm_human_monitor_command {
     my ($vmid, $cmdline) = @_;
 
-- 
1.7.2.5



More information about the pve-devel mailing list