]> git.proxmox.com Git - qemu-server.git/blame - PVE/API2/Qemu/Agent.pm
tree wide cleanup of s/return undef/return/
[qemu-server.git] / PVE / API2 / Qemu / Agent.pm
CommitLineData
b8158701
DC
1package PVE::API2::Qemu::Agent;
2
3use strict;
4use warnings;
5
6use PVE::RESTHandler;
7use PVE::JSONSchema qw(get_standard_option);
8use PVE::QemuServer;
ccd16638 9use PVE::QemuServer::Agent qw(agent_available agent_cmd check_agent_error);
0a13e08e 10use PVE::QemuServer::Monitor qw(mon_cmd);
b428fb63
DC
11use MIME::Base64 qw(encode_base64 decode_base64);
12use JSON;
b8158701
DC
13
14use base qw(PVE::RESTHandler);
15
bb14060a
DC
16# max size for file-read over the api
17my $MAX_READ_SIZE = 16 * 1024 * 1024; # 16 MiB
18
ea2bceaf
DC
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)
22my $guest_agent_commands = {
23 'ping' => {
24 method => 'POST',
25 },
26 'get-time' => {
e6bd703b 27 method => 'GET',
ea2bceaf
DC
28 },
29 'info' => {
e6bd703b 30 method => 'GET',
ea2bceaf
DC
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' => {
e6bd703b 45 method => 'GET',
ea2bceaf
DC
46 },
47 'get-vcpus' => {
e6bd703b 48 method => 'GET',
ea2bceaf
DC
49 },
50 'get-fsinfo' => {
e6bd703b 51 method => 'GET',
ea2bceaf
DC
52 },
53 'get-memory-blocks' => {
e6bd703b 54 method => 'GET',
ea2bceaf
DC
55 },
56 'get-memory-block-info' => {
e6bd703b 57 method => 'GET',
ea2bceaf
DC
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 },
5667cc55
DC
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 },
ea2bceaf
DC
84};
85
ad1f73b9
DC
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
b428fb63
DC
117 my $cmds = [keys %$guest_agent_commands];
118 push @$cmds, qw(
8efdf418
DC
119 exec
120 exec-status
bb14060a 121 file-read
735821a4 122 file-write
b428fb63
DC
123 set-user-password
124 );
125
126 for my $cmd ( sort @$cmds) {
ad1f73b9
DC
127 push @$result, { name => $cmd };
128 }
129
130 return $result;
131 }});
132
ea2bceaf
DC
133sub 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 = {
b8158701
DC
149 additionalProperties => 0,
150 properties => {
151 node => get_standard_option('pve-node'),
152 vmid => get_standard_option('pve-vmid', {
ea2bceaf 153 completion => \&PVE::QemuServer::complete_vmid_running }),
b8158701
DC
154 command => {
155 type => 'string',
156 description => "The QGA command.",
ea2bceaf 157 enum => [ sort keys %$guest_agent_commands ],
b8158701
DC
158 },
159 },
ea2bceaf
DC
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};
b8158701 188
ea2bceaf 189 my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists
b8158701 190
3824765e 191 agent_available($vmid, $conf);
b8158701 192
ea2bceaf 193 my $cmd = $param->{command} // $command;
0a13e08e 194 my $res = mon_cmd($vmid, "guest-$cmd");
b8158701 195
ea2bceaf
DC
196 return { result => $res };
197 }});
198}
b8158701 199
ea2bceaf
DC
200# old {vmid}/agent POST endpoint, here for compatibility
201__PACKAGE__->register_command('', 'POST');
b8158701 202
ea2bceaf
DC
203for my $cmd (sort keys %$guest_agent_commands) {
204 my $props = $guest_agent_commands->{$cmd};
205 __PACKAGE__->register_command($cmd, $props->{method}, $props->{perms});
206}
b8158701 207
b428fb63
DC
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,
ba728fb5 231 maxLength => 1024,
b428fb63
DC
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 };
50ecb1ba 256 my $res = agent_cmd($vmid, "set-user-password", $args, 'cannot set user password');
b428fb63
DC
257
258 return { result => $res };
259 }});
260
8efdf418
DC
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',
d8f61794
SR
279 optional => 1,
280 },
281 'input-data' => {
282 type => 'string',
109a0950 283 maxLength => 64 * 1024,
d8f61794
SR
284 description => "Data to pass as 'input-data' to the guest. Usually treated as STDIN to 'command'.",
285 optional => 1,
286 },
8efdf418
DC
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};
d8f61794
SR
302 my $cmd = undef;
303 if ($param->{command}) {
304 $cmd = [PVE::Tools::split_list($param->{command})];
305 }
8efdf418 306
d8f61794 307 my $res = PVE::QemuServer::Agent::qemu_exec($vmid, $param->{'input-data'}, $cmd);
8efdf418
DC
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
bb14060a
DC
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) {
0a13e08e 429 my $read = mon_cmd($vmid, "guest-file-read", handle => $qgafh, count => int($read_size));
bb14060a
DC
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
0a13e08e 437 my $res = mon_cmd($vmid, "guest-file-close", handle => $qgafh);
bb14060a
DC
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
735821a4
DC
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
d1c1af4b 489 return;
735821a4
DC
490 }});
491
b8158701 4921;