]> git.proxmox.com Git - qemu-server.git/blob - PVE/API2/Qemu/Agent.pm
7d35fe760077d7ff6793051b6f36ab6f50cddb4e
[qemu-server.git] / PVE / API2 / Qemu / Agent.pm
1 package PVE::API2::Qemu::Agent;
2
3 use strict;
4 use warnings;
5
6 use PVE::RESTHandler;
7 use PVE::JSONSchema qw(get_standard_option);
8 use PVE::QemuServer;
9 use PVE::QemuServer::Agent qw(agent_available agent_cmd check_agent_error);
10 use PVE::QemuServer::Monitor qw(mon_cmd);
11 use MIME::Base64 qw(encode_base64 decode_base64);
12 use JSON;
13
14 use base qw(PVE::RESTHandler);
15
16 # max size for file-read over the api
17 my $MAX_READ_SIZE = 16 * 1024 * 1024; # 16 MiB
18
19 # list of commands
20 # will generate one api endpoint per command
21 # needs a 'method' property and optionally a 'perms' property (default VM.Monitor)
22 my $guest_agent_commands = {
23 'ping' => {
24 method => 'POST',
25 },
26 'get-time' => {
27 method => 'GET',
28 },
29 'info' => {
30 method => 'GET',
31 },
32 'fsfreeze-status' => {
33 method => 'POST',
34 },
35 'fsfreeze-freeze' => {
36 method => 'POST',
37 },
38 'fsfreeze-thaw' => {
39 method => 'POST',
40 },
41 'fstrim' => {
42 method => 'POST',
43 },
44 'network-get-interfaces' => {
45 method => 'GET',
46 },
47 'get-vcpus' => {
48 method => 'GET',
49 },
50 'get-fsinfo' => {
51 method => 'GET',
52 },
53 'get-memory-blocks' => {
54 method => 'GET',
55 },
56 'get-memory-block-info' => {
57 method => 'GET',
58 },
59 'suspend-hybrid' => {
60 method => 'POST',
61 },
62 'suspend-ram' => {
63 method => 'POST',
64 },
65 'suspend-disk' => {
66 method => 'POST',
67 },
68 'shutdown' => {
69 method => 'POST',
70 },
71 # added since qemu 2.9
72 'get-host-name' => {
73 method => 'GET',
74 },
75 'get-osinfo' => {
76 method => 'GET',
77 },
78 'get-users' => {
79 method => 'GET',
80 },
81 'get-timezone' => {
82 method => 'GET',
83 },
84 };
85
86 __PACKAGE__->register_method({
87 name => 'index',
88 path => '',
89 proxyto => 'node',
90 method => 'GET',
91 description => "Qemu Agent command index.",
92 permissions => {
93 user => 'all',
94 },
95 parameters => {
96 additionalProperties => 1,
97 properties => {
98 node => get_standard_option('pve-node'),
99 vmid => get_standard_option('pve-vmid', {
100 completion => \&PVE::QemuServer::complete_vmid_running }),
101 },
102 },
103 returns => {
104 type => 'array',
105 items => {
106 type => "object",
107 properties => {},
108 },
109 links => [ { rel => 'child', href => '{name}' } ],
110 description => "Returns the list of Qemu Agent commands",
111 },
112 code => sub {
113 my ($param) = @_;
114
115 my $result = [];
116
117 my $cmds = [keys %$guest_agent_commands];
118 push @$cmds, qw(
119 exec
120 exec-status
121 file-read
122 file-write
123 set-user-password
124 );
125
126 for my $cmd ( sort @$cmds) {
127 push @$result, { name => $cmd };
128 }
129
130 return $result;
131 }});
132
133 sub register_command {
134 my ($class, $command, $method, $perm) = @_;
135
136 die "no method given\n" if !$method;
137 die "no command given\n" if !defined($command);
138
139 my $permission;
140
141 if (ref($perm) eq 'HASH') {
142 $permission = $perm;
143 } else {
144 $perm //= 'VM.Monitor';
145 $permission = { check => [ 'perm', '/vms/{vmid}', [ $perm ]]};
146 }
147
148 my $parameters = {
149 additionalProperties => 0,
150 properties => {
151 node => get_standard_option('pve-node'),
152 vmid => get_standard_option('pve-vmid', {
153 completion => \&PVE::QemuServer::complete_vmid_running,
154 }),
155 command => {
156 type => 'string',
157 description => "The QGA command.",
158 enum => [ sort keys %$guest_agent_commands ],
159 },
160 },
161 };
162
163 my $description = "Execute Qemu Guest Agent commands.";
164 my $name = 'agent';
165
166 if ($command ne '') {
167 $description = "Execute $command.";
168 $name = $command;
169 delete $parameters->{properties}->{command};
170 }
171
172 __PACKAGE__->register_method({
173 name => $name,
174 path => $command,
175 method => $method,
176 protected => 1,
177 proxyto => 'node',
178 description => $description,
179 permissions => $permission,
180 parameters => $parameters,
181 returns => {
182 type => 'object',
183 description => "Returns an object with a single `result` property.",
184 },
185 code => sub {
186 my ($param) = @_;
187
188 my $vmid = $param->{vmid};
189
190 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
191
192 agent_available($vmid, $conf);
193
194 my $cmd = $param->{command} // $command;
195 my $res = mon_cmd($vmid, "guest-$cmd");
196
197 return { result => $res };
198 }});
199 }
200
201 # old {vmid}/agent POST endpoint, here for compatibility
202 __PACKAGE__->register_command('', 'POST');
203
204 for my $cmd (sort keys %$guest_agent_commands) {
205 my $props = $guest_agent_commands->{$cmd};
206 __PACKAGE__->register_command($cmd, $props->{method}, $props->{perms});
207 }
208
209 # commands with parameters are complicated and we want to register them manually
210 __PACKAGE__->register_method({
211 name => 'set-user-password',
212 path => 'set-user-password',
213 method => 'POST',
214 protected => 1,
215 proxyto => 'node',
216 description => "Sets the password for the given user to the given password",
217 permissions => { check => [ 'perm', '/vms/{vmid}', [ 'VM.Monitor' ]]},
218 parameters => {
219 additionalProperties => 0,
220 properties => {
221 node => get_standard_option('pve-node'),
222 vmid => get_standard_option('pve-vmid', {
223 completion => \&PVE::QemuServer::complete_vmid_running }),
224 username => {
225 type => 'string',
226 description => 'The user to set the password for.'
227 },
228 password => {
229 type => 'string',
230 description => 'The new password.',
231 minLength => 5,
232 maxLength => 1024,
233 },
234 crypted => {
235 type => 'boolean',
236 description => 'set to 1 if the password has already been passed through crypt()',
237 optional => 1,
238 default => 0,
239 },
240 },
241 },
242 returns => {
243 type => 'object',
244 description => "Returns an object with a single `result` property.",
245 },
246 code => sub {
247 my ($param) = @_;
248
249 my $vmid = $param->{vmid};
250
251 my $crypted = $param->{crypted} // 0;
252 my $args = {
253 username => $param->{username},
254 password => encode_base64($param->{password}),
255 crypted => $crypted ? JSON::true : JSON::false,
256 };
257 my $res = agent_cmd($vmid, "set-user-password", $args, 'cannot set user password');
258
259 return { result => $res };
260 }});
261
262 __PACKAGE__->register_method({
263 name => 'exec',
264 path => 'exec',
265 method => 'POST',
266 protected => 1,
267 proxyto => 'node',
268 description => "Executes the given command in the vm via the guest-agent and returns an object with the pid.",
269 permissions => { check => [ 'perm', '/vms/{vmid}', [ 'VM.Monitor' ]]},
270 parameters => {
271 additionalProperties => 0,
272 properties => {
273 node => get_standard_option('pve-node'),
274 vmid => get_standard_option('pve-vmid', {
275 completion => \&PVE::QemuServer::complete_vmid_running }),
276 command => {
277 type => 'string',
278 format => 'string-alist',
279 description => 'The command as a list of program + arguments',
280 optional => 1,
281 },
282 'input-data' => {
283 type => 'string',
284 maxLength => 64 * 1024,
285 description => "Data to pass as 'input-data' to the guest. Usually treated as STDIN to 'command'.",
286 optional => 1,
287 },
288 },
289 },
290 returns => {
291 type => 'object',
292 properties => {
293 pid => {
294 type => 'integer',
295 description => "The PID of the process started by the guest-agent.",
296 },
297 },
298 },
299 code => sub {
300 my ($param) = @_;
301
302 my $vmid = $param->{vmid};
303 my $cmd = undef;
304 if ($param->{command}) {
305 $cmd = [PVE::Tools::split_list($param->{command})];
306 }
307
308 my $res = PVE::QemuServer::Agent::qemu_exec($vmid, $param->{'input-data'}, $cmd);
309 return $res;
310 }});
311
312 __PACKAGE__->register_method({
313 name => 'exec-status',
314 path => 'exec-status',
315 method => 'GET',
316 protected => 1,
317 proxyto => 'node',
318 description => "Gets the status of the given pid started by the guest-agent",
319 permissions => { check => [ 'perm', '/vms/{vmid}', [ 'VM.Monitor' ]]},
320 parameters => {
321 additionalProperties => 0,
322 properties => {
323 node => get_standard_option('pve-node'),
324 vmid => get_standard_option('pve-vmid', {
325 completion => \&PVE::QemuServer::complete_vmid_running }),
326 pid => {
327 type => 'integer',
328 description => 'The PID to query'
329 },
330 },
331 },
332 returns => {
333 type => 'object',
334 properties => {
335 exited => {
336 type => 'boolean',
337 description => 'Tells if the given command has exited yet.',
338 },
339 exitcode => {
340 type => 'integer',
341 optional => 1,
342 description => 'process exit code if it was normally terminated.',
343 },
344 signal=> {
345 type => 'integer',
346 optional => 1,
347 description => 'signal number or exception code if the process was abnormally terminated.',
348 },
349 'out-data' => {
350 type => 'string',
351 optional => 1,
352 description => 'stdout of the process',
353 },
354 'err-data' => {
355 type => 'string',
356 optional => 1,
357 description => 'stderr of the process',
358 },
359 'out-truncated' => {
360 type => 'boolean',
361 optional => 1,
362 description => 'true if stdout was not fully captured',
363 },
364 'err-truncated' => {
365 type => 'boolean',
366 optional => 1,
367 description => 'true if stderr was not fully captured',
368 },
369 },
370 },
371 code => sub {
372 my ($param) = @_;
373
374 my $vmid = $param->{vmid};
375 my $pid = int($param->{pid});
376
377 my $res = PVE::QemuServer::Agent::qemu_exec_status($vmid, $pid);
378
379 return $res;
380 }});
381
382 __PACKAGE__->register_method({
383 name => 'file-read',
384 path => 'file-read',
385 method => 'GET',
386 protected => 1,
387 proxyto => 'node',
388 description => "Reads the given file via guest agent. Is limited to $MAX_READ_SIZE bytes.",
389 permissions => { check => [ 'perm', '/vms/{vmid}', [ 'VM.Monitor' ]]},
390 parameters => {
391 additionalProperties => 0,
392 properties => {
393 node => get_standard_option('pve-node'),
394 vmid => get_standard_option('pve-vmid', {
395 completion => \&PVE::QemuServer::complete_vmid_running }),
396 file => {
397 type => 'string',
398 description => 'The path to the file'
399 },
400 },
401 },
402 returns => {
403 type => 'object',
404 description => "Returns an object with a `content` property.",
405 properties => {
406 content => {
407 type => 'string',
408 description => "The content of the file, maximum $MAX_READ_SIZE",
409 },
410 truncated => {
411 type => 'boolean',
412 optional => 1,
413 description => "If set to 1, the output is truncated and not complete"
414 }
415 },
416 },
417 code => sub {
418 my ($param) = @_;
419
420 my $vmid = $param->{vmid};
421
422 my $qgafh = agent_cmd($vmid, "file-open", { path => $param->{file} }, "can't open file");
423
424 my $bytes_left = $MAX_READ_SIZE;
425 my $eof = 0;
426 my $read_size = 1024*1024;
427 my $content = "";
428
429 while ($bytes_left > 0 && !$eof) {
430 my $read = mon_cmd($vmid, "guest-file-read", handle => $qgafh, count => int($read_size));
431 check_agent_error($read, "can't read from file");
432
433 $content .= decode_base64($read->{'buf-b64'});
434 $bytes_left -= $read->{count};
435 $eof = $read->{eof} // 0;
436 }
437
438 my $res = mon_cmd($vmid, "guest-file-close", handle => $qgafh);
439 check_agent_error($res, "can't close file", 1);
440
441 my $result = {
442 content => $content,
443 'bytes-read' => ($MAX_READ_SIZE-$bytes_left),
444 };
445
446 if (!$eof) {
447 warn "agent file-read: reached maximum read size: $MAX_READ_SIZE bytes. output might be truncated.\n";
448 $result->{truncated} = 1;
449 }
450
451 return $result;
452 }});
453
454 __PACKAGE__->register_method({
455 name => 'file-write',
456 path => 'file-write',
457 method => 'POST',
458 protected => 1,
459 proxyto => 'node',
460 description => "Writes the given file via guest agent.",
461 permissions => { check => [ 'perm', '/vms/{vmid}', [ 'VM.Monitor' ]]},
462 parameters => {
463 additionalProperties => 0,
464 properties => {
465 node => get_standard_option('pve-node'),
466 vmid => get_standard_option('pve-vmid', {
467 completion => \&PVE::QemuServer::complete_vmid_running }),
468 file => {
469 type => 'string',
470 description => 'The path to the file.'
471 },
472 content => {
473 type => 'string',
474 maxLength => 60 * 1024, # 60k, smaller than our 64k POST limit
475 description => "The content to write into the file."
476 },
477 encode => {
478 type => 'boolean',
479 description => "If set, the content will be encoded as base64 (required by QEMU)."
480 ."Otherwise the content needs to be encoded beforehand - defaults to true.",
481 optional => 1,
482 default => 1,
483 },
484 },
485 },
486 returns => { type => 'null' },
487 code => sub {
488 my ($param) = @_;
489
490 my $vmid = $param->{vmid};
491
492 my $buf = ($param->{encode} // 1) ? encode_base64($param->{content}) : $param->{content};
493
494 my $qgafh = agent_cmd($vmid, "file-open", { path => $param->{file}, mode => 'wb' }, "can't open file");
495 my $write = agent_cmd($vmid, "file-write", { handle => $qgafh, 'buf-b64' => $buf }, "can't write to file");
496 my $res = agent_cmd($vmid, "file-close", { handle => $qgafh }, "can't close file");
497
498 return;
499 }});
500
501 1;