]> git.proxmox.com Git - qemu-server.git/blob - PVE/API2/Qemu/Agent.pm
bump version to 6.2-5
[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 command => {
155 type => 'string',
156 description => "The QGA command.",
157 enum => [ sort keys %$guest_agent_commands ],
158 },
159 },
160 };
161
162 my $description = "Execute Qemu Guest Agent commands.";
163 my $name = 'agent';
164
165 if ($command ne '') {
166 $description = "Execute $command.";
167 $name = $command;
168 delete $parameters->{properties}->{command};
169 }
170
171 __PACKAGE__->register_method({
172 name => $name,
173 path => $command,
174 method => $method,
175 protected => 1,
176 proxyto => 'node',
177 description => $description,
178 permissions => $permission,
179 parameters => $parameters,
180 returns => {
181 type => 'object',
182 description => "Returns an object with a single `result` property.",
183 },
184 code => sub {
185 my ($param) = @_;
186
187 my $vmid = $param->{vmid};
188
189 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
190
191 agent_available($vmid, $conf);
192
193 my $cmd = $param->{command} // $command;
194 my $res = mon_cmd($vmid, "guest-$cmd");
195
196 return { result => $res };
197 }});
198 }
199
200 # old {vmid}/agent POST endpoint, here for compatibility
201 __PACKAGE__->register_command('', 'POST');
202
203 for my $cmd (sort keys %$guest_agent_commands) {
204 my $props = $guest_agent_commands->{$cmd};
205 __PACKAGE__->register_command($cmd, $props->{method}, $props->{perms});
206 }
207
208 # commands with parameters are complicated and we want to register them manually
209 __PACKAGE__->register_method({
210 name => 'set-user-password',
211 path => 'set-user-password',
212 method => 'POST',
213 protected => 1,
214 proxyto => 'node',
215 description => "Sets the password for the given user to the given password",
216 permissions => { check => [ 'perm', '/vms/{vmid}', [ 'VM.Monitor' ]]},
217 parameters => {
218 additionalProperties => 0,
219 properties => {
220 node => get_standard_option('pve-node'),
221 vmid => get_standard_option('pve-vmid', {
222 completion => \&PVE::QemuServer::complete_vmid_running }),
223 username => {
224 type => 'string',
225 description => 'The user to set the password for.'
226 },
227 password => {
228 type => 'string',
229 description => 'The new password.',
230 minLength => 5,
231 maxLength => 1024,
232 },
233 crypted => {
234 type => 'boolean',
235 description => 'set to 1 if the password has already been passed through crypt()',
236 optional => 1,
237 default => 0,
238 },
239 },
240 },
241 returns => {
242 type => 'object',
243 description => "Returns an object with a single `result` property.",
244 },
245 code => sub {
246 my ($param) = @_;
247
248 my $vmid = $param->{vmid};
249
250 my $crypted = $param->{crypted} // 0;
251 my $args = {
252 username => $param->{username},
253 password => encode_base64($param->{password}),
254 crypted => $crypted ? JSON::true : JSON::false,
255 };
256 my $res = agent_cmd($vmid, "set-user-password", $args, 'cannot set user password');
257
258 return { result => $res };
259 }});
260
261 __PACKAGE__->register_method({
262 name => 'exec',
263 path => 'exec',
264 method => 'POST',
265 protected => 1,
266 proxyto => 'node',
267 description => "Executes the given command in the vm via the guest-agent and returns an object with the pid.",
268 permissions => { check => [ 'perm', '/vms/{vmid}', [ 'VM.Monitor' ]]},
269 parameters => {
270 additionalProperties => 0,
271 properties => {
272 node => get_standard_option('pve-node'),
273 vmid => get_standard_option('pve-vmid', {
274 completion => \&PVE::QemuServer::complete_vmid_running }),
275 command => {
276 type => 'string',
277 format => 'string-alist',
278 description => 'The command as a list of program + arguments',
279 optional => 1,
280 },
281 'input-data' => {
282 type => 'string',
283 maxLength => 64 * 1024,
284 description => "Data to pass as 'input-data' to the guest. Usually treated as STDIN to 'command'.",
285 optional => 1,
286 },
287 },
288 },
289 returns => {
290 type => 'object',
291 properties => {
292 pid => {
293 type => 'integer',
294 description => "The PID of the process started by the guest-agent.",
295 },
296 },
297 },
298 code => sub {
299 my ($param) = @_;
300
301 my $vmid = $param->{vmid};
302 my $cmd = undef;
303 if ($param->{command}) {
304 $cmd = [PVE::Tools::split_list($param->{command})];
305 }
306
307 my $res = PVE::QemuServer::Agent::qemu_exec($vmid, $param->{'input-data'}, $cmd);
308 return $res;
309 }});
310
311 __PACKAGE__->register_method({
312 name => 'exec-status',
313 path => 'exec-status',
314 method => 'GET',
315 protected => 1,
316 proxyto => 'node',
317 description => "Gets the status of the given pid started by the guest-agent",
318 permissions => { check => [ 'perm', '/vms/{vmid}', [ 'VM.Monitor' ]]},
319 parameters => {
320 additionalProperties => 0,
321 properties => {
322 node => get_standard_option('pve-node'),
323 vmid => get_standard_option('pve-vmid', {
324 completion => \&PVE::QemuServer::complete_vmid_running }),
325 pid => {
326 type => 'integer',
327 description => 'The PID to query'
328 },
329 },
330 },
331 returns => {
332 type => 'object',
333 properties => {
334 exited => {
335 type => 'boolean',
336 description => 'Tells if the given command has exited yet.',
337 },
338 exitcode => {
339 type => 'integer',
340 optional => 1,
341 description => 'process exit code if it was normally terminated.',
342 },
343 signal=> {
344 type => 'integer',
345 optional => 1,
346 description => 'signal number or exception code if the process was abnormally terminated.',
347 },
348 'out-data' => {
349 type => 'string',
350 optional => 1,
351 description => 'stdout of the process',
352 },
353 'err-data' => {
354 type => 'string',
355 optional => 1,
356 description => 'stderr of the process',
357 },
358 'out-truncated' => {
359 type => 'boolean',
360 optional => 1,
361 description => 'true if stdout was not fully captured',
362 },
363 'err-truncated' => {
364 type => 'boolean',
365 optional => 1,
366 description => 'true if stderr was not fully captured',
367 },
368 },
369 },
370 code => sub {
371 my ($param) = @_;
372
373 my $vmid = $param->{vmid};
374 my $pid = int($param->{pid});
375
376 my $res = PVE::QemuServer::Agent::qemu_exec_status($vmid, $pid);
377
378 return $res;
379 }});
380
381 __PACKAGE__->register_method({
382 name => 'file-read',
383 path => 'file-read',
384 method => 'GET',
385 protected => 1,
386 proxyto => 'node',
387 description => "Reads the given file via guest agent. Is limited to $MAX_READ_SIZE bytes.",
388 permissions => { check => [ 'perm', '/vms/{vmid}', [ 'VM.Monitor' ]]},
389 parameters => {
390 additionalProperties => 0,
391 properties => {
392 node => get_standard_option('pve-node'),
393 vmid => get_standard_option('pve-vmid', {
394 completion => \&PVE::QemuServer::complete_vmid_running }),
395 file => {
396 type => 'string',
397 description => 'The path to the file'
398 },
399 },
400 },
401 returns => {
402 type => 'object',
403 description => "Returns an object with a `content` property.",
404 properties => {
405 content => {
406 type => 'string',
407 description => "The content of the file, maximum $MAX_READ_SIZE",
408 },
409 truncated => {
410 type => 'boolean',
411 optional => 1,
412 description => "If set to 1, the output is truncated and not complete"
413 }
414 },
415 },
416 code => sub {
417 my ($param) = @_;
418
419 my $vmid = $param->{vmid};
420
421 my $qgafh = agent_cmd($vmid, "file-open", { path => $param->{file} }, "can't open file");
422
423 my $bytes_left = $MAX_READ_SIZE;
424 my $eof = 0;
425 my $read_size = 1024*1024;
426 my $content = "";
427
428 while ($bytes_left > 0 && !$eof) {
429 my $read = mon_cmd($vmid, "guest-file-read", handle => $qgafh, count => int($read_size));
430 check_agent_error($read, "can't read from file");
431
432 $content .= decode_base64($read->{'buf-b64'});
433 $bytes_left -= $read->{count};
434 $eof = $read->{eof} // 0;
435 }
436
437 my $res = mon_cmd($vmid, "guest-file-close", handle => $qgafh);
438 check_agent_error($res, "can't close file", 1);
439
440 my $result = {
441 content => $content,
442 'bytes-read' => ($MAX_READ_SIZE-$bytes_left),
443 };
444
445 if (!$eof) {
446 warn "agent file-read: reached maximum read size: $MAX_READ_SIZE bytes. output might be truncated.\n";
447 $result->{truncated} = 1;
448 }
449
450 return $result;
451 }});
452
453 __PACKAGE__->register_method({
454 name => 'file-write',
455 path => 'file-write',
456 method => 'POST',
457 protected => 1,
458 proxyto => 'node',
459 description => "Writes the given file via guest agent.",
460 permissions => { check => [ 'perm', '/vms/{vmid}', [ 'VM.Monitor' ]]},
461 parameters => {
462 additionalProperties => 0,
463 properties => {
464 node => get_standard_option('pve-node'),
465 vmid => get_standard_option('pve-vmid', {
466 completion => \&PVE::QemuServer::complete_vmid_running }),
467 file => {
468 type => 'string',
469 description => 'The path to the file.'
470 },
471 content => {
472 type => 'string',
473 maxLength => 60*1024, # 60k, smaller than our 64k POST limit
474 description => "The content to write into the file."
475 }
476 },
477 },
478 returns => { type => 'null' },
479 code => sub {
480 my ($param) = @_;
481
482 my $vmid = $param->{vmid};
483 my $buf = encode_base64($param->{content});
484
485 my $qgafh = agent_cmd($vmid, "file-open", { path => $param->{file}, mode => 'wb' }, "can't open file");
486 my $write = agent_cmd($vmid, "file-write", { handle => $qgafh, 'buf-b64' => $buf }, "can't write to file");
487 my $res = agent_cmd($vmid, "file-close", { handle => $qgafh }, "can't close file");
488
489 return undef;
490 }});
491
492 1;