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