]>
Commit | Line | Data |
---|---|---|
b8158701 DC |
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; | |
3824765e | 9 | use PVE::QemuServer::Agent qw(agent_available); |
b428fb63 DC |
10 | use MIME::Base64 qw(encode_base64 decode_base64); |
11 | use JSON; | |
b8158701 DC |
12 | |
13 | use base qw(PVE::RESTHandler); | |
14 | ||
bb14060a DC |
15 | # max size for file-read over the api |
16 | my $MAX_READ_SIZE = 16 * 1024 * 1024; # 16 MiB | |
17 | ||
ea2bceaf DC |
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' => { | |
e6bd703b | 26 | method => 'GET', |
ea2bceaf DC |
27 | }, |
28 | 'info' => { | |
e6bd703b | 29 | method => 'GET', |
ea2bceaf DC |
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' => { | |
e6bd703b | 44 | method => 'GET', |
ea2bceaf DC |
45 | }, |
46 | 'get-vcpus' => { | |
e6bd703b | 47 | method => 'GET', |
ea2bceaf DC |
48 | }, |
49 | 'get-fsinfo' => { | |
e6bd703b | 50 | method => 'GET', |
ea2bceaf DC |
51 | }, |
52 | 'get-memory-blocks' => { | |
e6bd703b | 53 | method => 'GET', |
ea2bceaf DC |
54 | }, |
55 | 'get-memory-block-info' => { | |
e6bd703b | 56 | method => 'GET', |
ea2bceaf DC |
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 | }, | |
5667cc55 DC |
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 | }, | |
ea2bceaf DC |
83 | }; |
84 | ||
ad1f73b9 DC |
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 | ||
b428fb63 DC |
116 | my $cmds = [keys %$guest_agent_commands]; |
117 | push @$cmds, qw( | |
8efdf418 DC |
118 | exec |
119 | exec-status | |
bb14060a | 120 | file-read |
b428fb63 DC |
121 | set-user-password |
122 | ); | |
123 | ||
124 | for my $cmd ( sort @$cmds) { | |
ad1f73b9 DC |
125 | push @$result, { name => $cmd }; |
126 | } | |
127 | ||
128 | return $result; | |
129 | }}); | |
130 | ||
ea2bceaf DC |
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 = { | |
b8158701 DC |
147 | additionalProperties => 0, |
148 | properties => { | |
149 | node => get_standard_option('pve-node'), | |
150 | vmid => get_standard_option('pve-vmid', { | |
ea2bceaf | 151 | completion => \&PVE::QemuServer::complete_vmid_running }), |
b8158701 DC |
152 | command => { |
153 | type => 'string', | |
154 | description => "The QGA command.", | |
ea2bceaf | 155 | enum => [ sort keys %$guest_agent_commands ], |
b8158701 DC |
156 | }, |
157 | }, | |
ea2bceaf DC |
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}; | |
b8158701 | 186 | |
ea2bceaf | 187 | my $conf = PVE::QemuConfig->load_config ($vmid); # check if VM exists |
b8158701 | 188 | |
3824765e | 189 | agent_available($vmid, $conf); |
b8158701 | 190 | |
ea2bceaf DC |
191 | my $cmd = $param->{command} // $command; |
192 | my $res = PVE::QemuServer::vm_mon_cmd($vmid, "guest-$cmd"); | |
b8158701 | 193 | |
ea2bceaf DC |
194 | return { result => $res }; |
195 | }}); | |
196 | } | |
b8158701 | 197 | |
ea2bceaf DC |
198 | # old {vmid}/agent POST endpoint, here for compatibility |
199 | __PACKAGE__->register_command('', 'POST'); | |
b8158701 | 200 | |
ea2bceaf DC |
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 | } | |
b8158701 | 205 | |
b428fb63 DC |
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 | ||
8efdf418 DC |
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 | ||
bb14060a DC |
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 | ||
b8158701 | 441 | 1; |