]>
Commit | Line | Data |
---|---|---|
1e3baf05 DM |
1 | package PVE::API2::Qemu; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5b9d692a | 5 | use Cwd 'abs_path'; |
451b2b81 | 6 | use Net::SSLeay; |
1e3baf05 | 7 | |
502d18a2 | 8 | use PVE::Cluster qw (cfs_read_file cfs_write_file);; |
1e3baf05 DM |
9 | use PVE::SafeSyslog; |
10 | use PVE::Tools qw(extract_param); | |
f9bfceef | 11 | use PVE::Exception qw(raise raise_param_exc raise_perm_exc); |
1e3baf05 DM |
12 | use PVE::Storage; |
13 | use PVE::JSONSchema qw(get_standard_option); | |
14 | use PVE::RESTHandler; | |
15 | use PVE::QemuServer; | |
3ea94c60 | 16 | use PVE::QemuMigrate; |
1e3baf05 DM |
17 | use PVE::RPCEnvironment; |
18 | use PVE::AccessControl; | |
19 | use PVE::INotify; | |
de8f60b2 | 20 | use PVE::Network; |
1e3baf05 DM |
21 | |
22 | use Data::Dumper; # fixme: remove | |
23 | ||
24 | use base qw(PVE::RESTHandler); | |
25 | ||
26 | my $opt_force_description = "Force physical removal. Without this, we simple remove the disk from the config file and create an additional configuration entry called 'unused[n]', which contains the volume ID. Unlink of unused[n] always cause physical removal."; | |
27 | ||
28 | my $resolve_cdrom_alias = sub { | |
29 | my $param = shift; | |
30 | ||
31 | if (my $value = $param->{cdrom}) { | |
32 | $value .= ",media=cdrom" if $value !~ m/media=/; | |
33 | $param->{ide2} = $value; | |
34 | delete $param->{cdrom}; | |
35 | } | |
36 | }; | |
37 | ||
a0d1b1a2 | 38 | |
ae57f6b3 | 39 | my $check_storage_access = sub { |
fcbb753e | 40 | my ($rpcenv, $authuser, $storecfg, $vmid, $settings, $default_storage) = @_; |
a0d1b1a2 | 41 | |
ae57f6b3 DM |
42 | PVE::QemuServer::foreach_drive($settings, sub { |
43 | my ($ds, $drive) = @_; | |
a0d1b1a2 | 44 | |
ae57f6b3 | 45 | my $isCDROM = PVE::QemuServer::drive_is_cdrom($drive); |
a0d1b1a2 | 46 | |
ae57f6b3 | 47 | my $volid = $drive->{file}; |
a0d1b1a2 | 48 | |
09d0ee64 DM |
49 | if (!$volid || $volid eq 'none') { |
50 | # nothing to check | |
f5782fd0 DM |
51 | } elsif ($isCDROM && ($volid eq 'cdrom')) { |
52 | $rpcenv->check($authuser, "/", ['Sys.Console']); | |
09d0ee64 | 53 | } elsif (!$isCDROM && ($volid =~ m/^(([^:\s]+):)?(\d+(\.\d+)?)$/)) { |
a0d1b1a2 DM |
54 | my ($storeid, $size) = ($2 || $default_storage, $3); |
55 | die "no storage ID specified (and no default storage)\n" if !$storeid; | |
fcbb753e | 56 | $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']); |
a0d1b1a2 | 57 | } else { |
8b192abf | 58 | $rpcenv->check_volume_access($authuser, $storecfg, $vmid, $volid); |
a0d1b1a2 DM |
59 | } |
60 | }); | |
ae57f6b3 | 61 | }; |
a0d1b1a2 | 62 | |
9418baad | 63 | my $check_storage_access_clone = sub { |
81f043eb | 64 | my ($rpcenv, $authuser, $storecfg, $conf, $storage) = @_; |
6116f729 | 65 | |
55173c6b DM |
66 | my $sharedvm = 1; |
67 | ||
6116f729 DM |
68 | PVE::QemuServer::foreach_drive($conf, sub { |
69 | my ($ds, $drive) = @_; | |
70 | ||
71 | my $isCDROM = PVE::QemuServer::drive_is_cdrom($drive); | |
72 | ||
73 | my $volid = $drive->{file}; | |
74 | ||
75 | return if !$volid || $volid eq 'none'; | |
76 | ||
77 | if ($isCDROM) { | |
78 | if ($volid eq 'cdrom') { | |
79 | $rpcenv->check($authuser, "/", ['Sys.Console']); | |
80 | } else { | |
75466c4f | 81 | # we simply allow access |
55173c6b DM |
82 | my ($sid, $volname) = PVE::Storage::parse_volume_id($volid); |
83 | my $scfg = PVE::Storage::storage_config($storecfg, $sid); | |
84 | $sharedvm = 0 if !$scfg->{shared}; | |
85 | ||
6116f729 DM |
86 | } |
87 | } else { | |
55173c6b DM |
88 | my ($sid, $volname) = PVE::Storage::parse_volume_id($volid); |
89 | my $scfg = PVE::Storage::storage_config($storecfg, $sid); | |
90 | $sharedvm = 0 if !$scfg->{shared}; | |
91 | ||
81f043eb | 92 | $sid = $storage if $storage; |
6116f729 DM |
93 | $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']); |
94 | } | |
95 | }); | |
55173c6b DM |
96 | |
97 | return $sharedvm; | |
6116f729 DM |
98 | }; |
99 | ||
ae57f6b3 DM |
100 | # Note: $pool is only needed when creating a VM, because pool permissions |
101 | # are automatically inherited if VM already exists inside a pool. | |
102 | my $create_disks = sub { | |
103 | my ($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $settings, $default_storage) = @_; | |
a0d1b1a2 DM |
104 | |
105 | my $vollist = []; | |
a0d1b1a2 | 106 | |
ae57f6b3 DM |
107 | my $res = {}; |
108 | PVE::QemuServer::foreach_drive($settings, sub { | |
109 | my ($ds, $disk) = @_; | |
110 | ||
111 | my $volid = $disk->{file}; | |
112 | ||
f5782fd0 | 113 | if (!$volid || $volid eq 'none' || $volid eq 'cdrom') { |
628e9a2b AD |
114 | delete $disk->{size}; |
115 | $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk); | |
09d0ee64 | 116 | } elsif ($volid =~ m/^(([^:\s]+):)?(\d+(\.\d+)?)$/) { |
ae57f6b3 DM |
117 | my ($storeid, $size) = ($2 || $default_storage, $3); |
118 | die "no storage ID specified (and no default storage)\n" if !$storeid; | |
119 | my $defformat = PVE::Storage::storage_default_format($storecfg, $storeid); | |
120 | my $fmt = $disk->{format} || $defformat; | |
a0d1b1a2 DM |
121 | my $volid = PVE::Storage::vdisk_alloc($storecfg, $storeid, $vmid, |
122 | $fmt, undef, $size*1024*1024); | |
a0d1b1a2 | 123 | $disk->{file} = $volid; |
24afaca0 | 124 | $disk->{size} = $size*1024*1024*1024; |
a0d1b1a2 | 125 | push @$vollist, $volid; |
ae57f6b3 DM |
126 | delete $disk->{format}; # no longer needed |
127 | $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk); | |
128 | } else { | |
eabe0da0 | 129 | |
ba68cf09 | 130 | my $path = $rpcenv->check_volume_access($authuser, $storecfg, $vmid, $volid); |
75466c4f | 131 | |
7043d946 | 132 | my $volid_is_new = 1; |
35cb731c | 133 | |
7043d946 DM |
134 | if ($conf->{$ds}) { |
135 | my $olddrive = PVE::QemuServer::parse_drive($ds, $conf->{$ds}); | |
136 | $volid_is_new = undef if $olddrive->{file} && $olddrive->{file} eq $volid; | |
eabe0da0 | 137 | } |
75466c4f | 138 | |
d52b8b77 | 139 | if ($volid_is_new) { |
09a89895 | 140 | |
7043d946 DM |
141 | my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, 1); |
142 | ||
09a89895 | 143 | PVE::Storage::activate_volumes($storecfg, [ $volid ]) if $storeid; |
d52b8b77 DM |
144 | |
145 | my $size = PVE::Storage::volume_size_info($storecfg, $volid); | |
146 | ||
147 | die "volume $volid does not exists\n" if !$size; | |
148 | ||
149 | $disk->{size} = $size; | |
09a89895 | 150 | } |
24afaca0 | 151 | |
24afaca0 | 152 | $res->{$ds} = PVE::QemuServer::print_drive($vmid, $disk); |
a0d1b1a2 | 153 | } |
ae57f6b3 | 154 | }); |
a0d1b1a2 DM |
155 | |
156 | # free allocated images on error | |
157 | if (my $err = $@) { | |
158 | syslog('err', "VM $vmid creating disks failed"); | |
159 | foreach my $volid (@$vollist) { | |
160 | eval { PVE::Storage::vdisk_free($storecfg, $volid); }; | |
161 | warn $@ if $@; | |
162 | } | |
163 | die $err; | |
164 | } | |
165 | ||
166 | # modify vm config if everything went well | |
ae57f6b3 DM |
167 | foreach my $ds (keys %$res) { |
168 | $conf->{$ds} = $res->{$ds}; | |
a0d1b1a2 DM |
169 | } |
170 | ||
171 | return $vollist; | |
172 | }; | |
173 | ||
174 | my $check_vm_modify_config_perm = sub { | |
ae57f6b3 | 175 | my ($rpcenv, $authuser, $vmid, $pool, $key_list) = @_; |
a0d1b1a2 | 176 | |
6e5c4da7 | 177 | return 1 if $authuser eq 'root@pam'; |
a0d1b1a2 | 178 | |
ae57f6b3 | 179 | foreach my $opt (@$key_list) { |
a0d1b1a2 DM |
180 | # disk checks need to be done somewhere else |
181 | next if PVE::QemuServer::valid_drivename($opt); | |
182 | ||
183 | if ($opt eq 'sockets' || $opt eq 'cores' || | |
75466c4f | 184 | $opt eq 'cpu' || $opt eq 'smp' || |
ab6b35df | 185 | $opt eq 'cpulimit' || $opt eq 'cpuunits') { |
a0d1b1a2 DM |
186 | $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']); |
187 | } elsif ($opt eq 'boot' || $opt eq 'bootdisk') { | |
188 | $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']); | |
ccd5438f | 189 | } elsif ($opt eq 'memory' || $opt eq 'balloon' || $opt eq 'shares') { |
a0d1b1a2 DM |
190 | $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']); |
191 | } elsif ($opt eq 'args' || $opt eq 'lock') { | |
192 | die "only root can set '$opt' config\n"; | |
6dbe8b45 | 193 | } elsif ($opt eq 'cpu' || $opt eq 'kvm' || $opt eq 'acpi' || $opt eq 'machine' || |
a0d1b1a2 DM |
194 | $opt eq 'vga' || $opt eq 'watchdog' || $opt eq 'tablet') { |
195 | $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']); | |
196 | } elsif ($opt =~ m/^net\d+$/) { | |
197 | $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']); | |
198 | } else { | |
199 | $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']); | |
200 | } | |
201 | } | |
202 | ||
203 | return 1; | |
204 | }; | |
205 | ||
1e3baf05 | 206 | __PACKAGE__->register_method({ |
afdb31d5 DM |
207 | name => 'vmlist', |
208 | path => '', | |
1e3baf05 DM |
209 | method => 'GET', |
210 | description => "Virtual machine index (per node).", | |
a0d1b1a2 DM |
211 | permissions => { |
212 | description => "Only list VMs where you have VM.Audit permissons on /vms/<vmid>.", | |
213 | user => 'all', | |
214 | }, | |
1e3baf05 DM |
215 | proxyto => 'node', |
216 | protected => 1, # qemu pid files are only readable by root | |
217 | parameters => { | |
218 | additionalProperties => 0, | |
219 | properties => { | |
220 | node => get_standard_option('pve-node'), | |
221 | }, | |
222 | }, | |
223 | returns => { | |
224 | type => 'array', | |
225 | items => { | |
226 | type => "object", | |
227 | properties => {}, | |
228 | }, | |
229 | links => [ { rel => 'child', href => "{vmid}" } ], | |
230 | }, | |
231 | code => sub { | |
232 | my ($param) = @_; | |
233 | ||
a0d1b1a2 DM |
234 | my $rpcenv = PVE::RPCEnvironment::get(); |
235 | my $authuser = $rpcenv->get_user(); | |
236 | ||
1e3baf05 DM |
237 | my $vmstatus = PVE::QemuServer::vmstatus(); |
238 | ||
a0d1b1a2 DM |
239 | my $res = []; |
240 | foreach my $vmid (keys %$vmstatus) { | |
241 | next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1); | |
242 | ||
243 | my $data = $vmstatus->{$vmid}; | |
244 | $data->{vmid} = $vmid; | |
245 | push @$res, $data; | |
246 | } | |
1e3baf05 | 247 | |
a0d1b1a2 | 248 | return $res; |
1e3baf05 DM |
249 | }}); |
250 | ||
d703d4c0 | 251 | |
d703d4c0 | 252 | |
1e3baf05 | 253 | __PACKAGE__->register_method({ |
afdb31d5 DM |
254 | name => 'create_vm', |
255 | path => '', | |
1e3baf05 | 256 | method => 'POST', |
3e16d5fc | 257 | description => "Create or restore a virtual machine.", |
a0d1b1a2 | 258 | permissions => { |
f9bfceef DM |
259 | description => "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " . |
260 | "For restore (option 'archive'), it is enough if the user has 'VM.Backup' permission and the VM already exists. " . | |
261 | "If you create disks you need 'Datastore.AllocateSpace' on any used storage.", | |
262 | user => 'all', # check inside | |
a0d1b1a2 | 263 | }, |
1e3baf05 DM |
264 | protected => 1, |
265 | proxyto => 'node', | |
266 | parameters => { | |
267 | additionalProperties => 0, | |
268 | properties => PVE::QemuServer::json_config_properties( | |
269 | { | |
270 | node => get_standard_option('pve-node'), | |
271 | vmid => get_standard_option('pve-vmid'), | |
3e16d5fc DM |
272 | archive => { |
273 | description => "The backup file.", | |
274 | type => 'string', | |
275 | optional => 1, | |
276 | maxLength => 255, | |
277 | }, | |
278 | storage => get_standard_option('pve-storage-id', { | |
279 | description => "Default storage.", | |
280 | optional => 1, | |
281 | }), | |
282 | force => { | |
afdb31d5 | 283 | optional => 1, |
3e16d5fc DM |
284 | type => 'boolean', |
285 | description => "Allow to overwrite existing VM.", | |
51586c3a DM |
286 | requires => 'archive', |
287 | }, | |
288 | unique => { | |
afdb31d5 | 289 | optional => 1, |
51586c3a DM |
290 | type => 'boolean', |
291 | description => "Assign a unique random ethernet address.", | |
292 | requires => 'archive', | |
3e16d5fc | 293 | }, |
75466c4f | 294 | pool => { |
a0d1b1a2 DM |
295 | optional => 1, |
296 | type => 'string', format => 'pve-poolid', | |
297 | description => "Add the VM to the specified pool.", | |
298 | }, | |
1e3baf05 DM |
299 | }), |
300 | }, | |
afdb31d5 | 301 | returns => { |
5fdbe4f0 DM |
302 | type => 'string', |
303 | }, | |
1e3baf05 DM |
304 | code => sub { |
305 | my ($param) = @_; | |
306 | ||
5fdbe4f0 DM |
307 | my $rpcenv = PVE::RPCEnvironment::get(); |
308 | ||
a0d1b1a2 | 309 | my $authuser = $rpcenv->get_user(); |
5fdbe4f0 | 310 | |
1e3baf05 DM |
311 | my $node = extract_param($param, 'node'); |
312 | ||
1e3baf05 DM |
313 | my $vmid = extract_param($param, 'vmid'); |
314 | ||
3e16d5fc DM |
315 | my $archive = extract_param($param, 'archive'); |
316 | ||
317 | my $storage = extract_param($param, 'storage'); | |
318 | ||
51586c3a DM |
319 | my $force = extract_param($param, 'force'); |
320 | ||
321 | my $unique = extract_param($param, 'unique'); | |
75466c4f | 322 | |
a0d1b1a2 | 323 | my $pool = extract_param($param, 'pool'); |
51586c3a | 324 | |
1e3baf05 | 325 | my $filename = PVE::QemuServer::config_file($vmid); |
afdb31d5 DM |
326 | |
327 | my $storecfg = PVE::Storage::config(); | |
1e3baf05 | 328 | |
3e16d5fc | 329 | PVE::Cluster::check_cfs_quorum(); |
1e3baf05 | 330 | |
a0d1b1a2 DM |
331 | if (defined($pool)) { |
332 | $rpcenv->check_pool_exist($pool); | |
75466c4f | 333 | } |
a0d1b1a2 | 334 | |
fcbb753e | 335 | $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']) |
a0d1b1a2 DM |
336 | if defined($storage); |
337 | ||
f9bfceef DM |
338 | if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) { |
339 | # OK | |
340 | } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) { | |
341 | # OK | |
342 | } elsif ($archive && $force && (-f $filename) && | |
343 | $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) { | |
344 | # OK: user has VM.Backup permissions, and want to restore an existing VM | |
345 | } else { | |
346 | raise_perm_exc(); | |
347 | } | |
348 | ||
afdb31d5 | 349 | if (!$archive) { |
3e16d5fc | 350 | &$resolve_cdrom_alias($param); |
1e3baf05 | 351 | |
fcbb753e | 352 | &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param, $storage); |
ae57f6b3 DM |
353 | |
354 | &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, $pool, [ keys %$param]); | |
355 | ||
3e16d5fc DM |
356 | foreach my $opt (keys %$param) { |
357 | if (PVE::QemuServer::valid_drivename($opt)) { | |
358 | my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt}); | |
359 | raise_param_exc({ $opt => "unable to parse drive options" }) if !$drive; | |
afdb31d5 | 360 | |
3e16d5fc DM |
361 | PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive); |
362 | $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive); | |
363 | } | |
1e3baf05 | 364 | } |
3e16d5fc DM |
365 | |
366 | PVE::QemuServer::add_random_macs($param); | |
51586c3a DM |
367 | } else { |
368 | my $keystr = join(' ', keys %$param); | |
bc4dcb99 DM |
369 | raise_param_exc({ archive => "option conflicts with other options ($keystr)"}) if $keystr; |
370 | ||
5b9d692a | 371 | if ($archive eq '-') { |
afdb31d5 | 372 | die "pipe requires cli environment\n" |
d7810bc1 | 373 | if $rpcenv->{type} ne 'cli'; |
5b9d692a | 374 | } else { |
ba68cf09 | 375 | my $path = $rpcenv->check_volume_access($authuser, $storecfg, $vmid, $archive); |
d7810bc1 DM |
376 | |
377 | PVE::Storage::activate_volumes($storecfg, [ $archive ]) | |
378 | if PVE::Storage::parse_volume_id ($archive, 1); | |
379 | ||
971f27c4 DM |
380 | die "can't find archive file '$archive'\n" if !($path && -f $path); |
381 | $archive = $path; | |
382 | } | |
1e3baf05 DM |
383 | } |
384 | ||
3e16d5fc DM |
385 | my $restorefn = sub { |
386 | ||
6116f729 | 387 | # fixme: this test does not work if VM exists on other node! |
3e16d5fc | 388 | if (-f $filename) { |
afdb31d5 | 389 | die "unable to restore vm $vmid: config file already exists\n" |
51586c3a | 390 | if !$force; |
3e16d5fc | 391 | |
afdb31d5 | 392 | die "unable to restore vm $vmid: vm is running\n" |
3e16d5fc DM |
393 | if PVE::QemuServer::check_running($vmid); |
394 | } | |
395 | ||
396 | my $realcmd = sub { | |
a0d1b1a2 | 397 | PVE::QemuServer::restore_archive($archive, $vmid, $authuser, { |
51586c3a | 398 | storage => $storage, |
a0d1b1a2 | 399 | pool => $pool, |
51586c3a | 400 | unique => $unique }); |
502d18a2 | 401 | |
be517049 | 402 | PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool; |
3e16d5fc DM |
403 | }; |
404 | ||
a0d1b1a2 | 405 | return $rpcenv->fork_worker('qmrestore', $vmid, $authuser, $realcmd); |
3e16d5fc | 406 | }; |
1e3baf05 | 407 | |
1e3baf05 DM |
408 | my $createfn = sub { |
409 | ||
191435c6 | 410 | # test after locking |
afdb31d5 | 411 | die "unable to create vm $vmid: config file already exists\n" |
1e3baf05 DM |
412 | if -f $filename; |
413 | ||
5fdbe4f0 | 414 | my $realcmd = sub { |
1e3baf05 | 415 | |
5fdbe4f0 | 416 | my $vollist = []; |
1e3baf05 | 417 | |
1858638f DM |
418 | my $conf = $param; |
419 | ||
5fdbe4f0 | 420 | eval { |
ae57f6b3 | 421 | |
1858638f | 422 | $vollist = &$create_disks($rpcenv, $authuser, $conf, $storecfg, $vmid, $pool, $param, $storage); |
1e3baf05 | 423 | |
5fdbe4f0 DM |
424 | # try to be smart about bootdisk |
425 | my @disks = PVE::QemuServer::disknames(); | |
426 | my $firstdisk; | |
427 | foreach my $ds (reverse @disks) { | |
1858638f DM |
428 | next if !$conf->{$ds}; |
429 | my $disk = PVE::QemuServer::parse_drive($ds, $conf->{$ds}); | |
5fdbe4f0 DM |
430 | next if PVE::QemuServer::drive_is_cdrom($disk); |
431 | $firstdisk = $ds; | |
432 | } | |
1e3baf05 | 433 | |
1858638f DM |
434 | if (!$conf->{bootdisk} && $firstdisk) { |
435 | $conf->{bootdisk} = $firstdisk; | |
5fdbe4f0 | 436 | } |
1e3baf05 | 437 | |
ae9ca91d DM |
438 | PVE::QemuServer::update_config_nolock($vmid, $conf); |
439 | ||
5fdbe4f0 DM |
440 | }; |
441 | my $err = $@; | |
1e3baf05 | 442 | |
5fdbe4f0 DM |
443 | if ($err) { |
444 | foreach my $volid (@$vollist) { | |
445 | eval { PVE::Storage::vdisk_free($storecfg, $volid); }; | |
446 | warn $@ if $@; | |
447 | } | |
448 | die "create failed - $err"; | |
449 | } | |
502d18a2 | 450 | |
be517049 | 451 | PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool; |
5fdbe4f0 DM |
452 | }; |
453 | ||
a0d1b1a2 | 454 | return $rpcenv->fork_worker('qmcreate', $vmid, $authuser, $realcmd); |
5fdbe4f0 DM |
455 | }; |
456 | ||
191435c6 | 457 | return PVE::QemuServer::lock_config_full($vmid, 1, $archive ? $restorefn : $createfn); |
1e3baf05 DM |
458 | }}); |
459 | ||
460 | __PACKAGE__->register_method({ | |
461 | name => 'vmdiridx', | |
afdb31d5 | 462 | path => '{vmid}', |
1e3baf05 DM |
463 | method => 'GET', |
464 | proxyto => 'node', | |
465 | description => "Directory index", | |
a0d1b1a2 DM |
466 | permissions => { |
467 | user => 'all', | |
468 | }, | |
1e3baf05 DM |
469 | parameters => { |
470 | additionalProperties => 0, | |
471 | properties => { | |
472 | node => get_standard_option('pve-node'), | |
473 | vmid => get_standard_option('pve-vmid'), | |
474 | }, | |
475 | }, | |
476 | returns => { | |
477 | type => 'array', | |
478 | items => { | |
479 | type => "object", | |
480 | properties => { | |
481 | subdir => { type => 'string' }, | |
482 | }, | |
483 | }, | |
484 | links => [ { rel => 'child', href => "{subdir}" } ], | |
485 | }, | |
486 | code => sub { | |
487 | my ($param) = @_; | |
488 | ||
489 | my $res = [ | |
490 | { subdir => 'config' }, | |
491 | { subdir => 'status' }, | |
492 | { subdir => 'unlink' }, | |
493 | { subdir => 'vncproxy' }, | |
3ea94c60 | 494 | { subdir => 'migrate' }, |
2f48a4f5 | 495 | { subdir => 'resize' }, |
586bfa78 | 496 | { subdir => 'move' }, |
1e3baf05 DM |
497 | { subdir => 'rrd' }, |
498 | { subdir => 'rrddata' }, | |
91c94f0a | 499 | { subdir => 'monitor' }, |
7e7d7b61 | 500 | { subdir => 'snapshot' }, |
288eeea8 | 501 | { subdir => 'spiceproxy' }, |
1e3baf05 | 502 | ]; |
afdb31d5 | 503 | |
1e3baf05 DM |
504 | return $res; |
505 | }}); | |
506 | ||
507 | __PACKAGE__->register_method({ | |
afdb31d5 DM |
508 | name => 'rrd', |
509 | path => '{vmid}/rrd', | |
1e3baf05 DM |
510 | method => 'GET', |
511 | protected => 1, # fixme: can we avoid that? | |
512 | permissions => { | |
378b359e | 513 | check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]], |
1e3baf05 DM |
514 | }, |
515 | description => "Read VM RRD statistics (returns PNG)", | |
516 | parameters => { | |
517 | additionalProperties => 0, | |
518 | properties => { | |
519 | node => get_standard_option('pve-node'), | |
520 | vmid => get_standard_option('pve-vmid'), | |
521 | timeframe => { | |
522 | description => "Specify the time frame you are interested in.", | |
523 | type => 'string', | |
524 | enum => [ 'hour', 'day', 'week', 'month', 'year' ], | |
525 | }, | |
526 | ds => { | |
527 | description => "The list of datasources you want to display.", | |
528 | type => 'string', format => 'pve-configid-list', | |
529 | }, | |
530 | cf => { | |
531 | description => "The RRD consolidation function", | |
532 | type => 'string', | |
533 | enum => [ 'AVERAGE', 'MAX' ], | |
534 | optional => 1, | |
535 | }, | |
536 | }, | |
537 | }, | |
538 | returns => { | |
539 | type => "object", | |
540 | properties => { | |
541 | filename => { type => 'string' }, | |
542 | }, | |
543 | }, | |
544 | code => sub { | |
545 | my ($param) = @_; | |
546 | ||
547 | return PVE::Cluster::create_rrd_graph( | |
afdb31d5 | 548 | "pve2-vm/$param->{vmid}", $param->{timeframe}, |
1e3baf05 | 549 | $param->{ds}, $param->{cf}); |
afdb31d5 | 550 | |
1e3baf05 DM |
551 | }}); |
552 | ||
553 | __PACKAGE__->register_method({ | |
afdb31d5 DM |
554 | name => 'rrddata', |
555 | path => '{vmid}/rrddata', | |
1e3baf05 DM |
556 | method => 'GET', |
557 | protected => 1, # fixme: can we avoid that? | |
558 | permissions => { | |
378b359e | 559 | check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]], |
1e3baf05 DM |
560 | }, |
561 | description => "Read VM RRD statistics", | |
562 | parameters => { | |
563 | additionalProperties => 0, | |
564 | properties => { | |
565 | node => get_standard_option('pve-node'), | |
566 | vmid => get_standard_option('pve-vmid'), | |
567 | timeframe => { | |
568 | description => "Specify the time frame you are interested in.", | |
569 | type => 'string', | |
570 | enum => [ 'hour', 'day', 'week', 'month', 'year' ], | |
571 | }, | |
572 | cf => { | |
573 | description => "The RRD consolidation function", | |
574 | type => 'string', | |
575 | enum => [ 'AVERAGE', 'MAX' ], | |
576 | optional => 1, | |
577 | }, | |
578 | }, | |
579 | }, | |
580 | returns => { | |
581 | type => "array", | |
582 | items => { | |
583 | type => "object", | |
584 | properties => {}, | |
585 | }, | |
586 | }, | |
587 | code => sub { | |
588 | my ($param) = @_; | |
589 | ||
590 | return PVE::Cluster::create_rrd_data( | |
591 | "pve2-vm/$param->{vmid}", $param->{timeframe}, $param->{cf}); | |
592 | }}); | |
593 | ||
594 | ||
595 | __PACKAGE__->register_method({ | |
afdb31d5 DM |
596 | name => 'vm_config', |
597 | path => '{vmid}/config', | |
1e3baf05 DM |
598 | method => 'GET', |
599 | proxyto => 'node', | |
600 | description => "Get virtual machine configuration.", | |
a0d1b1a2 DM |
601 | permissions => { |
602 | check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]], | |
603 | }, | |
1e3baf05 DM |
604 | parameters => { |
605 | additionalProperties => 0, | |
606 | properties => { | |
607 | node => get_standard_option('pve-node'), | |
608 | vmid => get_standard_option('pve-vmid'), | |
609 | }, | |
610 | }, | |
afdb31d5 | 611 | returns => { |
1e3baf05 | 612 | type => "object", |
554ac7e7 DM |
613 | properties => { |
614 | digest => { | |
615 | type => 'string', | |
616 | description => 'SHA1 digest of configuration file. This can be used to prevent concurrent modifications.', | |
617 | } | |
618 | }, | |
1e3baf05 DM |
619 | }, |
620 | code => sub { | |
621 | my ($param) = @_; | |
622 | ||
623 | my $conf = PVE::QemuServer::load_config($param->{vmid}); | |
624 | ||
22c377f0 DM |
625 | delete $conf->{snapshots}; |
626 | ||
1e3baf05 DM |
627 | return $conf; |
628 | }}); | |
629 | ||
ae57f6b3 DM |
630 | my $vm_is_volid_owner = sub { |
631 | my ($storecfg, $vmid, $volid) =@_; | |
632 | ||
633 | if ($volid !~ m|^/|) { | |
634 | my ($path, $owner); | |
635 | eval { ($path, $owner) = PVE::Storage::path($storecfg, $volid); }; | |
636 | if ($owner && ($owner == $vmid)) { | |
637 | return 1; | |
638 | } | |
639 | } | |
640 | ||
641 | return undef; | |
642 | }; | |
643 | ||
644 | my $test_deallocate_drive = sub { | |
645 | my ($storecfg, $vmid, $key, $drive, $force) = @_; | |
646 | ||
647 | if (!PVE::QemuServer::drive_is_cdrom($drive)) { | |
648 | my $volid = $drive->{file}; | |
649 | if (&$vm_is_volid_owner($storecfg, $vmid, $volid)) { | |
650 | if ($force || $key =~ m/^unused/) { | |
651 | my $sid = PVE::Storage::parse_volume_id($volid); | |
652 | return $sid; | |
653 | } | |
654 | } | |
655 | } | |
656 | ||
657 | return undef; | |
658 | }; | |
659 | ||
5d7a6767 | 660 | my $delete_drive = sub { |
1858638f | 661 | my ($conf, $storecfg, $vmid, $key, $drive, $force) = @_; |
5d7a6767 DM |
662 | |
663 | if (!PVE::QemuServer::drive_is_cdrom($drive)) { | |
664 | my $volid = $drive->{file}; | |
a8e2f942 | 665 | |
ae57f6b3 | 666 | if (&$vm_is_volid_owner($storecfg, $vmid, $volid)) { |
7043d946 DM |
667 | if ($force || $key =~ m/^unused/) { |
668 | eval { | |
669 | # check if the disk is really unused | |
a8e2f942 | 670 | my $used_paths = PVE::QemuServer::get_used_paths($vmid, $storecfg, $conf, 1, $key); |
7043d946 | 671 | my $path = PVE::Storage::path($storecfg, $volid); |
a8e2f942 DM |
672 | |
673 | die "unable to delete '$volid' - volume is still in use (snapshot?)\n" | |
674 | if $used_paths->{$path}; | |
675 | ||
7043d946 | 676 | PVE::Storage::vdisk_free($storecfg, $volid); |
a8e2f942 | 677 | }; |
7e4e69a6 | 678 | die $@ if $@; |
ae57f6b3 DM |
679 | } else { |
680 | PVE::QemuServer::add_unused_volume($conf, $volid, $vmid); | |
5d7a6767 | 681 | } |
ae57f6b3 DM |
682 | } |
683 | } | |
49f9db93 DM |
684 | |
685 | delete $conf->{$key}; | |
ae57f6b3 DM |
686 | }; |
687 | ||
688 | my $vmconfig_delete_option = sub { | |
689 | my ($rpcenv, $authuser, $conf, $storecfg, $vmid, $opt, $force) = @_; | |
690 | ||
691 | return if !defined($conf->{$opt}); | |
692 | ||
693 | my $isDisk = PVE::QemuServer::valid_drivename($opt)|| ($opt =~ m/^unused/); | |
694 | ||
695 | if ($isDisk) { | |
696 | $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']); | |
697 | ||
698 | my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt}); | |
75466c4f | 699 | if (my $sid = &$test_deallocate_drive($storecfg, $vmid, $opt, $drive, $force)) { |
fcbb753e | 700 | $rpcenv->check($authuser, "/storage/$sid", ['Datastore.Allocate']); |
5d7a6767 | 701 | } |
ae57f6b3 | 702 | } |
9a8d6b66 AD |
703 | |
704 | my $unplugwarning = ""; | |
705 | if($conf->{ostype} && $conf->{ostype} eq 'l26'){ | |
706 | $unplugwarning = "<br>verify that you have acpiphp && pci_hotplug modules loaded in your guest VM"; | |
707 | }elsif($conf->{ostype} && $conf->{ostype} eq 'l24'){ | |
708 | $unplugwarning = "<br>kernel 2.4 don't support hotplug, please disable hotplug in options"; | |
709 | }elsif(!$conf->{ostype} || ($conf->{ostype} && $conf->{ostype} eq 'other')){ | |
710 | $unplugwarning = "<br>verify that your guest support acpi hotplug"; | |
711 | } | |
712 | ||
e8a7e9b4 AD |
713 | if($opt eq 'tablet'){ |
714 | PVE::QemuServer::vm_deviceplug(undef, $conf, $vmid, $opt); | |
715 | }else{ | |
716 | die "error hot-unplug $opt $unplugwarning" if !PVE::QemuServer::vm_deviceunplug($vmid, $conf, $opt); | |
717 | } | |
cd6ecb89 | 718 | |
ae57f6b3 DM |
719 | if ($isDisk) { |
720 | my $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt}); | |
721 | &$delete_drive($conf, $storecfg, $vmid, $opt, $drive, $force); | |
5d7a6767 | 722 | } else { |
ae57f6b3 DM |
723 | delete $conf->{$opt}; |
724 | } | |
725 | ||
726 | PVE::QemuServer::update_config_nolock($vmid, $conf, 1); | |
727 | }; | |
728 | ||
9bf371a6 | 729 | my $safe_num_ne = sub { |
93ae06e1 DM |
730 | my ($a, $b) = @_; |
731 | ||
75466c4f DM |
732 | return 0 if !defined($a) && !defined($b); |
733 | return 1 if !defined($a); | |
734 | return 1 if !defined($b); | |
93ae06e1 DM |
735 | |
736 | return $a != $b; | |
737 | }; | |
738 | ||
ae57f6b3 DM |
739 | my $vmconfig_update_disk = sub { |
740 | my ($rpcenv, $authuser, $conf, $storecfg, $vmid, $opt, $value, $force) = @_; | |
5d7a6767 | 741 | |
ae57f6b3 DM |
742 | my $drive = PVE::QemuServer::parse_drive($opt, $value); |
743 | ||
744 | if (PVE::QemuServer::drive_is_cdrom($drive)) { #cdrom | |
745 | $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.CDROM']); | |
746 | } else { | |
747 | $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk']); | |
748 | } | |
749 | ||
750 | if ($conf->{$opt}) { | |
751 | ||
752 | if (my $old_drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt})) { | |
753 | ||
754 | my $media = $drive->{media} || 'disk'; | |
755 | my $oldmedia = $old_drive->{media} || 'disk'; | |
756 | die "unable to change media type\n" if $media ne $oldmedia; | |
757 | ||
758 | if (!PVE::QemuServer::drive_is_cdrom($old_drive) && | |
759 | ($drive->{file} ne $old_drive->{file})) { # delete old disks | |
760 | ||
761 | &$vmconfig_delete_option($rpcenv, $authuser, $conf, $storecfg, $vmid, $opt, $force); | |
762 | $conf = PVE::QemuServer::load_config($vmid); # update/reload | |
763 | } | |
0f56d571 | 764 | |
9bf371a6 DM |
765 | if(&$safe_num_ne($drive->{mbps}, $old_drive->{mbps}) || |
766 | &$safe_num_ne($drive->{mbps_rd}, $old_drive->{mbps_rd}) || | |
767 | &$safe_num_ne($drive->{mbps_wr}, $old_drive->{mbps_wr}) || | |
768 | &$safe_num_ne($drive->{iops}, $old_drive->{iops}) || | |
769 | &$safe_num_ne($drive->{iops_rd}, $old_drive->{iops_rd}) || | |
770 | &$safe_num_ne($drive->{iops_wr}, $old_drive->{iops_wr})) { | |
9b2c0efb DM |
771 | PVE::QemuServer::qemu_block_set_io_throttle($vmid,"drive-$opt", |
772 | ($drive->{mbps} || 0)*1024*1024, | |
773 | ($drive->{mbps_rd} || 0)*1024*1024, | |
774 | ($drive->{mbps_wr} || 0)*1024*1024, | |
775 | $drive->{iops} || 0, | |
776 | $drive->{iops_rd} || 0, | |
777 | $drive->{iops_wr} || 0) | |
9bf371a6 | 778 | if !PVE::QemuServer::drive_is_cdrom($drive); |
0f56d571 | 779 | } |
ae57f6b3 DM |
780 | } |
781 | } | |
782 | ||
783 | &$create_disks($rpcenv, $authuser, $conf, $storecfg, $vmid, undef, {$opt => $value}); | |
784 | PVE::QemuServer::update_config_nolock($vmid, $conf, 1); | |
785 | ||
786 | $conf = PVE::QemuServer::load_config($vmid); # update/reload | |
787 | $drive = PVE::QemuServer::parse_drive($opt, $conf->{$opt}); | |
788 | ||
789 | if (PVE::QemuServer::drive_is_cdrom($drive)) { # cdrom | |
790 | ||
791 | if (PVE::QemuServer::check_running($vmid)) { | |
792 | if ($drive->{file} eq 'none') { | |
ce156282 | 793 | PVE::QemuServer::vm_mon_cmd($vmid, "eject",force => JSON::true,device => "drive-$opt"); |
ae57f6b3 DM |
794 | } else { |
795 | my $path = PVE::QemuServer::get_iso_path($storecfg, $vmid, $drive->{file}); | |
ce156282 AD |
796 | PVE::QemuServer::vm_mon_cmd($vmid, "eject",force => JSON::true,device => "drive-$opt"); #force eject if locked |
797 | PVE::QemuServer::vm_mon_cmd($vmid, "change",device => "drive-$opt",target => "$path") if $path; | |
ae57f6b3 DM |
798 | } |
799 | } | |
800 | ||
801 | } else { # hotplug new disks | |
802 | ||
803 | die "error hotplug $opt" if !PVE::QemuServer::vm_deviceplug($storecfg, $conf, $vmid, $opt, $drive); | |
5d7a6767 | 804 | } |
5d7a6767 DM |
805 | }; |
806 | ||
ae57f6b3 DM |
807 | my $vmconfig_update_net = sub { |
808 | my ($rpcenv, $authuser, $conf, $storecfg, $vmid, $opt, $value) = @_; | |
809 | ||
de8f60b2 AD |
810 | if ($conf->{$opt} && PVE::QemuServer::check_running($vmid)) { |
811 | my $oldnet = PVE::QemuServer::parse_net($conf->{$opt}); | |
812 | my $newnet = PVE::QemuServer::parse_net($value); | |
813 | ||
814 | if($oldnet->{model} ne $newnet->{model}){ | |
815 | #if model change, we try to hot-unplug | |
816 | die "error hot-unplug $opt for update" if !PVE::QemuServer::vm_deviceunplug($vmid, $conf, $opt); | |
817 | }else{ | |
75466c4f | 818 | |
de8f60b2 AD |
819 | if($newnet->{bridge} && $oldnet->{bridge}){ |
820 | my $iface = "tap".$vmid."i".$1 if $opt =~ m/net(\d+)/; | |
ae57f6b3 | 821 | |
de8f60b2 AD |
822 | if($newnet->{rate} ne $oldnet->{rate}){ |
823 | PVE::Network::tap_rate_limit($iface, $newnet->{rate}); | |
824 | } | |
825 | ||
826 | if(($newnet->{bridge} ne $oldnet->{bridge}) || ($newnet->{tag} ne $oldnet->{tag})){ | |
827 | eval{PVE::Network::tap_unplug($iface, $oldnet->{bridge}, $oldnet->{tag});}; | |
828 | PVE::Network::tap_plug($iface, $newnet->{bridge}, $newnet->{tag}); | |
829 | } | |
830 | ||
831 | }else{ | |
832 | #if bridge/nat mode change, we try to hot-unplug | |
833 | die "error hot-unplug $opt for update" if !PVE::QemuServer::vm_deviceunplug($vmid, $conf, $opt); | |
834 | } | |
835 | } | |
75466c4f | 836 | |
de8f60b2 | 837 | } |
ae57f6b3 DM |
838 | $conf->{$opt} = $value; |
839 | PVE::QemuServer::update_config_nolock($vmid, $conf, 1); | |
840 | $conf = PVE::QemuServer::load_config($vmid); # update/reload | |
841 | ||
842 | my $net = PVE::QemuServer::parse_net($conf->{$opt}); | |
843 | ||
844 | die "error hotplug $opt" if !PVE::QemuServer::vm_deviceplug($storecfg, $conf, $vmid, $opt, $net); | |
845 | }; | |
846 | ||
5555edea DM |
847 | # POST/PUT {vmid}/config implementation |
848 | # | |
849 | # The original API used PUT (idempotent) an we assumed that all operations | |
850 | # are fast. But it turned out that almost any configuration change can | |
851 | # involve hot-plug actions, or disk alloc/free. Such actions can take long | |
852 | # time to complete and have side effects (not idempotent). | |
853 | # | |
7043d946 | 854 | # The new implementation uses POST and forks a worker process. We added |
5555edea | 855 | # a new option 'background_delay'. If specified we wait up to |
7043d946 | 856 | # 'background_delay' second for the worker task to complete. It returns null |
5555edea | 857 | # if the task is finished within that time, else we return the UPID. |
7043d946 | 858 | |
5555edea DM |
859 | my $update_vm_api = sub { |
860 | my ($param, $sync) = @_; | |
a0d1b1a2 | 861 | |
5555edea | 862 | my $rpcenv = PVE::RPCEnvironment::get(); |
1e3baf05 | 863 | |
5555edea | 864 | my $authuser = $rpcenv->get_user(); |
1e3baf05 | 865 | |
5555edea | 866 | my $node = extract_param($param, 'node'); |
1e3baf05 | 867 | |
5555edea | 868 | my $vmid = extract_param($param, 'vmid'); |
1e3baf05 | 869 | |
5555edea | 870 | my $digest = extract_param($param, 'digest'); |
1e3baf05 | 871 | |
5555edea | 872 | my $background_delay = extract_param($param, 'background_delay'); |
1e3baf05 | 873 | |
5555edea DM |
874 | my @paramarr = (); # used for log message |
875 | foreach my $key (keys %$param) { | |
876 | push @paramarr, "-$key", $param->{$key}; | |
877 | } | |
0532bc63 | 878 | |
5555edea DM |
879 | my $skiplock = extract_param($param, 'skiplock'); |
880 | raise_param_exc({ skiplock => "Only root may use this option." }) | |
881 | if $skiplock && $authuser ne 'root@pam'; | |
1e3baf05 | 882 | |
5555edea | 883 | my $delete_str = extract_param($param, 'delete'); |
0532bc63 | 884 | |
5555edea | 885 | my $force = extract_param($param, 'force'); |
1e68cb19 | 886 | |
5555edea | 887 | die "no options specified\n" if !$delete_str && !scalar(keys %$param); |
7bfdeb5f | 888 | |
5555edea | 889 | my $storecfg = PVE::Storage::config(); |
1e68cb19 | 890 | |
5555edea | 891 | my $defaults = PVE::QemuServer::load_defaults(); |
1e68cb19 | 892 | |
5555edea | 893 | &$resolve_cdrom_alias($param); |
0532bc63 | 894 | |
5555edea | 895 | # now try to verify all parameters |
ae57f6b3 | 896 | |
5555edea DM |
897 | my @delete = (); |
898 | foreach my $opt (PVE::Tools::split_list($delete_str)) { | |
899 | $opt = 'ide2' if $opt eq 'cdrom'; | |
900 | raise_param_exc({ delete => "you can't use '-$opt' and " . | |
901 | "-delete $opt' at the same time" }) | |
902 | if defined($param->{$opt}); | |
7043d946 | 903 | |
5555edea DM |
904 | if (!PVE::QemuServer::option_exists($opt)) { |
905 | raise_param_exc({ delete => "unknown option '$opt'" }); | |
0532bc63 | 906 | } |
1e3baf05 | 907 | |
5555edea DM |
908 | push @delete, $opt; |
909 | } | |
910 | ||
911 | foreach my $opt (keys %$param) { | |
912 | if (PVE::QemuServer::valid_drivename($opt)) { | |
913 | # cleanup drive path | |
914 | my $drive = PVE::QemuServer::parse_drive($opt, $param->{$opt}); | |
915 | PVE::QemuServer::cleanup_drive_path($opt, $storecfg, $drive); | |
916 | $param->{$opt} = PVE::QemuServer::print_drive($vmid, $drive); | |
917 | } elsif ($opt =~ m/^net(\d+)$/) { | |
918 | # add macaddr | |
919 | my $net = PVE::QemuServer::parse_net($param->{$opt}); | |
920 | $param->{$opt} = PVE::QemuServer::print_net($net); | |
1e68cb19 | 921 | } |
5555edea | 922 | } |
1e3baf05 | 923 | |
5555edea | 924 | &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [@delete]); |
ae57f6b3 | 925 | |
5555edea | 926 | &$check_vm_modify_config_perm($rpcenv, $authuser, $vmid, undef, [keys %$param]); |
ae57f6b3 | 927 | |
5555edea | 928 | &$check_storage_access($rpcenv, $authuser, $storecfg, $vmid, $param); |
1e3baf05 | 929 | |
5555edea | 930 | my $updatefn = sub { |
1e3baf05 | 931 | |
5555edea | 932 | my $conf = PVE::QemuServer::load_config($vmid); |
1e3baf05 | 933 | |
5555edea DM |
934 | die "checksum missmatch (file change by other user?)\n" |
935 | if $digest && $digest ne $conf->{digest}; | |
936 | ||
937 | PVE::QemuServer::check_lock($conf) if !$skiplock; | |
7043d946 | 938 | |
5555edea DM |
939 | if ($param->{memory} || defined($param->{balloon})) { |
940 | my $maxmem = $param->{memory} || $conf->{memory} || $defaults->{memory}; | |
941 | my $balloon = defined($param->{balloon}) ? $param->{balloon} : $conf->{balloon}; | |
7043d946 | 942 | |
5555edea DM |
943 | die "balloon value too large (must be smaller than assigned memory)\n" |
944 | if $balloon && $balloon > $maxmem; | |
945 | } | |
1e3baf05 | 946 | |
5555edea | 947 | PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: " . join (' ', @paramarr)); |
1e3baf05 | 948 | |
5555edea | 949 | my $worker = sub { |
7bfdeb5f | 950 | |
5555edea | 951 | print "update VM $vmid: " . join (' ', @paramarr) . "\n"; |
c2a64aa7 | 952 | |
5d7a6767 | 953 | foreach my $opt (@delete) { # delete |
1e68cb19 | 954 | $conf = PVE::QemuServer::load_config($vmid); # update/reload |
ae57f6b3 | 955 | &$vmconfig_delete_option($rpcenv, $authuser, $conf, $storecfg, $vmid, $opt, $force); |
5d39a182 | 956 | } |
1e3baf05 | 957 | |
7bfdeb5f | 958 | my $running = PVE::QemuServer::check_running($vmid); |
7043d946 | 959 | |
5d7a6767 | 960 | foreach my $opt (keys %$param) { # add/change |
7043d946 | 961 | |
1e68cb19 | 962 | $conf = PVE::QemuServer::load_config($vmid); # update/reload |
7043d946 | 963 | |
5d7a6767 DM |
964 | next if $conf->{$opt} && ($param->{$opt} eq $conf->{$opt}); # skip if nothing changed |
965 | ||
ae57f6b3 | 966 | if (PVE::QemuServer::valid_drivename($opt)) { |
5d7a6767 | 967 | |
75466c4f | 968 | &$vmconfig_update_disk($rpcenv, $authuser, $conf, $storecfg, $vmid, |
ae57f6b3 | 969 | $opt, $param->{$opt}, $force); |
75466c4f | 970 | |
ae57f6b3 | 971 | } elsif ($opt =~ m/^net(\d+)$/) { #nics |
1858638f | 972 | |
75466c4f | 973 | &$vmconfig_update_net($rpcenv, $authuser, $conf, $storecfg, $vmid, |
ae57f6b3 | 974 | $opt, $param->{$opt}); |
1e68cb19 DM |
975 | |
976 | } else { | |
977 | ||
cd6ecb89 AD |
978 | if($opt eq 'tablet' && $param->{$opt} == 1){ |
979 | PVE::QemuServer::vm_deviceplug(undef, $conf, $vmid, $opt); | |
5555edea | 980 | } elsif($opt eq 'tablet' && $param->{$opt} == 0){ |
cd6ecb89 AD |
981 | PVE::QemuServer::vm_deviceunplug($vmid, $conf, $opt); |
982 | } | |
983 | ||
1858638f DM |
984 | $conf->{$opt} = $param->{$opt}; |
985 | PVE::QemuServer::update_config_nolock($vmid, $conf, 1); | |
3a1e36bb | 986 | } |
5d39a182 | 987 | } |
7bfdeb5f DM |
988 | |
989 | # allow manual ballooning if shares is set to zero | |
75466c4f | 990 | if ($running && defined($param->{balloon}) && |
7bfdeb5f DM |
991 | defined($conf->{shares}) && ($conf->{shares} == 0)) { |
992 | my $balloon = $param->{'balloon'} || $conf->{memory} || $defaults->{memory}; | |
993 | PVE::QemuServer::vm_mon_cmd($vmid, "balloon", value => $balloon*1024*1024); | |
994 | } | |
5d39a182 DM |
995 | }; |
996 | ||
5555edea DM |
997 | if ($sync) { |
998 | &$worker(); | |
999 | return undef; | |
1000 | } else { | |
1001 | my $upid = $rpcenv->fork_worker('qmconfig', $vmid, $authuser, $worker); | |
fcdb0117 | 1002 | |
5555edea DM |
1003 | if ($background_delay) { |
1004 | ||
1005 | # Note: It would be better to do that in the Event based HTTPServer | |
7043d946 | 1006 | # to avoid blocking call to sleep. |
5555edea DM |
1007 | |
1008 | my $end_time = time() + $background_delay; | |
1009 | ||
1010 | my $task = PVE::Tools::upid_decode($upid); | |
1011 | ||
1012 | my $running = 1; | |
1013 | while (time() < $end_time) { | |
1014 | $running = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart}); | |
1015 | last if !$running; | |
1016 | sleep(1); # this gets interrupted when child process ends | |
1017 | } | |
1018 | ||
1019 | if (!$running) { | |
1020 | my $status = PVE::Tools::upid_read_status($upid); | |
1021 | return undef if $status eq 'OK'; | |
1022 | die $status; | |
1023 | } | |
7043d946 | 1024 | } |
5555edea DM |
1025 | |
1026 | return $upid; | |
1027 | } | |
1028 | }; | |
1029 | ||
1030 | return PVE::QemuServer::lock_config($vmid, $updatefn); | |
1031 | }; | |
1032 | ||
1033 | my $vm_config_perm_list = [ | |
1034 | 'VM.Config.Disk', | |
1035 | 'VM.Config.CDROM', | |
1036 | 'VM.Config.CPU', | |
1037 | 'VM.Config.Memory', | |
1038 | 'VM.Config.Network', | |
1039 | 'VM.Config.HWType', | |
1040 | 'VM.Config.Options', | |
1041 | ]; | |
1042 | ||
1043 | __PACKAGE__->register_method({ | |
1044 | name => 'update_vm_async', | |
1045 | path => '{vmid}/config', | |
1046 | method => 'POST', | |
1047 | protected => 1, | |
1048 | proxyto => 'node', | |
1049 | description => "Set virtual machine options (asynchrounous API).", | |
1050 | permissions => { | |
1051 | check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1], | |
1052 | }, | |
1053 | parameters => { | |
1054 | additionalProperties => 0, | |
1055 | properties => PVE::QemuServer::json_config_properties( | |
1056 | { | |
1057 | node => get_standard_option('pve-node'), | |
1058 | vmid => get_standard_option('pve-vmid'), | |
1059 | skiplock => get_standard_option('skiplock'), | |
1060 | delete => { | |
1061 | type => 'string', format => 'pve-configid-list', | |
1062 | description => "A list of settings you want to delete.", | |
1063 | optional => 1, | |
1064 | }, | |
1065 | force => { | |
1066 | type => 'boolean', | |
1067 | description => $opt_force_description, | |
1068 | optional => 1, | |
1069 | requires => 'delete', | |
1070 | }, | |
1071 | digest => { | |
1072 | type => 'string', | |
1073 | description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.', | |
1074 | maxLength => 40, | |
1075 | optional => 1, | |
1076 | }, | |
1077 | background_delay => { | |
1078 | type => 'integer', | |
1079 | description => "Time to wait for the task to finish. We return 'null' if the task finish within that time.", | |
1080 | minimum => 1, | |
1081 | maximum => 30, | |
1082 | optional => 1, | |
1083 | }, | |
1084 | }), | |
1085 | }, | |
1086 | returns => { | |
1087 | type => 'string', | |
1088 | optional => 1, | |
1089 | }, | |
1090 | code => $update_vm_api, | |
1091 | }); | |
1092 | ||
1093 | __PACKAGE__->register_method({ | |
1094 | name => 'update_vm', | |
1095 | path => '{vmid}/config', | |
1096 | method => 'PUT', | |
1097 | protected => 1, | |
1098 | proxyto => 'node', | |
1099 | description => "Set virtual machine options (synchrounous API) - You should consider using the POST method instead for any actions involving hotplug or storage allocation.", | |
1100 | permissions => { | |
1101 | check => ['perm', '/vms/{vmid}', $vm_config_perm_list, any => 1], | |
1102 | }, | |
1103 | parameters => { | |
1104 | additionalProperties => 0, | |
1105 | properties => PVE::QemuServer::json_config_properties( | |
1106 | { | |
1107 | node => get_standard_option('pve-node'), | |
1108 | vmid => get_standard_option('pve-vmid'), | |
1109 | skiplock => get_standard_option('skiplock'), | |
1110 | delete => { | |
1111 | type => 'string', format => 'pve-configid-list', | |
1112 | description => "A list of settings you want to delete.", | |
1113 | optional => 1, | |
1114 | }, | |
1115 | force => { | |
1116 | type => 'boolean', | |
1117 | description => $opt_force_description, | |
1118 | optional => 1, | |
1119 | requires => 'delete', | |
1120 | }, | |
1121 | digest => { | |
1122 | type => 'string', | |
1123 | description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.', | |
1124 | maxLength => 40, | |
1125 | optional => 1, | |
1126 | }, | |
1127 | }), | |
1128 | }, | |
1129 | returns => { type => 'null' }, | |
1130 | code => sub { | |
1131 | my ($param) = @_; | |
1132 | &$update_vm_api($param, 1); | |
1e3baf05 | 1133 | return undef; |
5555edea DM |
1134 | } |
1135 | }); | |
1e3baf05 DM |
1136 | |
1137 | ||
1138 | __PACKAGE__->register_method({ | |
afdb31d5 DM |
1139 | name => 'destroy_vm', |
1140 | path => '{vmid}', | |
1e3baf05 DM |
1141 | method => 'DELETE', |
1142 | protected => 1, | |
1143 | proxyto => 'node', | |
1144 | description => "Destroy the vm (also delete all used/owned volumes).", | |
a0d1b1a2 DM |
1145 | permissions => { |
1146 | check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']], | |
1147 | }, | |
1e3baf05 DM |
1148 | parameters => { |
1149 | additionalProperties => 0, | |
1150 | properties => { | |
1151 | node => get_standard_option('pve-node'), | |
1152 | vmid => get_standard_option('pve-vmid'), | |
3ea94c60 | 1153 | skiplock => get_standard_option('skiplock'), |
1e3baf05 DM |
1154 | }, |
1155 | }, | |
afdb31d5 | 1156 | returns => { |
5fdbe4f0 DM |
1157 | type => 'string', |
1158 | }, | |
1e3baf05 DM |
1159 | code => sub { |
1160 | my ($param) = @_; | |
1161 | ||
1162 | my $rpcenv = PVE::RPCEnvironment::get(); | |
1163 | ||
a0d1b1a2 | 1164 | my $authuser = $rpcenv->get_user(); |
1e3baf05 DM |
1165 | |
1166 | my $vmid = $param->{vmid}; | |
1167 | ||
1168 | my $skiplock = $param->{skiplock}; | |
afdb31d5 | 1169 | raise_param_exc({ skiplock => "Only root may use this option." }) |
a0d1b1a2 | 1170 | if $skiplock && $authuser ne 'root@pam'; |
1e3baf05 | 1171 | |
5fdbe4f0 DM |
1172 | # test if VM exists |
1173 | my $conf = PVE::QemuServer::load_config($vmid); | |
1174 | ||
afdb31d5 | 1175 | my $storecfg = PVE::Storage::config(); |
1e3baf05 | 1176 | |
75466c4f | 1177 | my $delVMfromPoolFn = sub { |
502d18a2 | 1178 | my $usercfg = cfs_read_file("user.cfg"); |
5d0094ea DM |
1179 | if (my $pool = $usercfg->{vms}->{$vmid}) { |
1180 | if (my $data = $usercfg->{pools}->{$pool}) { | |
1181 | delete $data->{vms}->{$vmid}; | |
1182 | delete $usercfg->{vms}->{$vmid}; | |
1183 | cfs_write_file("user.cfg", $usercfg); | |
1184 | } | |
502d18a2 DM |
1185 | } |
1186 | }; | |
1187 | ||
5fdbe4f0 | 1188 | my $realcmd = sub { |
ff1a2432 DM |
1189 | my $upid = shift; |
1190 | ||
1191 | syslog('info', "destroy VM $vmid: $upid\n"); | |
1192 | ||
5fdbe4f0 | 1193 | PVE::QemuServer::vm_destroy($storecfg, $vmid, $skiplock); |
502d18a2 | 1194 | |
be517049 | 1195 | PVE::AccessControl::remove_vm_from_pool($vmid); |
5fdbe4f0 | 1196 | }; |
1e3baf05 | 1197 | |
a0d1b1a2 | 1198 | return $rpcenv->fork_worker('qmdestroy', $vmid, $authuser, $realcmd); |
1e3baf05 DM |
1199 | }}); |
1200 | ||
1201 | __PACKAGE__->register_method({ | |
afdb31d5 DM |
1202 | name => 'unlink', |
1203 | path => '{vmid}/unlink', | |
1e3baf05 DM |
1204 | method => 'PUT', |
1205 | protected => 1, | |
1206 | proxyto => 'node', | |
1207 | description => "Unlink/delete disk images.", | |
a0d1b1a2 DM |
1208 | permissions => { |
1209 | check => [ 'perm', '/vms/{vmid}', ['VM.Config.Disk']], | |
1210 | }, | |
1e3baf05 DM |
1211 | parameters => { |
1212 | additionalProperties => 0, | |
1213 | properties => { | |
1214 | node => get_standard_option('pve-node'), | |
1215 | vmid => get_standard_option('pve-vmid'), | |
1216 | idlist => { | |
1217 | type => 'string', format => 'pve-configid-list', | |
1218 | description => "A list of disk IDs you want to delete.", | |
1219 | }, | |
1220 | force => { | |
1221 | type => 'boolean', | |
1222 | description => $opt_force_description, | |
1223 | optional => 1, | |
1224 | }, | |
1225 | }, | |
1226 | }, | |
1227 | returns => { type => 'null'}, | |
1228 | code => sub { | |
1229 | my ($param) = @_; | |
1230 | ||
1231 | $param->{delete} = extract_param($param, 'idlist'); | |
1232 | ||
1233 | __PACKAGE__->update_vm($param); | |
1234 | ||
1235 | return undef; | |
1236 | }}); | |
1237 | ||
1238 | my $sslcert; | |
1239 | ||
1240 | __PACKAGE__->register_method({ | |
afdb31d5 DM |
1241 | name => 'vncproxy', |
1242 | path => '{vmid}/vncproxy', | |
1e3baf05 DM |
1243 | method => 'POST', |
1244 | protected => 1, | |
1245 | permissions => { | |
378b359e | 1246 | check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]], |
1e3baf05 DM |
1247 | }, |
1248 | description => "Creates a TCP VNC proxy connections.", | |
1249 | parameters => { | |
1250 | additionalProperties => 0, | |
1251 | properties => { | |
1252 | node => get_standard_option('pve-node'), | |
1253 | vmid => get_standard_option('pve-vmid'), | |
1254 | }, | |
1255 | }, | |
afdb31d5 | 1256 | returns => { |
1e3baf05 DM |
1257 | additionalProperties => 0, |
1258 | properties => { | |
1259 | user => { type => 'string' }, | |
1260 | ticket => { type => 'string' }, | |
1261 | cert => { type => 'string' }, | |
1262 | port => { type => 'integer' }, | |
1263 | upid => { type => 'string' }, | |
1264 | }, | |
1265 | }, | |
1266 | code => sub { | |
1267 | my ($param) = @_; | |
1268 | ||
1269 | my $rpcenv = PVE::RPCEnvironment::get(); | |
1270 | ||
a0d1b1a2 | 1271 | my $authuser = $rpcenv->get_user(); |
1e3baf05 DM |
1272 | |
1273 | my $vmid = $param->{vmid}; | |
1274 | my $node = $param->{node}; | |
1275 | ||
ef5e2be2 DM |
1276 | my $conf = PVE::QemuServer::load_config($vmid); # check if VM exists |
1277 | ||
b6f39da2 DM |
1278 | my $authpath = "/vms/$vmid"; |
1279 | ||
a0d1b1a2 | 1280 | my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath); |
b6f39da2 | 1281 | |
1e3baf05 DM |
1282 | $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192) |
1283 | if !$sslcert; | |
1284 | ||
1285 | my $port = PVE::Tools::next_vnc_port(); | |
1286 | ||
1287 | my $remip; | |
ef5e2be2 | 1288 | my $remcmd = []; |
afdb31d5 | 1289 | |
4f1be36c | 1290 | if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) { |
1e3baf05 | 1291 | $remip = PVE::Cluster::remote_node_ip($node); |
ef5e2be2 DM |
1292 | # NOTE: kvm VNC traffic is already TLS encrypted |
1293 | $remcmd = ['/usr/bin/ssh', '-T', '-o', 'BatchMode=yes', $remip]; | |
1e3baf05 DM |
1294 | } |
1295 | ||
afdb31d5 | 1296 | my $timeout = 10; |
1e3baf05 DM |
1297 | |
1298 | my $realcmd = sub { | |
1299 | my $upid = shift; | |
1300 | ||
1301 | syslog('info', "starting vnc proxy $upid\n"); | |
1302 | ||
ef5e2be2 | 1303 | my $cmd; |
1e3baf05 | 1304 | |
ef5e2be2 DM |
1305 | if ($conf->{vga} =~ m/^serial\d+$/) { |
1306 | ||
1307 | my $termcmd = [ '/usr/sbin/qm', 'terminal', $vmid, '-iface', $conf->{vga} ]; | |
1308 | #my $termcmd = "/usr/bin/qm terminal -iface $conf->{vga}"; | |
1309 | $cmd = ['/usr/bin/vncterm', '-rfbport', $port, | |
1310 | '-timeout', $timeout, '-authpath', $authpath, | |
1311 | '-perm', 'Sys.Console', '-c', @$remcmd, @$termcmd]; | |
1312 | } else { | |
1e3baf05 | 1313 | |
ef5e2be2 DM |
1314 | my $qmcmd = [@$remcmd, "/usr/sbin/qm", 'vncproxy', $vmid]; |
1315 | ||
1316 | my $qmstr = join(' ', @$qmcmd); | |
1317 | ||
1318 | # also redirect stderr (else we get RFB protocol errors) | |
1319 | $cmd = ['/bin/nc', '-l', '-p', $port, '-w', $timeout, '-c', "$qmstr 2>/dev/null"]; | |
1320 | } | |
1e3baf05 | 1321 | |
be62c45c | 1322 | PVE::Tools::run_command($cmd); |
1e3baf05 DM |
1323 | |
1324 | return; | |
1325 | }; | |
1326 | ||
a0d1b1a2 | 1327 | my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd); |
1e3baf05 | 1328 | |
3da85107 DM |
1329 | PVE::Tools::wait_for_vnc_port($port); |
1330 | ||
1e3baf05 | 1331 | return { |
a0d1b1a2 | 1332 | user => $authuser, |
1e3baf05 | 1333 | ticket => $ticket, |
afdb31d5 DM |
1334 | port => $port, |
1335 | upid => $upid, | |
1336 | cert => $sslcert, | |
1e3baf05 DM |
1337 | }; |
1338 | }}); | |
1339 | ||
288eeea8 DM |
1340 | __PACKAGE__->register_method({ |
1341 | name => 'spiceproxy', | |
1342 | path => '{vmid}/spiceproxy', | |
3309e65a | 1343 | method => 'GET', |
288eeea8 DM |
1344 | protected => 1, |
1345 | proxyto => 'node', # fixme: use direct connections or ssh tunnel? | |
1346 | permissions => { | |
1347 | check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]], | |
1348 | }, | |
1349 | description => "Returns a SPICE configuration to connect to the VM.", | |
1350 | parameters => { | |
1351 | additionalProperties => 0, | |
1352 | properties => { | |
1353 | node => get_standard_option('pve-node'), | |
1354 | vmid => get_standard_option('pve-vmid'), | |
fb6c7260 | 1355 | proxy => { |
31178e13 | 1356 | description => "This can be used by the client to specify the proxy server. All nodes in a cluster runs 'spiceproxy', so it is up to the client to choose one. By default, we return the node where the VM is currently running. As resonable setting is to use same node you use to connect to the API (This is window.location.hostname for the JS GUI).", |
fb6c7260 DM |
1357 | type => 'string', format => 'dns-name', |
1358 | optional => 1, | |
1359 | }, | |
288eeea8 DM |
1360 | }, |
1361 | }, | |
1362 | returns => { | |
fb6c7260 | 1363 | description => "Returned values can be directly passed to the 'remote-viewer' application.", |
288eeea8 DM |
1364 | additionalProperties => 1, |
1365 | properties => { | |
1366 | type => { type => 'string' }, | |
1367 | password => { type => 'string' }, | |
3309e65a | 1368 | proxy => { type => 'string' }, |
288eeea8 | 1369 | host => { type => 'string' }, |
943340a6 | 1370 | 'tls-port' => { type => 'integer' }, |
288eeea8 DM |
1371 | }, |
1372 | }, | |
1373 | code => sub { | |
1374 | my ($param) = @_; | |
1375 | ||
1376 | my $rpcenv = PVE::RPCEnvironment::get(); | |
1377 | ||
1378 | my $authuser = $rpcenv->get_user(); | |
1379 | ||
1380 | my $vmid = $param->{vmid}; | |
1381 | my $node = $param->{node}; | |
fb6c7260 | 1382 | my $proxy = $param->{proxy}; |
288eeea8 | 1383 | |
3309e65a | 1384 | my ($ticket, $proxyticket) = PVE::AccessControl::assemble_spice_ticket($authuser, $vmid, $node); |
5ecf258f | 1385 | |
288eeea8 DM |
1386 | my $timeout = 10; |
1387 | ||
943340a6 | 1388 | my $port = PVE::QemuServer::spice_port($vmid); |
288eeea8 DM |
1389 | PVE::QemuServer::vm_mon_cmd($vmid, "set_password", protocol => 'spice', password => $ticket); |
1390 | PVE::QemuServer::vm_mon_cmd($vmid, "expire_password", protocol => 'spice', time => "+30"); | |
1391 | ||
fb6c7260 DM |
1392 | if (!$proxy) { |
1393 | my $host = `hostname -f` || PVE::INotify::nodename(); | |
1394 | chomp $host; | |
1395 | $proxy = $host; | |
1396 | } | |
288eeea8 | 1397 | |
451b2b81 | 1398 | my $filename = "/etc/pve/local/pve-ssl.pem"; |
1481f3f2 | 1399 | my $subject = PVE::QemuServer::read_x509_subject_spice($filename); |
943340a6 DM |
1400 | |
1401 | my $cacert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192); | |
1402 | $cacert =~ s/\n/\\n/g; | |
1403 | ||
288eeea8 DM |
1404 | return { |
1405 | type => 'spice', | |
943340a6 DM |
1406 | title => "VM $vmid", |
1407 | host => $proxyticket, # this break tls hostname verification, so we need to use 'host-subject' | |
fb6c7260 | 1408 | proxy => "http://$proxy:3128", |
943340a6 DM |
1409 | 'tls-port' => $port, |
1410 | 'host-subject' => $subject, | |
1411 | ca => $cacert, | |
716a470c DM |
1412 | password => $ticket, |
1413 | 'delete-this-file' => 1, | |
288eeea8 DM |
1414 | }; |
1415 | }}); | |
1416 | ||
5fdbe4f0 DM |
1417 | __PACKAGE__->register_method({ |
1418 | name => 'vmcmdidx', | |
afdb31d5 | 1419 | path => '{vmid}/status', |
5fdbe4f0 DM |
1420 | method => 'GET', |
1421 | proxyto => 'node', | |
1422 | description => "Directory index", | |
a0d1b1a2 DM |
1423 | permissions => { |
1424 | user => 'all', | |
1425 | }, | |
5fdbe4f0 DM |
1426 | parameters => { |
1427 | additionalProperties => 0, | |
1428 | properties => { | |
1429 | node => get_standard_option('pve-node'), | |
1430 | vmid => get_standard_option('pve-vmid'), | |
1431 | }, | |
1432 | }, | |
1433 | returns => { | |
1434 | type => 'array', | |
1435 | items => { | |
1436 | type => "object", | |
1437 | properties => { | |
1438 | subdir => { type => 'string' }, | |
1439 | }, | |
1440 | }, | |
1441 | links => [ { rel => 'child', href => "{subdir}" } ], | |
1442 | }, | |
1443 | code => sub { | |
1444 | my ($param) = @_; | |
1445 | ||
1446 | # test if VM exists | |
1447 | my $conf = PVE::QemuServer::load_config($param->{vmid}); | |
1448 | ||
1449 | my $res = [ | |
1450 | { subdir => 'current' }, | |
1451 | { subdir => 'start' }, | |
1452 | { subdir => 'stop' }, | |
1453 | ]; | |
afdb31d5 | 1454 | |
5fdbe4f0 DM |
1455 | return $res; |
1456 | }}); | |
1457 | ||
88fc87b4 DM |
1458 | my $vm_is_ha_managed = sub { |
1459 | my ($vmid) = @_; | |
1460 | ||
1461 | my $cc = PVE::Cluster::cfs_read_file('cluster.conf'); | |
1462 | if (PVE::Cluster::cluster_conf_lookup_pvevm($cc, 0, $vmid, 1)) { | |
1463 | return 1; | |
75466c4f | 1464 | } |
88fc87b4 DM |
1465 | return 0; |
1466 | }; | |
1467 | ||
1e3baf05 | 1468 | __PACKAGE__->register_method({ |
afdb31d5 | 1469 | name => 'vm_status', |
5fdbe4f0 | 1470 | path => '{vmid}/status/current', |
1e3baf05 DM |
1471 | method => 'GET', |
1472 | proxyto => 'node', | |
1473 | protected => 1, # qemu pid files are only readable by root | |
1474 | description => "Get virtual machine status.", | |
a0d1b1a2 DM |
1475 | permissions => { |
1476 | check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]], | |
1477 | }, | |
1e3baf05 DM |
1478 | parameters => { |
1479 | additionalProperties => 0, | |
1480 | properties => { | |
1481 | node => get_standard_option('pve-node'), | |
1482 | vmid => get_standard_option('pve-vmid'), | |
1483 | }, | |
1484 | }, | |
1485 | returns => { type => 'object' }, | |
1486 | code => sub { | |
1487 | my ($param) = @_; | |
1488 | ||
1489 | # test if VM exists | |
1490 | my $conf = PVE::QemuServer::load_config($param->{vmid}); | |
1491 | ||
03a33f30 | 1492 | my $vmstatus = PVE::QemuServer::vmstatus($param->{vmid}, 1); |
8610701a | 1493 | my $status = $vmstatus->{$param->{vmid}}; |
1e3baf05 | 1494 | |
88fc87b4 | 1495 | $status->{ha} = &$vm_is_ha_managed($param->{vmid}); |
8610701a | 1496 | |
86b8228b | 1497 | $status->{spice} = 1 if PVE::QemuServer::vga_conf_has_spice($conf->{vga}); |
46246f04 | 1498 | |
8610701a | 1499 | return $status; |
1e3baf05 DM |
1500 | }}); |
1501 | ||
1502 | __PACKAGE__->register_method({ | |
afdb31d5 | 1503 | name => 'vm_start', |
5fdbe4f0 DM |
1504 | path => '{vmid}/status/start', |
1505 | method => 'POST', | |
1e3baf05 DM |
1506 | protected => 1, |
1507 | proxyto => 'node', | |
5fdbe4f0 | 1508 | description => "Start virtual machine.", |
a0d1b1a2 DM |
1509 | permissions => { |
1510 | check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]], | |
1511 | }, | |
1e3baf05 DM |
1512 | parameters => { |
1513 | additionalProperties => 0, | |
1514 | properties => { | |
1515 | node => get_standard_option('pve-node'), | |
1516 | vmid => get_standard_option('pve-vmid'), | |
3ea94c60 DM |
1517 | skiplock => get_standard_option('skiplock'), |
1518 | stateuri => get_standard_option('pve-qm-stateuri'), | |
7e8dcf2c | 1519 | migratedfrom => get_standard_option('pve-node',{ optional => 1 }), |
952958bc | 1520 | machine => get_standard_option('pve-qm-machine'), |
1e3baf05 DM |
1521 | }, |
1522 | }, | |
afdb31d5 | 1523 | returns => { |
5fdbe4f0 DM |
1524 | type => 'string', |
1525 | }, | |
1e3baf05 DM |
1526 | code => sub { |
1527 | my ($param) = @_; | |
1528 | ||
1529 | my $rpcenv = PVE::RPCEnvironment::get(); | |
1530 | ||
a0d1b1a2 | 1531 | my $authuser = $rpcenv->get_user(); |
1e3baf05 DM |
1532 | |
1533 | my $node = extract_param($param, 'node'); | |
1534 | ||
1e3baf05 DM |
1535 | my $vmid = extract_param($param, 'vmid'); |
1536 | ||
952958bc DM |
1537 | my $machine = extract_param($param, 'machine'); |
1538 | ||
3ea94c60 | 1539 | my $stateuri = extract_param($param, 'stateuri'); |
afdb31d5 | 1540 | raise_param_exc({ stateuri => "Only root may use this option." }) |
a0d1b1a2 | 1541 | if $stateuri && $authuser ne 'root@pam'; |
3ea94c60 | 1542 | |
1e3baf05 | 1543 | my $skiplock = extract_param($param, 'skiplock'); |
afdb31d5 | 1544 | raise_param_exc({ skiplock => "Only root may use this option." }) |
a0d1b1a2 | 1545 | if $skiplock && $authuser ne 'root@pam'; |
1e3baf05 | 1546 | |
7e8dcf2c AD |
1547 | my $migratedfrom = extract_param($param, 'migratedfrom'); |
1548 | raise_param_exc({ migratedfrom => "Only root may use this option." }) | |
1549 | if $migratedfrom && $authuser ne 'root@pam'; | |
1550 | ||
7c14dcae DM |
1551 | # read spice ticket from STDIN |
1552 | my $spice_ticket; | |
1553 | if ($stateuri && ($stateuri eq 'tcp') && $migratedfrom && ($rpcenv->{type} eq 'cli')) { | |
1554 | my $line = <>; | |
1555 | chomp $line; | |
1556 | $spice_ticket = $line if $line; | |
1557 | } | |
1558 | ||
afdb31d5 | 1559 | my $storecfg = PVE::Storage::config(); |
5fdbe4f0 | 1560 | |
cce37749 DM |
1561 | if (&$vm_is_ha_managed($vmid) && !$stateuri && |
1562 | $rpcenv->{type} ne 'ha') { | |
5fdbe4f0 | 1563 | |
88fc87b4 DM |
1564 | my $hacmd = sub { |
1565 | my $upid = shift; | |
5fdbe4f0 | 1566 | |
88fc87b4 | 1567 | my $service = "pvevm:$vmid"; |
5fdbe4f0 | 1568 | |
88fc87b4 DM |
1569 | my $cmd = ['clusvcadm', '-e', $service, '-m', $node]; |
1570 | ||
1571 | print "Executing HA start for VM $vmid\n"; | |
1572 | ||
1573 | PVE::Tools::run_command($cmd); | |
1574 | ||
1575 | return; | |
1576 | }; | |
1577 | ||
1578 | return $rpcenv->fork_worker('hastart', $vmid, $authuser, $hacmd); | |
1579 | ||
1580 | } else { | |
1581 | ||
1582 | my $realcmd = sub { | |
1583 | my $upid = shift; | |
1584 | ||
1585 | syslog('info', "start VM $vmid: $upid\n"); | |
1586 | ||
7c14dcae DM |
1587 | PVE::QemuServer::vm_start($storecfg, $vmid, $stateuri, $skiplock, $migratedfrom, undef, |
1588 | $machine, $spice_ticket); | |
88fc87b4 DM |
1589 | |
1590 | return; | |
1591 | }; | |
5fdbe4f0 | 1592 | |
88fc87b4 DM |
1593 | return $rpcenv->fork_worker('qmstart', $vmid, $authuser, $realcmd); |
1594 | } | |
5fdbe4f0 DM |
1595 | }}); |
1596 | ||
1597 | __PACKAGE__->register_method({ | |
afdb31d5 | 1598 | name => 'vm_stop', |
5fdbe4f0 DM |
1599 | path => '{vmid}/status/stop', |
1600 | method => 'POST', | |
1601 | protected => 1, | |
1602 | proxyto => 'node', | |
1603 | description => "Stop virtual machine.", | |
a0d1b1a2 DM |
1604 | permissions => { |
1605 | check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]], | |
1606 | }, | |
5fdbe4f0 DM |
1607 | parameters => { |
1608 | additionalProperties => 0, | |
1609 | properties => { | |
1610 | node => get_standard_option('pve-node'), | |
1611 | vmid => get_standard_option('pve-vmid'), | |
1612 | skiplock => get_standard_option('skiplock'), | |
af30308f | 1613 | migratedfrom => get_standard_option('pve-node',{ optional => 1 }), |
c6bb9502 DM |
1614 | timeout => { |
1615 | description => "Wait maximal timeout seconds.", | |
1616 | type => 'integer', | |
1617 | minimum => 0, | |
1618 | optional => 1, | |
254575e9 DM |
1619 | }, |
1620 | keepActive => { | |
1621 | description => "Do not decativate storage volumes.", | |
1622 | type => 'boolean', | |
1623 | optional => 1, | |
1624 | default => 0, | |
c6bb9502 | 1625 | } |
5fdbe4f0 DM |
1626 | }, |
1627 | }, | |
afdb31d5 | 1628 | returns => { |
5fdbe4f0 DM |
1629 | type => 'string', |
1630 | }, | |
1631 | code => sub { | |
1632 | my ($param) = @_; | |
1633 | ||
1634 | my $rpcenv = PVE::RPCEnvironment::get(); | |
1635 | ||
a0d1b1a2 | 1636 | my $authuser = $rpcenv->get_user(); |
5fdbe4f0 DM |
1637 | |
1638 | my $node = extract_param($param, 'node'); | |
1639 | ||
1640 | my $vmid = extract_param($param, 'vmid'); | |
1641 | ||
1642 | my $skiplock = extract_param($param, 'skiplock'); | |
afdb31d5 | 1643 | raise_param_exc({ skiplock => "Only root may use this option." }) |
a0d1b1a2 | 1644 | if $skiplock && $authuser ne 'root@pam'; |
5fdbe4f0 | 1645 | |
254575e9 | 1646 | my $keepActive = extract_param($param, 'keepActive'); |
afdb31d5 | 1647 | raise_param_exc({ keepActive => "Only root may use this option." }) |
a0d1b1a2 | 1648 | if $keepActive && $authuser ne 'root@pam'; |
254575e9 | 1649 | |
af30308f DM |
1650 | my $migratedfrom = extract_param($param, 'migratedfrom'); |
1651 | raise_param_exc({ migratedfrom => "Only root may use this option." }) | |
1652 | if $migratedfrom && $authuser ne 'root@pam'; | |
1653 | ||
1654 | ||
ff1a2432 DM |
1655 | my $storecfg = PVE::Storage::config(); |
1656 | ||
3be30d63 | 1657 | if (&$vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') { |
5fdbe4f0 | 1658 | |
88fc87b4 DM |
1659 | my $hacmd = sub { |
1660 | my $upid = shift; | |
5fdbe4f0 | 1661 | |
88fc87b4 | 1662 | my $service = "pvevm:$vmid"; |
c6bb9502 | 1663 | |
88fc87b4 DM |
1664 | my $cmd = ['clusvcadm', '-d', $service]; |
1665 | ||
1666 | print "Executing HA stop for VM $vmid\n"; | |
1667 | ||
1668 | PVE::Tools::run_command($cmd); | |
5fdbe4f0 | 1669 | |
88fc87b4 DM |
1670 | return; |
1671 | }; | |
1672 | ||
1673 | return $rpcenv->fork_worker('hastop', $vmid, $authuser, $hacmd); | |
1674 | ||
1675 | } else { | |
1676 | my $realcmd = sub { | |
1677 | my $upid = shift; | |
1678 | ||
1679 | syslog('info', "stop VM $vmid: $upid\n"); | |
1680 | ||
1681 | PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, | |
af30308f | 1682 | $param->{timeout}, 0, 1, $keepActive, $migratedfrom); |
88fc87b4 DM |
1683 | |
1684 | return; | |
1685 | }; | |
1686 | ||
1687 | return $rpcenv->fork_worker('qmstop', $vmid, $authuser, $realcmd); | |
1688 | } | |
5fdbe4f0 DM |
1689 | }}); |
1690 | ||
1691 | __PACKAGE__->register_method({ | |
afdb31d5 | 1692 | name => 'vm_reset', |
5fdbe4f0 DM |
1693 | path => '{vmid}/status/reset', |
1694 | method => 'POST', | |
1695 | protected => 1, | |
1696 | proxyto => 'node', | |
1697 | description => "Reset virtual machine.", | |
a0d1b1a2 DM |
1698 | permissions => { |
1699 | check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]], | |
1700 | }, | |
5fdbe4f0 DM |
1701 | parameters => { |
1702 | additionalProperties => 0, | |
1703 | properties => { | |
1704 | node => get_standard_option('pve-node'), | |
1705 | vmid => get_standard_option('pve-vmid'), | |
1706 | skiplock => get_standard_option('skiplock'), | |
1707 | }, | |
1708 | }, | |
afdb31d5 | 1709 | returns => { |
5fdbe4f0 DM |
1710 | type => 'string', |
1711 | }, | |
1712 | code => sub { | |
1713 | my ($param) = @_; | |
1714 | ||
1715 | my $rpcenv = PVE::RPCEnvironment::get(); | |
1716 | ||
a0d1b1a2 | 1717 | my $authuser = $rpcenv->get_user(); |
5fdbe4f0 DM |
1718 | |
1719 | my $node = extract_param($param, 'node'); | |
1720 | ||
1721 | my $vmid = extract_param($param, 'vmid'); | |
1722 | ||
1723 | my $skiplock = extract_param($param, 'skiplock'); | |
afdb31d5 | 1724 | raise_param_exc({ skiplock => "Only root may use this option." }) |
a0d1b1a2 | 1725 | if $skiplock && $authuser ne 'root@pam'; |
5fdbe4f0 | 1726 | |
ff1a2432 DM |
1727 | die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid); |
1728 | ||
5fdbe4f0 DM |
1729 | my $realcmd = sub { |
1730 | my $upid = shift; | |
1731 | ||
1e3baf05 | 1732 | PVE::QemuServer::vm_reset($vmid, $skiplock); |
5fdbe4f0 DM |
1733 | |
1734 | return; | |
1735 | }; | |
1736 | ||
a0d1b1a2 | 1737 | return $rpcenv->fork_worker('qmreset', $vmid, $authuser, $realcmd); |
5fdbe4f0 DM |
1738 | }}); |
1739 | ||
1740 | __PACKAGE__->register_method({ | |
afdb31d5 | 1741 | name => 'vm_shutdown', |
5fdbe4f0 DM |
1742 | path => '{vmid}/status/shutdown', |
1743 | method => 'POST', | |
1744 | protected => 1, | |
1745 | proxyto => 'node', | |
1746 | description => "Shutdown virtual machine.", | |
a0d1b1a2 DM |
1747 | permissions => { |
1748 | check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]], | |
1749 | }, | |
5fdbe4f0 DM |
1750 | parameters => { |
1751 | additionalProperties => 0, | |
1752 | properties => { | |
1753 | node => get_standard_option('pve-node'), | |
1754 | vmid => get_standard_option('pve-vmid'), | |
1755 | skiplock => get_standard_option('skiplock'), | |
c6bb9502 DM |
1756 | timeout => { |
1757 | description => "Wait maximal timeout seconds.", | |
1758 | type => 'integer', | |
1759 | minimum => 0, | |
1760 | optional => 1, | |
9269013a DM |
1761 | }, |
1762 | forceStop => { | |
1763 | description => "Make sure the VM stops.", | |
1764 | type => 'boolean', | |
1765 | optional => 1, | |
1766 | default => 0, | |
254575e9 DM |
1767 | }, |
1768 | keepActive => { | |
1769 | description => "Do not decativate storage volumes.", | |
1770 | type => 'boolean', | |
1771 | optional => 1, | |
1772 | default => 0, | |
c6bb9502 | 1773 | } |
5fdbe4f0 DM |
1774 | }, |
1775 | }, | |
afdb31d5 | 1776 | returns => { |
5fdbe4f0 DM |
1777 | type => 'string', |
1778 | }, | |
1779 | code => sub { | |
1780 | my ($param) = @_; | |
1781 | ||
1782 | my $rpcenv = PVE::RPCEnvironment::get(); | |
1783 | ||
a0d1b1a2 | 1784 | my $authuser = $rpcenv->get_user(); |
5fdbe4f0 DM |
1785 | |
1786 | my $node = extract_param($param, 'node'); | |
1787 | ||
1788 | my $vmid = extract_param($param, 'vmid'); | |
1789 | ||
1790 | my $skiplock = extract_param($param, 'skiplock'); | |
afdb31d5 | 1791 | raise_param_exc({ skiplock => "Only root may use this option." }) |
a0d1b1a2 | 1792 | if $skiplock && $authuser ne 'root@pam'; |
5fdbe4f0 | 1793 | |
254575e9 | 1794 | my $keepActive = extract_param($param, 'keepActive'); |
afdb31d5 | 1795 | raise_param_exc({ keepActive => "Only root may use this option." }) |
a0d1b1a2 | 1796 | if $keepActive && $authuser ne 'root@pam'; |
254575e9 | 1797 | |
02d07cf5 DM |
1798 | my $storecfg = PVE::Storage::config(); |
1799 | ||
5fdbe4f0 DM |
1800 | my $realcmd = sub { |
1801 | my $upid = shift; | |
1802 | ||
1803 | syslog('info', "shutdown VM $vmid: $upid\n"); | |
1804 | ||
afdb31d5 | 1805 | PVE::QemuServer::vm_stop($storecfg, $vmid, $skiplock, 0, $param->{timeout}, |
254575e9 | 1806 | 1, $param->{forceStop}, $keepActive); |
c6bb9502 | 1807 | |
5fdbe4f0 DM |
1808 | return; |
1809 | }; | |
1810 | ||
a0d1b1a2 | 1811 | return $rpcenv->fork_worker('qmshutdown', $vmid, $authuser, $realcmd); |
5fdbe4f0 DM |
1812 | }}); |
1813 | ||
1814 | __PACKAGE__->register_method({ | |
afdb31d5 | 1815 | name => 'vm_suspend', |
5fdbe4f0 DM |
1816 | path => '{vmid}/status/suspend', |
1817 | method => 'POST', | |
1818 | protected => 1, | |
1819 | proxyto => 'node', | |
1820 | description => "Suspend virtual machine.", | |
a0d1b1a2 DM |
1821 | permissions => { |
1822 | check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]], | |
1823 | }, | |
5fdbe4f0 DM |
1824 | parameters => { |
1825 | additionalProperties => 0, | |
1826 | properties => { | |
1827 | node => get_standard_option('pve-node'), | |
1828 | vmid => get_standard_option('pve-vmid'), | |
1829 | skiplock => get_standard_option('skiplock'), | |
1830 | }, | |
1831 | }, | |
afdb31d5 | 1832 | returns => { |
5fdbe4f0 DM |
1833 | type => 'string', |
1834 | }, | |
1835 | code => sub { | |
1836 | my ($param) = @_; | |
1837 | ||
1838 | my $rpcenv = PVE::RPCEnvironment::get(); | |
1839 | ||
a0d1b1a2 | 1840 | my $authuser = $rpcenv->get_user(); |
5fdbe4f0 DM |
1841 | |
1842 | my $node = extract_param($param, 'node'); | |
1843 | ||
1844 | my $vmid = extract_param($param, 'vmid'); | |
1845 | ||
1846 | my $skiplock = extract_param($param, 'skiplock'); | |
afdb31d5 | 1847 | raise_param_exc({ skiplock => "Only root may use this option." }) |
a0d1b1a2 | 1848 | if $skiplock && $authuser ne 'root@pam'; |
5fdbe4f0 | 1849 | |
ff1a2432 DM |
1850 | die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid); |
1851 | ||
5fdbe4f0 DM |
1852 | my $realcmd = sub { |
1853 | my $upid = shift; | |
1854 | ||
1855 | syslog('info', "suspend VM $vmid: $upid\n"); | |
1856 | ||
1e3baf05 | 1857 | PVE::QemuServer::vm_suspend($vmid, $skiplock); |
5fdbe4f0 DM |
1858 | |
1859 | return; | |
1860 | }; | |
1861 | ||
a0d1b1a2 | 1862 | return $rpcenv->fork_worker('qmsuspend', $vmid, $authuser, $realcmd); |
5fdbe4f0 DM |
1863 | }}); |
1864 | ||
1865 | __PACKAGE__->register_method({ | |
afdb31d5 | 1866 | name => 'vm_resume', |
5fdbe4f0 DM |
1867 | path => '{vmid}/status/resume', |
1868 | method => 'POST', | |
1869 | protected => 1, | |
1870 | proxyto => 'node', | |
1871 | description => "Resume virtual machine.", | |
a0d1b1a2 DM |
1872 | permissions => { |
1873 | check => ['perm', '/vms/{vmid}', [ 'VM.PowerMgmt' ]], | |
1874 | }, | |
5fdbe4f0 DM |
1875 | parameters => { |
1876 | additionalProperties => 0, | |
1877 | properties => { | |
1878 | node => get_standard_option('pve-node'), | |
1879 | vmid => get_standard_option('pve-vmid'), | |
1880 | skiplock => get_standard_option('skiplock'), | |
1881 | }, | |
1882 | }, | |
afdb31d5 | 1883 | returns => { |
5fdbe4f0 DM |
1884 | type => 'string', |
1885 | }, | |
1886 | code => sub { | |
1887 | my ($param) = @_; | |
1888 | ||
1889 | my $rpcenv = PVE::RPCEnvironment::get(); | |
1890 | ||
a0d1b1a2 | 1891 | my $authuser = $rpcenv->get_user(); |
5fdbe4f0 DM |
1892 | |
1893 | my $node = extract_param($param, 'node'); | |
1894 | ||
1895 | my $vmid = extract_param($param, 'vmid'); | |
1896 | ||
1897 | my $skiplock = extract_param($param, 'skiplock'); | |
afdb31d5 | 1898 | raise_param_exc({ skiplock => "Only root may use this option." }) |
a0d1b1a2 | 1899 | if $skiplock && $authuser ne 'root@pam'; |
5fdbe4f0 | 1900 | |
b7eeab21 | 1901 | die "VM $vmid not running\n" if !PVE::QemuServer::check_running($vmid); |
ff1a2432 | 1902 | |
5fdbe4f0 DM |
1903 | my $realcmd = sub { |
1904 | my $upid = shift; | |
1905 | ||
1906 | syslog('info', "resume VM $vmid: $upid\n"); | |
1907 | ||
1e3baf05 | 1908 | PVE::QemuServer::vm_resume($vmid, $skiplock); |
1e3baf05 | 1909 | |
5fdbe4f0 DM |
1910 | return; |
1911 | }; | |
1912 | ||
a0d1b1a2 | 1913 | return $rpcenv->fork_worker('qmresume', $vmid, $authuser, $realcmd); |
5fdbe4f0 DM |
1914 | }}); |
1915 | ||
1916 | __PACKAGE__->register_method({ | |
afdb31d5 | 1917 | name => 'vm_sendkey', |
5fdbe4f0 DM |
1918 | path => '{vmid}/sendkey', |
1919 | method => 'PUT', | |
1920 | protected => 1, | |
1921 | proxyto => 'node', | |
1922 | description => "Send key event to virtual machine.", | |
a0d1b1a2 DM |
1923 | permissions => { |
1924 | check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]], | |
1925 | }, | |
5fdbe4f0 DM |
1926 | parameters => { |
1927 | additionalProperties => 0, | |
1928 | properties => { | |
1929 | node => get_standard_option('pve-node'), | |
1930 | vmid => get_standard_option('pve-vmid'), | |
1931 | skiplock => get_standard_option('skiplock'), | |
1932 | key => { | |
1933 | description => "The key (qemu monitor encoding).", | |
1934 | type => 'string' | |
1935 | } | |
1936 | }, | |
1937 | }, | |
1938 | returns => { type => 'null'}, | |
1939 | code => sub { | |
1940 | my ($param) = @_; | |
1941 | ||
1942 | my $rpcenv = PVE::RPCEnvironment::get(); | |
1943 | ||
a0d1b1a2 | 1944 | my $authuser = $rpcenv->get_user(); |
5fdbe4f0 DM |
1945 | |
1946 | my $node = extract_param($param, 'node'); | |
1947 | ||
1948 | my $vmid = extract_param($param, 'vmid'); | |
1949 | ||
1950 | my $skiplock = extract_param($param, 'skiplock'); | |
afdb31d5 | 1951 | raise_param_exc({ skiplock => "Only root may use this option." }) |
a0d1b1a2 | 1952 | if $skiplock && $authuser ne 'root@pam'; |
5fdbe4f0 DM |
1953 | |
1954 | PVE::QemuServer::vm_sendkey($vmid, $skiplock, $param->{key}); | |
1955 | ||
1956 | return; | |
1e3baf05 DM |
1957 | }}); |
1958 | ||
1ac0d2ee AD |
1959 | __PACKAGE__->register_method({ |
1960 | name => 'vm_feature', | |
1961 | path => '{vmid}/feature', | |
1962 | method => 'GET', | |
1963 | proxyto => 'node', | |
75466c4f | 1964 | protected => 1, |
1ac0d2ee AD |
1965 | description => "Check if feature for virtual machine is available.", |
1966 | permissions => { | |
1967 | check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]], | |
1968 | }, | |
1969 | parameters => { | |
1970 | additionalProperties => 0, | |
1971 | properties => { | |
1972 | node => get_standard_option('pve-node'), | |
1973 | vmid => get_standard_option('pve-vmid'), | |
1974 | feature => { | |
1975 | description => "Feature to check.", | |
1976 | type => 'string', | |
7758ce86 | 1977 | enum => [ 'snapshot', 'clone', 'copy' ], |
1ac0d2ee AD |
1978 | }, |
1979 | snapname => get_standard_option('pve-snapshot-name', { | |
1980 | optional => 1, | |
1981 | }), | |
1982 | }, | |
1ac0d2ee AD |
1983 | }, |
1984 | returns => { | |
719893a9 DM |
1985 | type => "object", |
1986 | properties => { | |
1987 | hasFeature => { type => 'boolean' }, | |
7043d946 | 1988 | nodes => { |
719893a9 DM |
1989 | type => 'array', |
1990 | items => { type => 'string' }, | |
1991 | } | |
1992 | }, | |
1ac0d2ee AD |
1993 | }, |
1994 | code => sub { | |
1995 | my ($param) = @_; | |
1996 | ||
1997 | my $node = extract_param($param, 'node'); | |
1998 | ||
1999 | my $vmid = extract_param($param, 'vmid'); | |
2000 | ||
2001 | my $snapname = extract_param($param, 'snapname'); | |
2002 | ||
2003 | my $feature = extract_param($param, 'feature'); | |
2004 | ||
2005 | my $running = PVE::QemuServer::check_running($vmid); | |
2006 | ||
2007 | my $conf = PVE::QemuServer::load_config($vmid); | |
2008 | ||
2009 | if($snapname){ | |
2010 | my $snap = $conf->{snapshots}->{$snapname}; | |
2011 | die "snapshot '$snapname' does not exist\n" if !defined($snap); | |
2012 | $conf = $snap; | |
2013 | } | |
2014 | my $storecfg = PVE::Storage::config(); | |
2015 | ||
719893a9 DM |
2016 | my $nodelist = PVE::QemuServer::shared_nodes($conf, $storecfg); |
2017 | my $hasFeature = PVE::QemuServer::has_feature($feature, $conf, $storecfg, $snapname, $running); | |
7043d946 | 2018 | |
719893a9 DM |
2019 | return { |
2020 | hasFeature => $hasFeature, | |
2021 | nodes => [ keys %$nodelist ], | |
7043d946 | 2022 | }; |
1ac0d2ee AD |
2023 | }}); |
2024 | ||
6116f729 | 2025 | __PACKAGE__->register_method({ |
9418baad DM |
2026 | name => 'clone_vm', |
2027 | path => '{vmid}/clone', | |
6116f729 DM |
2028 | method => 'POST', |
2029 | protected => 1, | |
2030 | proxyto => 'node', | |
37329185 | 2031 | description => "Create a copy of virtual machine/template.", |
6116f729 | 2032 | permissions => { |
9418baad | 2033 | description => "You need 'VM.Clone' permissions on /vms/{vmid}, and 'VM.Allocate' permissions " . |
6116f729 DM |
2034 | "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " . |
2035 | "'Datastore.AllocateSpace' on any used storage.", | |
75466c4f DM |
2036 | check => |
2037 | [ 'and', | |
9418baad | 2038 | ['perm', '/vms/{vmid}', [ 'VM.Clone' ]], |
75466c4f | 2039 | [ 'or', |
6116f729 DM |
2040 | [ 'perm', '/vms/{newid}', ['VM.Allocate']], |
2041 | [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'], | |
2042 | ], | |
2043 | ] | |
2044 | }, | |
2045 | parameters => { | |
2046 | additionalProperties => 0, | |
2047 | properties => { | |
6116f729 DM |
2048 | node => get_standard_option('pve-node'), |
2049 | vmid => get_standard_option('pve-vmid'), | |
9418baad | 2050 | newid => get_standard_option('pve-vmid', { description => 'VMID for the clone.' }), |
a60ab1a6 DM |
2051 | name => { |
2052 | optional => 1, | |
2053 | type => 'string', format => 'dns-name', | |
2054 | description => "Set a name for the new VM.", | |
2055 | }, | |
2056 | description => { | |
2057 | optional => 1, | |
2058 | type => 'string', | |
2059 | description => "Description for the new VM.", | |
2060 | }, | |
75466c4f | 2061 | pool => { |
6116f729 DM |
2062 | optional => 1, |
2063 | type => 'string', format => 'pve-poolid', | |
2064 | description => "Add the new VM to the specified pool.", | |
2065 | }, | |
9076d880 DM |
2066 | snapname => get_standard_option('pve-snapshot-name', { |
2067 | requires => 'full', | |
2068 | optional => 1, | |
2069 | }), | |
81f043eb | 2070 | storage => get_standard_option('pve-storage-id', { |
9418baad | 2071 | description => "Target storage for full clone.", |
4e4f83fe | 2072 | requires => 'full', |
81f043eb AD |
2073 | optional => 1, |
2074 | }), | |
55173c6b | 2075 | 'format' => { |
42a19c87 AD |
2076 | description => "Target format for file storage.", |
2077 | requires => 'full', | |
2078 | type => 'string', | |
2079 | optional => 1, | |
55173c6b | 2080 | enum => [ 'raw', 'qcow2', 'vmdk'], |
42a19c87 | 2081 | }, |
6116f729 DM |
2082 | full => { |
2083 | optional => 1, | |
55173c6b DM |
2084 | type => 'boolean', |
2085 | description => "Create a full copy of all disk. This is always done when " . | |
9418baad | 2086 | "you clone a normal VM. For VM templates, we try to create a linked clone by default.", |
6116f729 DM |
2087 | default => 0, |
2088 | }, | |
75466c4f | 2089 | target => get_standard_option('pve-node', { |
55173c6b DM |
2090 | description => "Target node. Only allowed if the original VM is on shared storage.", |
2091 | optional => 1, | |
2092 | }), | |
2093 | }, | |
6116f729 DM |
2094 | }, |
2095 | returns => { | |
2096 | type => 'string', | |
2097 | }, | |
2098 | code => sub { | |
2099 | my ($param) = @_; | |
2100 | ||
2101 | my $rpcenv = PVE::RPCEnvironment::get(); | |
2102 | ||
55173c6b | 2103 | my $authuser = $rpcenv->get_user(); |
6116f729 DM |
2104 | |
2105 | my $node = extract_param($param, 'node'); | |
2106 | ||
2107 | my $vmid = extract_param($param, 'vmid'); | |
2108 | ||
2109 | my $newid = extract_param($param, 'newid'); | |
2110 | ||
6116f729 DM |
2111 | my $pool = extract_param($param, 'pool'); |
2112 | ||
2113 | if (defined($pool)) { | |
2114 | $rpcenv->check_pool_exist($pool); | |
2115 | } | |
2116 | ||
55173c6b | 2117 | my $snapname = extract_param($param, 'snapname'); |
9076d880 | 2118 | |
81f043eb AD |
2119 | my $storage = extract_param($param, 'storage'); |
2120 | ||
42a19c87 AD |
2121 | my $format = extract_param($param, 'format'); |
2122 | ||
55173c6b DM |
2123 | my $target = extract_param($param, 'target'); |
2124 | ||
2125 | my $localnode = PVE::INotify::nodename(); | |
2126 | ||
751cc556 | 2127 | undef $target if $target && ($target eq $localnode || $target eq 'localhost'); |
55173c6b DM |
2128 | |
2129 | PVE::Cluster::check_node_exists($target) if $target; | |
2130 | ||
6116f729 DM |
2131 | my $storecfg = PVE::Storage::config(); |
2132 | ||
4a5a2590 DM |
2133 | if ($storage) { |
2134 | # check if storage is enabled on local node | |
2135 | PVE::Storage::storage_check_enabled($storecfg, $storage); | |
2136 | if ($target) { | |
2137 | # check if storage is available on target node | |
2138 | PVE::Storage::storage_check_node($storecfg, $storage, $target); | |
2139 | # clone only works if target storage is shared | |
2140 | my $scfg = PVE::Storage::storage_config($storecfg, $storage); | |
2141 | die "can't clone to non-shared storage '$storage'\n" if !$scfg->{shared}; | |
2142 | } | |
2143 | } | |
2144 | ||
55173c6b | 2145 | PVE::Cluster::check_cfs_quorum(); |
6116f729 | 2146 | |
4e4f83fe DM |
2147 | my $running = PVE::QemuServer::check_running($vmid) || 0; |
2148 | ||
4e4f83fe DM |
2149 | # exclusive lock if VM is running - else shared lock is enough; |
2150 | my $shared_lock = $running ? 0 : 1; | |
2151 | ||
9418baad | 2152 | my $clonefn = sub { |
6116f729 | 2153 | |
829967a9 DM |
2154 | # do all tests after lock |
2155 | # we also try to do all tests before we fork the worker | |
2156 | ||
6116f729 DM |
2157 | my $conf = PVE::QemuServer::load_config($vmid); |
2158 | ||
2159 | PVE::QemuServer::check_lock($conf); | |
2160 | ||
4e4f83fe | 2161 | my $verify_running = PVE::QemuServer::check_running($vmid) || 0; |
6116f729 | 2162 | |
4e4f83fe | 2163 | die "unexpected state change\n" if $verify_running != $running; |
6116f729 | 2164 | |
75466c4f DM |
2165 | die "snapshot '$snapname' does not exist\n" |
2166 | if $snapname && !defined( $conf->{snapshots}->{$snapname}); | |
6116f729 | 2167 | |
75466c4f | 2168 | my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf; |
9076d880 | 2169 | |
9418baad | 2170 | my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage); |
6116f729 | 2171 | |
9418baad | 2172 | die "can't clone VM to node '$target' (VM uses local storage)\n" if $target && !$sharedvm; |
75466c4f | 2173 | |
6116f729 DM |
2174 | my $conffile = PVE::QemuServer::config_file($newid); |
2175 | ||
2176 | die "unable to create VM $newid: config file already exists\n" | |
2177 | if -f $conffile; | |
2178 | ||
9418baad | 2179 | my $newconf = { lock => 'clone' }; |
829967a9 DM |
2180 | my $drives = {}; |
2181 | my $vollist = []; | |
2182 | ||
2183 | foreach my $opt (keys %$oldconf) { | |
2184 | my $value = $oldconf->{$opt}; | |
2185 | ||
2186 | # do not copy snapshot related info | |
2187 | next if $opt eq 'snapshots' || $opt eq 'parent' || $opt eq 'snaptime' || | |
2188 | $opt eq 'vmstate' || $opt eq 'snapstate'; | |
2189 | ||
2190 | # always change MAC! address | |
2191 | if ($opt =~ m/^net(\d+)$/) { | |
2192 | my $net = PVE::QemuServer::parse_net($value); | |
2193 | $net->{macaddr} = PVE::Tools::random_ether_addr(); | |
2194 | $newconf->{$opt} = PVE::QemuServer::print_net($net); | |
2195 | } elsif (my $drive = PVE::QemuServer::parse_drive($opt, $value)) { | |
2196 | if (PVE::QemuServer::drive_is_cdrom($drive)) { | |
2197 | $newconf->{$opt} = $value; # simply copy configuration | |
2198 | } else { | |
dba198b0 | 2199 | if ($param->{full} || !PVE::Storage::volume_is_base($storecfg, $drive->{file})) { |
2dd53043 | 2200 | die "Full clone feature is not available" |
dba198b0 | 2201 | if !PVE::Storage::volume_has_feature($storecfg, 'copy', $drive->{file}, $snapname, $running); |
2dd53043 | 2202 | $drive->{full} = 1; |
dba198b0 | 2203 | } |
829967a9 DM |
2204 | $drives->{$opt} = $drive; |
2205 | push @$vollist, $drive->{file}; | |
2206 | } | |
2207 | } else { | |
2208 | # copy everything else | |
2209 | $newconf->{$opt} = $value; | |
2210 | } | |
2211 | } | |
2212 | ||
2213 | delete $newconf->{template}; | |
2214 | ||
2215 | if ($param->{name}) { | |
2216 | $newconf->{name} = $param->{name}; | |
2217 | } else { | |
c55fee03 DM |
2218 | if ($oldconf->{name}) { |
2219 | $newconf->{name} = "Copy-of-$oldconf->{name}"; | |
2220 | } else { | |
2221 | $newconf->{name} = "Copy-of-VM-$vmid"; | |
2222 | } | |
829967a9 | 2223 | } |
2dd53043 | 2224 | |
829967a9 DM |
2225 | if ($param->{description}) { |
2226 | $newconf->{description} = $param->{description}; | |
2227 | } | |
2228 | ||
6116f729 | 2229 | # create empty/temp config - this fails if VM already exists on other node |
9418baad | 2230 | PVE::Tools::file_set_contents($conffile, "# qmclone temporary file\nlock: clone\n"); |
6116f729 DM |
2231 | |
2232 | my $realcmd = sub { | |
2233 | my $upid = shift; | |
2234 | ||
b83e0181 | 2235 | my $newvollist = []; |
6116f729 | 2236 | |
b83e0181 | 2237 | eval { |
829967a9 | 2238 | local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; }; |
75466c4f | 2239 | |
6116f729 DM |
2240 | PVE::Storage::activate_volumes($storecfg, $vollist); |
2241 | ||
829967a9 DM |
2242 | foreach my $opt (keys %$drives) { |
2243 | my $drive = $drives->{$opt}; | |
2dd53043 | 2244 | |
152fe752 DM |
2245 | my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $opt, $drive, $snapname, |
2246 | $newid, $storage, $format, $drive->{full}, $newvollist); | |
00b095ca | 2247 | |
152fe752 | 2248 | $newconf->{$opt} = PVE::QemuServer::print_drive($vmid, $newdrive); |
2dd53043 | 2249 | |
829967a9 DM |
2250 | PVE::QemuServer::update_config_nolock($newid, $newconf, 1); |
2251 | } | |
b83e0181 DM |
2252 | |
2253 | delete $newconf->{lock}; | |
2254 | PVE::QemuServer::update_config_nolock($newid, $newconf, 1); | |
55173c6b DM |
2255 | |
2256 | if ($target) { | |
2257 | my $newconffile = PVE::QemuServer::config_file($newid, $target); | |
2258 | die "Failed to move config to node '$target' - rename failed: $!\n" | |
2259 | if !rename($conffile, $newconffile); | |
2260 | } | |
d703d4c0 | 2261 | |
be517049 | 2262 | PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool; |
6116f729 | 2263 | }; |
75466c4f | 2264 | if (my $err = $@) { |
6116f729 DM |
2265 | unlink $conffile; |
2266 | ||
b83e0181 DM |
2267 | sleep 1; # some storage like rbd need to wait before release volume - really? |
2268 | ||
2269 | foreach my $volid (@$newvollist) { | |
2270 | eval { PVE::Storage::vdisk_free($storecfg, $volid); }; | |
2271 | warn $@ if $@; | |
2272 | } | |
9418baad | 2273 | die "clone failed: $err"; |
6116f729 DM |
2274 | } |
2275 | ||
2276 | return; | |
2277 | }; | |
2278 | ||
9418baad | 2279 | return $rpcenv->fork_worker('qmclone', $vmid, $authuser, $realcmd); |
6116f729 DM |
2280 | }; |
2281 | ||
4e4f83fe | 2282 | return PVE::QemuServer::lock_config_mode($vmid, 1, $shared_lock, sub { |
6116f729 | 2283 | # Aquire exclusive lock lock for $newid |
9418baad | 2284 | return PVE::QemuServer::lock_config_full($newid, 1, $clonefn); |
6116f729 DM |
2285 | }); |
2286 | ||
2287 | }}); | |
2288 | ||
586bfa78 | 2289 | __PACKAGE__->register_method({ |
43bc02a9 DM |
2290 | name => 'move_vm_disk', |
2291 | path => '{vmid}/move_disk', | |
e2cd75fa | 2292 | method => 'POST', |
586bfa78 AD |
2293 | protected => 1, |
2294 | proxyto => 'node', | |
2295 | description => "Move volume to different storage.", | |
2296 | permissions => { | |
e2cd75fa DM |
2297 | description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " . |
2298 | "and 'Datastore.AllocateSpace' permissions on the storage.", | |
7043d946 | 2299 | check => |
e2cd75fa DM |
2300 | [ 'and', |
2301 | ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]], | |
2302 | ['perm', '/storage/{storage}', [ 'Datastore.AllocateSpace' ]], | |
2303 | ], | |
586bfa78 AD |
2304 | }, |
2305 | parameters => { | |
2306 | additionalProperties => 0, | |
2307 | properties => { | |
2308 | node => get_standard_option('pve-node'), | |
2309 | vmid => get_standard_option('pve-vmid'), | |
586bfa78 AD |
2310 | disk => { |
2311 | type => 'string', | |
2312 | description => "The disk you want to move.", | |
e2cd75fa | 2313 | enum => [ PVE::QemuServer::disknames() ], |
586bfa78 | 2314 | }, |
e2cd75fa | 2315 | storage => get_standard_option('pve-storage-id', { description => "Target Storage." }), |
635c3c44 | 2316 | 'format' => { |
586bfa78 AD |
2317 | type => 'string', |
2318 | description => "Target Format.", | |
2319 | enum => [ 'raw', 'qcow2', 'vmdk' ], | |
2320 | optional => 1, | |
2321 | }, | |
70d45e33 DM |
2322 | delete => { |
2323 | type => 'boolean', | |
2324 | description => "Delete the original disk after successful copy. By default the original disk is kept as unused disk.", | |
2325 | optional => 1, | |
2326 | default => 0, | |
2327 | }, | |
586bfa78 AD |
2328 | digest => { |
2329 | type => 'string', | |
2330 | description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.', | |
2331 | maxLength => 40, | |
2332 | optional => 1, | |
2333 | }, | |
2334 | }, | |
2335 | }, | |
e2cd75fa DM |
2336 | returns => { |
2337 | type => 'string', | |
2338 | description => "the task ID.", | |
2339 | }, | |
586bfa78 AD |
2340 | code => sub { |
2341 | my ($param) = @_; | |
2342 | ||
2343 | my $rpcenv = PVE::RPCEnvironment::get(); | |
2344 | ||
2345 | my $authuser = $rpcenv->get_user(); | |
2346 | ||
2347 | my $node = extract_param($param, 'node'); | |
2348 | ||
2349 | my $vmid = extract_param($param, 'vmid'); | |
2350 | ||
2351 | my $digest = extract_param($param, 'digest'); | |
2352 | ||
2353 | my $disk = extract_param($param, 'disk'); | |
2354 | ||
2355 | my $storeid = extract_param($param, 'storage'); | |
2356 | ||
2357 | my $format = extract_param($param, 'format'); | |
2358 | ||
586bfa78 AD |
2359 | my $storecfg = PVE::Storage::config(); |
2360 | ||
2361 | my $updatefn = sub { | |
2362 | ||
2363 | my $conf = PVE::QemuServer::load_config($vmid); | |
2364 | ||
2365 | die "checksum missmatch (file change by other user?)\n" | |
2366 | if $digest && $digest ne $conf->{digest}; | |
586bfa78 AD |
2367 | |
2368 | die "disk '$disk' does not exist\n" if !$conf->{$disk}; | |
2369 | ||
2370 | my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk}); | |
2371 | ||
70d45e33 | 2372 | my $old_volid = $drive->{file} || die "disk '$disk' has no associated volume\n"; |
586bfa78 AD |
2373 | |
2374 | die "you can't move a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive); | |
2375 | ||
e2cd75fa | 2376 | my $oldfmt; |
70d45e33 | 2377 | my ($oldstoreid, $oldvolname) = PVE::Storage::parse_volume_id($old_volid); |
586bfa78 AD |
2378 | if ($oldvolname =~ m/\.(raw|qcow2|vmdk)$/){ |
2379 | $oldfmt = $1; | |
2380 | } | |
2381 | ||
7043d946 | 2382 | die "you can't move on the same storage with same format\n" if $oldstoreid eq $storeid && |
e2cd75fa | 2383 | (!$format || !$oldfmt || $oldfmt eq $format); |
586bfa78 AD |
2384 | |
2385 | PVE::Cluster::log_msg('info', $authuser, "move disk VM $vmid: move --disk $disk --storage $storeid"); | |
2386 | ||
2387 | my $running = PVE::QemuServer::check_running($vmid); | |
e2cd75fa DM |
2388 | |
2389 | PVE::Storage::activate_volumes($storecfg, [ $drive->{file} ]); | |
2390 | ||
586bfa78 AD |
2391 | my $realcmd = sub { |
2392 | ||
2393 | my $newvollist = []; | |
2394 | ||
2395 | eval { | |
2396 | local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { die "interrupted by signal\n"; }; | |
2397 | ||
e2cd75fa DM |
2398 | my $newdrive = PVE::QemuServer::clone_disk($storecfg, $vmid, $running, $disk, $drive, undef, |
2399 | $vmid, $storeid, $format, 1, $newvollist); | |
2400 | ||
2401 | $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $newdrive); | |
2402 | ||
70d45e33 | 2403 | PVE::QemuServer::add_unused_volume($conf, $old_volid) if !$param->{delete}; |
7043d946 | 2404 | |
e2cd75fa | 2405 | PVE::QemuServer::update_config_nolock($vmid, $conf, 1); |
586bfa78 AD |
2406 | }; |
2407 | if (my $err = $@) { | |
2408 | ||
2409 | foreach my $volid (@$newvollist) { | |
2410 | eval { PVE::Storage::vdisk_free($storecfg, $volid); }; | |
2411 | warn $@ if $@; | |
2412 | } | |
2413 | die "storage migration failed: $err"; | |
2414 | } | |
70d45e33 DM |
2415 | |
2416 | if ($param->{delete}) { | |
2417 | eval { PVE::Storage::vdisk_free($storecfg, $old_volid); }; | |
2418 | warn $@ if $@; | |
2419 | } | |
586bfa78 AD |
2420 | }; |
2421 | ||
2422 | return $rpcenv->fork_worker('qmmove', $vmid, $authuser, $realcmd); | |
2423 | }; | |
e2cd75fa DM |
2424 | |
2425 | return PVE::QemuServer::lock_config($vmid, $updatefn); | |
586bfa78 AD |
2426 | }}); |
2427 | ||
3ea94c60 | 2428 | __PACKAGE__->register_method({ |
afdb31d5 | 2429 | name => 'migrate_vm', |
3ea94c60 DM |
2430 | path => '{vmid}/migrate', |
2431 | method => 'POST', | |
2432 | protected => 1, | |
2433 | proxyto => 'node', | |
2434 | description => "Migrate virtual machine. Creates a new migration task.", | |
a0d1b1a2 DM |
2435 | permissions => { |
2436 | check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]], | |
2437 | }, | |
3ea94c60 DM |
2438 | parameters => { |
2439 | additionalProperties => 0, | |
2440 | properties => { | |
2441 | node => get_standard_option('pve-node'), | |
2442 | vmid => get_standard_option('pve-vmid'), | |
2443 | target => get_standard_option('pve-node', { description => "Target node." }), | |
2444 | online => { | |
2445 | type => 'boolean', | |
2446 | description => "Use online/live migration.", | |
2447 | optional => 1, | |
2448 | }, | |
2449 | force => { | |
2450 | type => 'boolean', | |
2451 | description => "Allow to migrate VMs which use local devices. Only root may use this option.", | |
2452 | optional => 1, | |
2453 | }, | |
2454 | }, | |
2455 | }, | |
afdb31d5 | 2456 | returns => { |
3ea94c60 DM |
2457 | type => 'string', |
2458 | description => "the task ID.", | |
2459 | }, | |
2460 | code => sub { | |
2461 | my ($param) = @_; | |
2462 | ||
2463 | my $rpcenv = PVE::RPCEnvironment::get(); | |
2464 | ||
a0d1b1a2 | 2465 | my $authuser = $rpcenv->get_user(); |
3ea94c60 DM |
2466 | |
2467 | my $target = extract_param($param, 'target'); | |
2468 | ||
2469 | my $localnode = PVE::INotify::nodename(); | |
2470 | raise_param_exc({ target => "target is local node."}) if $target eq $localnode; | |
2471 | ||
2472 | PVE::Cluster::check_cfs_quorum(); | |
2473 | ||
2474 | PVE::Cluster::check_node_exists($target); | |
2475 | ||
2476 | my $targetip = PVE::Cluster::remote_node_ip($target); | |
2477 | ||
2478 | my $vmid = extract_param($param, 'vmid'); | |
2479 | ||
afdb31d5 | 2480 | raise_param_exc({ force => "Only root may use this option." }) |
a0d1b1a2 | 2481 | if $param->{force} && $authuser ne 'root@pam'; |
3ea94c60 DM |
2482 | |
2483 | # test if VM exists | |
a5ed42d3 | 2484 | my $conf = PVE::QemuServer::load_config($vmid); |
3ea94c60 DM |
2485 | |
2486 | # try to detect errors early | |
a5ed42d3 DM |
2487 | |
2488 | PVE::QemuServer::check_lock($conf); | |
2489 | ||
3ea94c60 | 2490 | if (PVE::QemuServer::check_running($vmid)) { |
afdb31d5 | 2491 | die "cant migrate running VM without --online\n" |
3ea94c60 DM |
2492 | if !$param->{online}; |
2493 | } | |
2494 | ||
47152e2e | 2495 | my $storecfg = PVE::Storage::config(); |
22d646a7 | 2496 | PVE::QemuServer::check_storage_availability($storecfg, $conf, $target); |
47152e2e | 2497 | |
3be30d63 | 2498 | if (&$vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') { |
3ea94c60 | 2499 | |
88fc87b4 DM |
2500 | my $hacmd = sub { |
2501 | my $upid = shift; | |
3ea94c60 | 2502 | |
88fc87b4 DM |
2503 | my $service = "pvevm:$vmid"; |
2504 | ||
2505 | my $cmd = ['clusvcadm', '-M', $service, '-m', $target]; | |
2506 | ||
2507 | print "Executing HA migrate for VM $vmid to node $target\n"; | |
2508 | ||
2509 | PVE::Tools::run_command($cmd); | |
2510 | ||
2511 | return; | |
2512 | }; | |
2513 | ||
2514 | return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd); | |
2515 | ||
2516 | } else { | |
2517 | ||
2518 | my $realcmd = sub { | |
2519 | my $upid = shift; | |
2520 | ||
2521 | PVE::QemuMigrate->migrate($target, $targetip, $vmid, $param); | |
2522 | }; | |
2523 | ||
2524 | return $rpcenv->fork_worker('qmigrate', $vmid, $authuser, $realcmd); | |
2525 | } | |
3ea94c60 | 2526 | |
3ea94c60 | 2527 | }}); |
1e3baf05 | 2528 | |
91c94f0a | 2529 | __PACKAGE__->register_method({ |
afdb31d5 DM |
2530 | name => 'monitor', |
2531 | path => '{vmid}/monitor', | |
91c94f0a DM |
2532 | method => 'POST', |
2533 | protected => 1, | |
2534 | proxyto => 'node', | |
2535 | description => "Execute Qemu monitor commands.", | |
a0d1b1a2 DM |
2536 | permissions => { |
2537 | check => ['perm', '/vms/{vmid}', [ 'VM.Monitor' ]], | |
2538 | }, | |
91c94f0a DM |
2539 | parameters => { |
2540 | additionalProperties => 0, | |
2541 | properties => { | |
2542 | node => get_standard_option('pve-node'), | |
2543 | vmid => get_standard_option('pve-vmid'), | |
2544 | command => { | |
2545 | type => 'string', | |
2546 | description => "The monitor command.", | |
2547 | } | |
2548 | }, | |
2549 | }, | |
2550 | returns => { type => 'string'}, | |
2551 | code => sub { | |
2552 | my ($param) = @_; | |
2553 | ||
2554 | my $vmid = $param->{vmid}; | |
2555 | ||
2556 | my $conf = PVE::QemuServer::load_config ($vmid); # check if VM exists | |
2557 | ||
2558 | my $res = ''; | |
2559 | eval { | |
7b7c6d1b | 2560 | $res = PVE::QemuServer::vm_human_monitor_command($vmid, $param->{command}); |
91c94f0a DM |
2561 | }; |
2562 | $res = "ERROR: $@" if $@; | |
2563 | ||
2564 | return $res; | |
2565 | }}); | |
2566 | ||
0d02881c AD |
2567 | __PACKAGE__->register_method({ |
2568 | name => 'resize_vm', | |
614e3941 | 2569 | path => '{vmid}/resize', |
0d02881c AD |
2570 | method => 'PUT', |
2571 | protected => 1, | |
2572 | proxyto => 'node', | |
2f48a4f5 | 2573 | description => "Extend volume size.", |
0d02881c | 2574 | permissions => { |
3b2773f6 | 2575 | check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]], |
0d02881c AD |
2576 | }, |
2577 | parameters => { | |
2578 | additionalProperties => 0, | |
2f48a4f5 DM |
2579 | properties => { |
2580 | node => get_standard_option('pve-node'), | |
2581 | vmid => get_standard_option('pve-vmid'), | |
2582 | skiplock => get_standard_option('skiplock'), | |
2583 | disk => { | |
2584 | type => 'string', | |
2585 | description => "The disk you want to resize.", | |
2586 | enum => [PVE::QemuServer::disknames()], | |
2587 | }, | |
2588 | size => { | |
2589 | type => 'string', | |
f91b2e45 | 2590 | pattern => '\+?\d+(\.\d+)?[KMGT]?', |
2f48a4f5 DM |
2591 | description => "The new size. With the '+' sign the value is added to the actual size of the volume and without it, the value is taken as an absolute one. Shrinking disk size is not supported.", |
2592 | }, | |
2593 | digest => { | |
2594 | type => 'string', | |
2595 | description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.', | |
2596 | maxLength => 40, | |
2597 | optional => 1, | |
2598 | }, | |
2599 | }, | |
0d02881c AD |
2600 | }, |
2601 | returns => { type => 'null'}, | |
2602 | code => sub { | |
2603 | my ($param) = @_; | |
2604 | ||
2605 | my $rpcenv = PVE::RPCEnvironment::get(); | |
2606 | ||
2607 | my $authuser = $rpcenv->get_user(); | |
2608 | ||
2609 | my $node = extract_param($param, 'node'); | |
2610 | ||
2611 | my $vmid = extract_param($param, 'vmid'); | |
2612 | ||
2613 | my $digest = extract_param($param, 'digest'); | |
2614 | ||
2f48a4f5 | 2615 | my $disk = extract_param($param, 'disk'); |
75466c4f | 2616 | |
2f48a4f5 | 2617 | my $sizestr = extract_param($param, 'size'); |
0d02881c | 2618 | |
f91b2e45 | 2619 | my $skiplock = extract_param($param, 'skiplock'); |
0d02881c AD |
2620 | raise_param_exc({ skiplock => "Only root may use this option." }) |
2621 | if $skiplock && $authuser ne 'root@pam'; | |
2622 | ||
0d02881c AD |
2623 | my $storecfg = PVE::Storage::config(); |
2624 | ||
0d02881c AD |
2625 | my $updatefn = sub { |
2626 | ||
2627 | my $conf = PVE::QemuServer::load_config($vmid); | |
2628 | ||
2629 | die "checksum missmatch (file change by other user?)\n" | |
2630 | if $digest && $digest ne $conf->{digest}; | |
2631 | PVE::QemuServer::check_lock($conf) if !$skiplock; | |
2632 | ||
f91b2e45 DM |
2633 | die "disk '$disk' does not exist\n" if !$conf->{$disk}; |
2634 | ||
2635 | my $drive = PVE::QemuServer::parse_drive($disk, $conf->{$disk}); | |
2636 | ||
2637 | my $volid = $drive->{file}; | |
2638 | ||
2639 | die "disk '$disk' has no associated volume\n" if !$volid; | |
2640 | ||
2641 | die "you can't resize a cdrom\n" if PVE::QemuServer::drive_is_cdrom($drive); | |
2642 | ||
75466c4f | 2643 | die "you can't online resize a virtio windows bootdisk\n" |
f2965e67 AD |
2644 | if PVE::QemuServer::check_running($vmid) && $conf->{bootdisk} eq $disk && $conf->{ostype} =~ m/^w/ && $disk =~ m/^virtio/; |
2645 | ||
f91b2e45 DM |
2646 | my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid); |
2647 | ||
2648 | $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']); | |
2649 | ||
2650 | my $size = PVE::Storage::volume_size_info($storecfg, $volid, 5); | |
2651 | ||
2652 | die "internal error" if $sizestr !~ m/^(\+)?(\d+(\.\d+)?)([KMGT])?$/; | |
2653 | my ($ext, $newsize, $unit) = ($1, $2, $4); | |
2654 | if ($unit) { | |
2655 | if ($unit eq 'K') { | |
2656 | $newsize = $newsize * 1024; | |
2657 | } elsif ($unit eq 'M') { | |
2658 | $newsize = $newsize * 1024 * 1024; | |
2659 | } elsif ($unit eq 'G') { | |
2660 | $newsize = $newsize * 1024 * 1024 * 1024; | |
2661 | } elsif ($unit eq 'T') { | |
2662 | $newsize = $newsize * 1024 * 1024 * 1024 * 1024; | |
2663 | } | |
2664 | } | |
2665 | $newsize += $size if $ext; | |
2666 | $newsize = int($newsize); | |
2667 | ||
2668 | die "unable to skrink disk size\n" if $newsize < $size; | |
2669 | ||
2670 | return if $size == $newsize; | |
2671 | ||
2f48a4f5 | 2672 | PVE::Cluster::log_msg('info', $authuser, "update VM $vmid: resize --disk $disk --size $sizestr"); |
0d02881c | 2673 | |
f91b2e45 | 2674 | PVE::QemuServer::qemu_block_resize($vmid, "drive-$disk", $storecfg, $volid, $newsize); |
75466c4f | 2675 | |
f91b2e45 DM |
2676 | $drive->{size} = $newsize; |
2677 | $conf->{$disk} = PVE::QemuServer::print_drive($vmid, $drive); | |
2678 | ||
2679 | PVE::QemuServer::update_config_nolock($vmid, $conf, 1); | |
2680 | }; | |
0d02881c AD |
2681 | |
2682 | PVE::QemuServer::lock_config($vmid, $updatefn); | |
2683 | return undef; | |
2684 | }}); | |
2685 | ||
9dbd1ee4 | 2686 | __PACKAGE__->register_method({ |
7e7d7b61 | 2687 | name => 'snapshot_list', |
9dbd1ee4 | 2688 | path => '{vmid}/snapshot', |
7e7d7b61 DM |
2689 | method => 'GET', |
2690 | description => "List all snapshots.", | |
2691 | permissions => { | |
2692 | check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]], | |
2693 | }, | |
2694 | proxyto => 'node', | |
2695 | protected => 1, # qemu pid files are only readable by root | |
2696 | parameters => { | |
2697 | additionalProperties => 0, | |
2698 | properties => { | |
2699 | vmid => get_standard_option('pve-vmid'), | |
2700 | node => get_standard_option('pve-node'), | |
2701 | }, | |
2702 | }, | |
2703 | returns => { | |
2704 | type => 'array', | |
2705 | items => { | |
2706 | type => "object", | |
2707 | properties => {}, | |
2708 | }, | |
2709 | links => [ { rel => 'child', href => "{name}" } ], | |
2710 | }, | |
2711 | code => sub { | |
2712 | my ($param) = @_; | |
2713 | ||
6aa4651b DM |
2714 | my $vmid = $param->{vmid}; |
2715 | ||
2716 | my $conf = PVE::QemuServer::load_config($vmid); | |
7e7d7b61 DM |
2717 | my $snaphash = $conf->{snapshots} || {}; |
2718 | ||
2719 | my $res = []; | |
2720 | ||
2721 | foreach my $name (keys %$snaphash) { | |
0ea6bc69 | 2722 | my $d = $snaphash->{$name}; |
75466c4f DM |
2723 | my $item = { |
2724 | name => $name, | |
2725 | snaptime => $d->{snaptime} || 0, | |
6aa4651b | 2726 | vmstate => $d->{vmstate} ? 1 : 0, |
982c7f12 DM |
2727 | description => $d->{description} || '', |
2728 | }; | |
0ea6bc69 | 2729 | $item->{parent} = $d->{parent} if $d->{parent}; |
3ee28e38 | 2730 | $item->{snapstate} = $d->{snapstate} if $d->{snapstate}; |
0ea6bc69 DM |
2731 | push @$res, $item; |
2732 | } | |
2733 | ||
6aa4651b DM |
2734 | my $running = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0; |
2735 | my $current = { name => 'current', digest => $conf->{digest}, running => $running }; | |
d1914468 DM |
2736 | $current->{parent} = $conf->{parent} if $conf->{parent}; |
2737 | ||
2738 | push @$res, $current; | |
7e7d7b61 DM |
2739 | |
2740 | return $res; | |
2741 | }}); | |
2742 | ||
2743 | __PACKAGE__->register_method({ | |
2744 | name => 'snapshot', | |
2745 | path => '{vmid}/snapshot', | |
2746 | method => 'POST', | |
9dbd1ee4 AD |
2747 | protected => 1, |
2748 | proxyto => 'node', | |
2749 | description => "Snapshot a VM.", | |
2750 | permissions => { | |
f1baf1df | 2751 | check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]], |
9dbd1ee4 AD |
2752 | }, |
2753 | parameters => { | |
2754 | additionalProperties => 0, | |
2755 | properties => { | |
2756 | node => get_standard_option('pve-node'), | |
2757 | vmid => get_standard_option('pve-vmid'), | |
8abd398b | 2758 | snapname => get_standard_option('pve-snapshot-name'), |
9dbd1ee4 AD |
2759 | vmstate => { |
2760 | optional => 1, | |
2761 | type => 'boolean', | |
2762 | description => "Save the vmstate", | |
2763 | }, | |
2764 | freezefs => { | |
2765 | optional => 1, | |
2766 | type => 'boolean', | |
2767 | description => "Freeze the filesystem", | |
2768 | }, | |
782f4f75 DM |
2769 | description => { |
2770 | optional => 1, | |
2771 | type => 'string', | |
2772 | description => "A textual description or comment.", | |
2773 | }, | |
9dbd1ee4 AD |
2774 | }, |
2775 | }, | |
7e7d7b61 DM |
2776 | returns => { |
2777 | type => 'string', | |
2778 | description => "the task ID.", | |
2779 | }, | |
9dbd1ee4 AD |
2780 | code => sub { |
2781 | my ($param) = @_; | |
2782 | ||
2783 | my $rpcenv = PVE::RPCEnvironment::get(); | |
2784 | ||
2785 | my $authuser = $rpcenv->get_user(); | |
2786 | ||
2787 | my $node = extract_param($param, 'node'); | |
2788 | ||
2789 | my $vmid = extract_param($param, 'vmid'); | |
2790 | ||
9dbd1ee4 AD |
2791 | my $snapname = extract_param($param, 'snapname'); |
2792 | ||
d1914468 DM |
2793 | die "unable to use snapshot name 'current' (reserved name)\n" |
2794 | if $snapname eq 'current'; | |
2795 | ||
7e7d7b61 | 2796 | my $realcmd = sub { |
22c377f0 | 2797 | PVE::Cluster::log_msg('info', $authuser, "snapshot VM $vmid: $snapname"); |
75466c4f | 2798 | PVE::QemuServer::snapshot_create($vmid, $snapname, $param->{vmstate}, |
782f4f75 | 2799 | $param->{freezefs}, $param->{description}); |
7e7d7b61 DM |
2800 | }; |
2801 | ||
2802 | return $rpcenv->fork_worker('qmsnapshot', $vmid, $authuser, $realcmd); | |
2803 | }}); | |
2804 | ||
154ccdcd DM |
2805 | __PACKAGE__->register_method({ |
2806 | name => 'snapshot_cmd_idx', | |
2807 | path => '{vmid}/snapshot/{snapname}', | |
2808 | description => '', | |
2809 | method => 'GET', | |
2810 | permissions => { | |
2811 | user => 'all', | |
2812 | }, | |
2813 | parameters => { | |
2814 | additionalProperties => 0, | |
2815 | properties => { | |
2816 | vmid => get_standard_option('pve-vmid'), | |
2817 | node => get_standard_option('pve-node'), | |
8abd398b | 2818 | snapname => get_standard_option('pve-snapshot-name'), |
154ccdcd DM |
2819 | }, |
2820 | }, | |
2821 | returns => { | |
2822 | type => 'array', | |
2823 | items => { | |
2824 | type => "object", | |
2825 | properties => {}, | |
2826 | }, | |
2827 | links => [ { rel => 'child', href => "{cmd}" } ], | |
2828 | }, | |
2829 | code => sub { | |
2830 | my ($param) = @_; | |
2831 | ||
2832 | my $res = []; | |
2833 | ||
2834 | push @$res, { cmd => 'rollback' }; | |
d788cea6 | 2835 | push @$res, { cmd => 'config' }; |
154ccdcd DM |
2836 | |
2837 | return $res; | |
2838 | }}); | |
2839 | ||
d788cea6 DM |
2840 | __PACKAGE__->register_method({ |
2841 | name => 'update_snapshot_config', | |
2842 | path => '{vmid}/snapshot/{snapname}/config', | |
2843 | method => 'PUT', | |
2844 | protected => 1, | |
2845 | proxyto => 'node', | |
2846 | description => "Update snapshot metadata.", | |
2847 | permissions => { | |
2848 | check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]], | |
2849 | }, | |
2850 | parameters => { | |
2851 | additionalProperties => 0, | |
2852 | properties => { | |
2853 | node => get_standard_option('pve-node'), | |
2854 | vmid => get_standard_option('pve-vmid'), | |
2855 | snapname => get_standard_option('pve-snapshot-name'), | |
2856 | description => { | |
2857 | optional => 1, | |
2858 | type => 'string', | |
2859 | description => "A textual description or comment.", | |
2860 | }, | |
2861 | }, | |
2862 | }, | |
2863 | returns => { type => 'null' }, | |
2864 | code => sub { | |
2865 | my ($param) = @_; | |
2866 | ||
2867 | my $rpcenv = PVE::RPCEnvironment::get(); | |
2868 | ||
2869 | my $authuser = $rpcenv->get_user(); | |
2870 | ||
2871 | my $vmid = extract_param($param, 'vmid'); | |
2872 | ||
2873 | my $snapname = extract_param($param, 'snapname'); | |
2874 | ||
2875 | return undef if !defined($param->{description}); | |
2876 | ||
2877 | my $updatefn = sub { | |
2878 | ||
2879 | my $conf = PVE::QemuServer::load_config($vmid); | |
2880 | ||
2881 | PVE::QemuServer::check_lock($conf); | |
2882 | ||
2883 | my $snap = $conf->{snapshots}->{$snapname}; | |
2884 | ||
75466c4f DM |
2885 | die "snapshot '$snapname' does not exist\n" if !defined($snap); |
2886 | ||
d788cea6 DM |
2887 | $snap->{description} = $param->{description} if defined($param->{description}); |
2888 | ||
2889 | PVE::QemuServer::update_config_nolock($vmid, $conf, 1); | |
2890 | }; | |
2891 | ||
2892 | PVE::QemuServer::lock_config($vmid, $updatefn); | |
2893 | ||
2894 | return undef; | |
2895 | }}); | |
2896 | ||
2897 | __PACKAGE__->register_method({ | |
2898 | name => 'get_snapshot_config', | |
2899 | path => '{vmid}/snapshot/{snapname}/config', | |
2900 | method => 'GET', | |
2901 | proxyto => 'node', | |
2902 | description => "Get snapshot configuration", | |
2903 | permissions => { | |
2904 | check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]], | |
2905 | }, | |
2906 | parameters => { | |
2907 | additionalProperties => 0, | |
2908 | properties => { | |
2909 | node => get_standard_option('pve-node'), | |
2910 | vmid => get_standard_option('pve-vmid'), | |
2911 | snapname => get_standard_option('pve-snapshot-name'), | |
2912 | }, | |
2913 | }, | |
2914 | returns => { type => "object" }, | |
2915 | code => sub { | |
2916 | my ($param) = @_; | |
2917 | ||
2918 | my $rpcenv = PVE::RPCEnvironment::get(); | |
2919 | ||
2920 | my $authuser = $rpcenv->get_user(); | |
2921 | ||
2922 | my $vmid = extract_param($param, 'vmid'); | |
2923 | ||
2924 | my $snapname = extract_param($param, 'snapname'); | |
2925 | ||
2926 | my $conf = PVE::QemuServer::load_config($vmid); | |
2927 | ||
2928 | my $snap = $conf->{snapshots}->{$snapname}; | |
2929 | ||
75466c4f DM |
2930 | die "snapshot '$snapname' does not exist\n" if !defined($snap); |
2931 | ||
d788cea6 DM |
2932 | return $snap; |
2933 | }}); | |
2934 | ||
7e7d7b61 DM |
2935 | __PACKAGE__->register_method({ |
2936 | name => 'rollback', | |
154ccdcd | 2937 | path => '{vmid}/snapshot/{snapname}/rollback', |
7e7d7b61 DM |
2938 | method => 'POST', |
2939 | protected => 1, | |
2940 | proxyto => 'node', | |
2941 | description => "Rollback VM state to specified snapshot.", | |
2942 | permissions => { | |
f1baf1df | 2943 | check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]], |
7e7d7b61 DM |
2944 | }, |
2945 | parameters => { | |
2946 | additionalProperties => 0, | |
2947 | properties => { | |
2948 | node => get_standard_option('pve-node'), | |
2949 | vmid => get_standard_option('pve-vmid'), | |
8abd398b | 2950 | snapname => get_standard_option('pve-snapshot-name'), |
7e7d7b61 DM |
2951 | }, |
2952 | }, | |
2953 | returns => { | |
2954 | type => 'string', | |
2955 | description => "the task ID.", | |
2956 | }, | |
2957 | code => sub { | |
2958 | my ($param) = @_; | |
2959 | ||
2960 | my $rpcenv = PVE::RPCEnvironment::get(); | |
2961 | ||
2962 | my $authuser = $rpcenv->get_user(); | |
2963 | ||
2964 | my $node = extract_param($param, 'node'); | |
2965 | ||
2966 | my $vmid = extract_param($param, 'vmid'); | |
2967 | ||
2968 | my $snapname = extract_param($param, 'snapname'); | |
2969 | ||
7e7d7b61 | 2970 | my $realcmd = sub { |
22c377f0 DM |
2971 | PVE::Cluster::log_msg('info', $authuser, "rollback snapshot VM $vmid: $snapname"); |
2972 | PVE::QemuServer::snapshot_rollback($vmid, $snapname); | |
7e7d7b61 DM |
2973 | }; |
2974 | ||
2975 | return $rpcenv->fork_worker('qmrollback', $vmid, $authuser, $realcmd); | |
2976 | }}); | |
2977 | ||
2978 | __PACKAGE__->register_method({ | |
2979 | name => 'delsnapshot', | |
2980 | path => '{vmid}/snapshot/{snapname}', | |
2981 | method => 'DELETE', | |
2982 | protected => 1, | |
2983 | proxyto => 'node', | |
2984 | description => "Delete a VM snapshot.", | |
2985 | permissions => { | |
f1baf1df | 2986 | check => ['perm', '/vms/{vmid}', [ 'VM.Snapshot' ]], |
7e7d7b61 DM |
2987 | }, |
2988 | parameters => { | |
2989 | additionalProperties => 0, | |
2990 | properties => { | |
2991 | node => get_standard_option('pve-node'), | |
2992 | vmid => get_standard_option('pve-vmid'), | |
8abd398b | 2993 | snapname => get_standard_option('pve-snapshot-name'), |
3ee28e38 DM |
2994 | force => { |
2995 | optional => 1, | |
2996 | type => 'boolean', | |
2997 | description => "For removal from config file, even if removing disk snapshots fails.", | |
2998 | }, | |
7e7d7b61 DM |
2999 | }, |
3000 | }, | |
3001 | returns => { | |
3002 | type => 'string', | |
3003 | description => "the task ID.", | |
3004 | }, | |
3005 | code => sub { | |
3006 | my ($param) = @_; | |
3007 | ||
3008 | my $rpcenv = PVE::RPCEnvironment::get(); | |
3009 | ||
3010 | my $authuser = $rpcenv->get_user(); | |
3011 | ||
3012 | my $node = extract_param($param, 'node'); | |
3013 | ||
3014 | my $vmid = extract_param($param, 'vmid'); | |
3015 | ||
3016 | my $snapname = extract_param($param, 'snapname'); | |
3017 | ||
7e7d7b61 | 3018 | my $realcmd = sub { |
22c377f0 | 3019 | PVE::Cluster::log_msg('info', $authuser, "delete snapshot VM $vmid: $snapname"); |
3ee28e38 | 3020 | PVE::QemuServer::snapshot_delete($vmid, $snapname, $param->{force}); |
7e7d7b61 | 3021 | }; |
9dbd1ee4 | 3022 | |
7b2257a8 | 3023 | return $rpcenv->fork_worker('qmdelsnapshot', $vmid, $authuser, $realcmd); |
9dbd1ee4 AD |
3024 | }}); |
3025 | ||
04a69bb4 AD |
3026 | __PACKAGE__->register_method({ |
3027 | name => 'template', | |
3028 | path => '{vmid}/template', | |
3029 | method => 'POST', | |
3030 | protected => 1, | |
3031 | proxyto => 'node', | |
3032 | description => "Create a Template.", | |
b02691d8 | 3033 | permissions => { |
7af0a6c8 DM |
3034 | description => "You need 'VM.Allocate' permissions on /vms/{vmid}", |
3035 | check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']], | |
b02691d8 | 3036 | }, |
04a69bb4 AD |
3037 | parameters => { |
3038 | additionalProperties => 0, | |
3039 | properties => { | |
3040 | node => get_standard_option('pve-node'), | |
3041 | vmid => get_standard_option('pve-vmid'), | |
3042 | disk => { | |
3043 | optional => 1, | |
3044 | type => 'string', | |
3045 | description => "If you want to convert only 1 disk to base image.", | |
3046 | enum => [PVE::QemuServer::disknames()], | |
3047 | }, | |
3048 | ||
3049 | }, | |
3050 | }, | |
3051 | returns => { type => 'null'}, | |
3052 | code => sub { | |
3053 | my ($param) = @_; | |
3054 | ||
3055 | my $rpcenv = PVE::RPCEnvironment::get(); | |
3056 | ||
3057 | my $authuser = $rpcenv->get_user(); | |
3058 | ||
3059 | my $node = extract_param($param, 'node'); | |
3060 | ||
3061 | my $vmid = extract_param($param, 'vmid'); | |
3062 | ||
3063 | my $disk = extract_param($param, 'disk'); | |
3064 | ||
3065 | my $updatefn = sub { | |
3066 | ||
3067 | my $conf = PVE::QemuServer::load_config($vmid); | |
3068 | ||
3069 | PVE::QemuServer::check_lock($conf); | |
3070 | ||
75466c4f | 3071 | die "unable to create template, because VM contains snapshots\n" |
b91c2aae | 3072 | if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}}); |
0402a80b | 3073 | |
75466c4f | 3074 | die "you can't convert a template to a template\n" |
03c2d0ad | 3075 | if PVE::QemuServer::is_template($conf) && !$disk; |
0402a80b | 3076 | |
75466c4f | 3077 | die "you can't convert a VM to template if VM is running\n" |
218cab9a | 3078 | if PVE::QemuServer::check_running($vmid); |
35c5fdef | 3079 | |
04a69bb4 AD |
3080 | my $realcmd = sub { |
3081 | PVE::QemuServer::template_create($vmid, $conf, $disk); | |
3082 | }; | |
04a69bb4 | 3083 | |
75e7e997 | 3084 | $conf->{template} = 1; |
04a69bb4 | 3085 | PVE::QemuServer::update_config_nolock($vmid, $conf, 1); |
75e7e997 DM |
3086 | |
3087 | return $rpcenv->fork_worker('qmtemplate', $vmid, $authuser, $realcmd); | |
04a69bb4 AD |
3088 | }; |
3089 | ||
3090 | PVE::QemuServer::lock_config($vmid, $updatefn); | |
3091 | return undef; | |
3092 | }}); | |
3093 | ||
1e3baf05 | 3094 | 1; |