]> git.proxmox.com Git - pve-container.git/blob - src/PVE/API2/LXC.pm
fix #5414: use proper percentages in `pct df`
[pve-container.git] / src / PVE / API2 / LXC.pm
1 package PVE::API2::LXC;
2
3 use strict;
4 use warnings;
5
6 use IO::Socket::UNIX;
7 use Socket qw(SOCK_STREAM);
8
9 use PVE::SafeSyslog;
10 use PVE::Tools qw(extract_param run_command);
11 use PVE::Exception qw(raise raise_param_exc raise_perm_exc);
12 use PVE::INotify;
13 use PVE::Cluster qw(cfs_read_file);
14 use PVE::RRD;
15 use PVE::DataCenterConfig;
16 use PVE::AccessControl;
17 use PVE::Firewall;
18 use PVE::Storage;
19 use PVE::RESTHandler;
20 use PVE::RPCEnvironment;
21 use PVE::ReplicationConfig;
22 use PVE::LXC;
23 use PVE::LXC::Create;
24 use PVE::LXC::Migrate;
25 use PVE::GuestHelpers;
26 use PVE::VZDump::Plugin;
27 use PVE::API2::LXC::Config;
28 use PVE::API2::LXC::Status;
29 use PVE::API2::LXC::Snapshot;
30 use PVE::JSONSchema qw(get_standard_option);
31 use base qw(PVE::RESTHandler);
32
33 BEGIN {
34 if (!$ENV{PVE_GENERATING_DOCS}) {
35 require PVE::HA::Env::PVE2;
36 import PVE::HA::Env::PVE2;
37 require PVE::HA::Config;
38 import PVE::HA::Config;
39 }
40 }
41
42 my $check_storage_access_migrate = sub {
43 my ($rpcenv, $authuser, $storecfg, $storage, $node) = @_;
44
45 PVE::Storage::storage_check_enabled($storecfg, $storage, $node);
46
47 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
48
49 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
50 die "storage '$storage' does not support CT rootdirs\n"
51 if !$scfg->{content}->{rootdir};
52 };
53
54 __PACKAGE__->register_method ({
55 subclass => "PVE::API2::LXC::Config",
56 path => '{vmid}/config',
57 });
58
59 __PACKAGE__->register_method ({
60 subclass => "PVE::API2::LXC::Status",
61 path => '{vmid}/status',
62 });
63
64 __PACKAGE__->register_method ({
65 subclass => "PVE::API2::LXC::Snapshot",
66 path => '{vmid}/snapshot',
67 });
68
69 __PACKAGE__->register_method ({
70 subclass => "PVE::API2::Firewall::CT",
71 path => '{vmid}/firewall',
72 });
73
74 __PACKAGE__->register_method({
75 name => 'vmlist',
76 path => '',
77 method => 'GET',
78 description => "LXC container index (per node).",
79 permissions => {
80 description => "Only list CTs where you have VM.Audit permissons on /vms/<vmid>.",
81 user => 'all',
82 },
83 proxyto => 'node',
84 protected => 1, # /proc files are only readable by root
85 parameters => {
86 additionalProperties => 0,
87 properties => {
88 node => get_standard_option('pve-node'),
89 },
90 },
91 returns => {
92 type => 'array',
93 items => {
94 type => "object",
95 properties => $PVE::LXC::vmstatus_return_properties,
96 },
97 links => [ { rel => 'child', href => "{vmid}" } ],
98 },
99 code => sub {
100 my ($param) = @_;
101
102 my $rpcenv = PVE::RPCEnvironment::get();
103 my $authuser = $rpcenv->get_user();
104
105 my $vmstatus = PVE::LXC::vmstatus();
106
107 my $res = [];
108 foreach my $vmid (keys %$vmstatus) {
109 next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
110
111 my $data = $vmstatus->{$vmid};
112 push @$res, $data;
113 }
114
115 return $res;
116
117 }});
118
119 __PACKAGE__->register_method({
120 name => 'create_vm',
121 path => '',
122 method => 'POST',
123 description => "Create or restore a container.",
124 permissions => {
125 user => 'all', # check inside
126 description => "You need 'VM.Allocate' permissions on /vms/{vmid} or on the VM pool /pool/{pool}. " .
127 "For restore, it is enough if the user has 'VM.Backup' permission and the VM already exists. " .
128 "You also need 'Datastore.AllocateSpace' permissions on the storage.",
129 },
130 protected => 1,
131 proxyto => 'node',
132 parameters => {
133 additionalProperties => 0,
134 properties => PVE::LXC::Config->json_config_properties({
135 node => get_standard_option('pve-node'),
136 vmid => get_standard_option('pve-vmid', { completion => \&PVE::Cluster::complete_next_vmid }),
137 ostemplate => {
138 description => "The OS template or backup file.",
139 type => 'string',
140 maxLength => 255,
141 completion => \&PVE::LXC::complete_os_templates,
142 },
143 password => {
144 optional => 1,
145 type => 'string',
146 description => "Sets root password inside container.",
147 minLength => 5,
148 },
149 storage => get_standard_option('pve-storage-id', {
150 description => "Default Storage.",
151 default => 'local',
152 optional => 1,
153 completion => \&PVE::Storage::complete_storage_enabled,
154 }),
155 force => {
156 optional => 1,
157 type => 'boolean',
158 description => "Allow to overwrite existing container.",
159 },
160 restore => {
161 optional => 1,
162 type => 'boolean',
163 description => "Mark this as restore task.",
164 },
165 unique => {
166 optional => 1,
167 type => 'boolean',
168 description => "Assign a unique random ethernet address.",
169 requires => 'restore',
170 },
171 pool => {
172 optional => 1,
173 type => 'string', format => 'pve-poolid',
174 description => "Add the VM to the specified pool.",
175 },
176 'ignore-unpack-errors' => {
177 optional => 1,
178 type => 'boolean',
179 description => "Ignore errors when extracting the template.",
180 },
181 'ssh-public-keys' => {
182 optional => 1,
183 type => 'string',
184 description => "Setup public SSH keys (one key per line, " .
185 "OpenSSH format).",
186 },
187 bwlimit => {
188 description => "Override I/O bandwidth limit (in KiB/s).",
189 optional => 1,
190 type => 'number',
191 minimum => '0',
192 default => 'restore limit from datacenter or storage config',
193 },
194 start => {
195 optional => 1,
196 type => 'boolean',
197 default => 0,
198 description => "Start the CT after its creation finished successfully.",
199 },
200 }),
201 },
202 returns => {
203 type => 'string',
204 },
205 code => sub {
206 my ($param) = @_;
207
208 PVE::Cluster::check_cfs_quorum();
209
210 my $rpcenv = PVE::RPCEnvironment::get();
211 my $authuser = $rpcenv->get_user();
212
213 my $node = extract_param($param, 'node');
214 my $vmid = extract_param($param, 'vmid');
215 my $ignore_unpack_errors = extract_param($param, 'ignore-unpack-errors');
216 my $bwlimit = extract_param($param, 'bwlimit');
217 my $start_after_create = extract_param($param, 'start');
218
219 my $basecfg_fn = PVE::LXC::Config->config_file($vmid);
220 my $same_container_exists = -f $basecfg_fn;
221
222 # 'unprivileged' is read-only, so we can't pass it to update_pct_config
223 my $unprivileged = extract_param($param, 'unprivileged');
224 my $restore = extract_param($param, 'restore');
225 my $unique = extract_param($param, 'unique');
226
227 $param->{cpuunits} = PVE::CGroup::clamp_cpu_shares($param->{cpuunits})
228 if defined($param->{cpuunits}); # clamp value depending on cgroup version
229
230 # used to skip firewall config restore if user lacks permission
231 my $skip_fw_config_restore = 0;
232
233 if ($restore) {
234 # fixme: limit allowed parameters
235 }
236
237 my $force = extract_param($param, 'force');
238
239 if (!($same_container_exists && $restore && $force)) {
240 PVE::Cluster::check_vmid_unused($vmid);
241 } else {
242 die "can't overwrite running container\n" if PVE::LXC::check_running($vmid);
243 my $conf = PVE::LXC::Config->load_config($vmid);
244 PVE::LXC::Config->check_protection($conf, "unable to restore CT $vmid");
245 }
246
247 my $password = extract_param($param, 'password');
248 my $ssh_keys = extract_param($param, 'ssh-public-keys');
249 PVE::Tools::validate_ssh_public_keys($ssh_keys) if defined($ssh_keys);
250
251 my $pool = extract_param($param, 'pool');
252 $rpcenv->check_pool_exist($pool) if defined($pool);
253
254 if ($rpcenv->check($authuser, "/vms/$vmid", ['VM.Allocate'], 1)) {
255 # OK
256 } elsif ($pool && $rpcenv->check($authuser, "/pool/$pool", ['VM.Allocate'], 1)) {
257 # OK
258 } elsif ($restore && $force && $same_container_exists &&
259 $rpcenv->check($authuser, "/vms/$vmid", ['VM.Backup'], 1)) {
260 # OK: user has VM.Backup permissions, and want to restore an existing VM
261
262 # we don't want to restore a container-provided FW conf in this case
263 # since the user is lacking permission to configure the container's FW
264 $skip_fw_config_restore = 1;
265
266 # error out if a user tries to change from unprivileged to privileged
267 # explicit change is checked here, implicit is checked down below or happening in root-only paths
268 my $conf = PVE::LXC::Config->load_config($vmid);
269 if ($conf->{unprivileged} && defined($unprivileged) && !$unprivileged) {
270 raise_perm_exc("cannot change from unprivileged to privileged without VM.Allocate");
271 }
272 } else {
273 raise_perm_exc();
274 }
275
276 my $ostemplate = extract_param($param, 'ostemplate');
277 my $storage = extract_param($param, 'storage') // 'local';
278
279 PVE::LXC::check_ct_modify_config_perm($rpcenv, $authuser, $vmid, $pool, undef, $param, [], $unprivileged);
280
281 my $storage_cfg = cfs_read_file("storage.cfg");
282
283 my $archive;
284 if ($ostemplate eq '-') {
285 die "pipe requires cli environment\n"
286 if $rpcenv->{type} ne 'cli';
287 die "pipe can only be used with restore tasks\n"
288 if !$restore;
289 $archive = '-';
290 die "restore from pipe requires rootfs parameter\n" if !defined($param->{rootfs});
291 } else {
292 my $content_type = $restore ? 'backup' : 'vztmpl';
293 PVE::Storage::check_volume_access(
294 $rpcenv,
295 $authuser,
296 $storage_cfg,
297 $vmid,
298 $ostemplate,
299 $content_type,
300 );
301 $archive = $ostemplate;
302 }
303
304 my %used_storages;
305 my $check_and_activate_storage = sub {
306 my ($sid) = @_;
307
308 my $scfg = PVE::Storage::storage_check_enabled($storage_cfg, $sid, $node);
309
310 raise_param_exc({ storage => "storage '$sid' does not support container directories"})
311 if !$scfg->{content}->{rootdir};
312
313 $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
314
315 PVE::Storage::activate_storage($storage_cfg, $sid);
316 $used_storages{$sid} = 1;
317 };
318
319 my $conf = {};
320
321 my $is_root = $authuser eq 'root@pam';
322
323 my $no_disk_param = {};
324 my $mp_param = {};
325 my $storage_only_mode = 1;
326 foreach my $opt (keys %$param) {
327 my $value = $param->{$opt};
328 if ($opt eq 'rootfs' || $opt =~ m/^mp\d+$/) {
329 # allow to use simple numbers (add default storage in that case)
330 if ($value =~ m/^\d+(\.\d+)?$/) {
331 $mp_param->{$opt} = "$storage:$value";
332 } else {
333 $mp_param->{$opt} = $value;
334 }
335 $storage_only_mode = 0;
336 } elsif ($opt =~ m/^unused\d+$/) {
337 warn "ignoring '$opt', cannot create/restore with unused volume\n";
338 delete $param->{$opt};
339 } else {
340 $no_disk_param->{$opt} = $value;
341 }
342 }
343
344 die "mount points configured, but 'rootfs' not set - aborting\n"
345 if !$storage_only_mode && !defined($mp_param->{rootfs});
346
347 # check storage access, activate storage
348 my $delayed_mp_param = {};
349 PVE::LXC::Config->foreach_volume($mp_param, sub {
350 my ($ms, $mountpoint) = @_;
351
352 my $volid = $mountpoint->{volume};
353 my $mp = $mountpoint->{mp};
354
355 if ($mountpoint->{type} ne 'volume') { # bind or device
356 die "Only root can pass arbitrary filesystem paths.\n"
357 if !$is_root;
358 } else {
359 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
360 &$check_and_activate_storage($sid);
361 }
362 });
363
364 # check/activate default storage
365 &$check_and_activate_storage($storage) if !defined($mp_param->{rootfs});
366
367 PVE::LXC::Config->update_pct_config($vmid, $conf, 0, $no_disk_param);
368
369 $conf->{unprivileged} = 1 if $unprivileged;
370
371 my $emsg = $restore ? "unable to restore CT $vmid -" : "unable to create CT $vmid -";
372
373 eval { PVE::LXC::Config->create_and_lock_config($vmid, $force) };
374 die "$emsg $@" if $@;
375
376 my $destroy_config_on_error = !$same_container_exists;
377
378 my $code = sub {
379 my $old_conf = PVE::LXC::Config->load_config($vmid);
380 my $was_template;
381
382 my $vollist = [];
383 eval {
384 my $orig_mp_param; # only used if $restore
385 if ($restore) {
386 die "can't overwrite running container\n" if PVE::LXC::check_running($vmid);
387 if ($archive ne '-') {
388 my $orig_conf;
389 print "recovering backed-up configuration from '$archive'\n";
390 ($orig_conf, $orig_mp_param) = PVE::LXC::Create::recover_config($storage_cfg, $archive, $vmid);
391
392 for my $opt (keys %$orig_conf) {
393 # early check before disks are created
394 # the "real" check is in later on when actually merging the configs
395 if ($opt =~ /^net\d+$/ && !defined($param->{$opt})) {
396 PVE::LXC::check_bridge_access($rpcenv, $authuser, $orig_conf->{$opt});
397 }
398 }
399
400 $was_template = delete $orig_conf->{template};
401
402 # When we're root call 'restore_configuration' with restricted=0,
403 # causing it to restore the raw lxc entries, among which there may be
404 # 'lxc.idmap' entries. We need to make sure that the extracted contents
405 # of the container match up with the restored configuration afterwards:
406 $conf->{lxc} = $orig_conf->{lxc} if $is_root;
407
408 $conf->{unprivileged} = $orig_conf->{unprivileged}
409 if !defined($unprivileged) && defined($orig_conf->{unprivileged});
410
411 # implicit privileged change is checked here
412 if ($old_conf->{unprivileged} && !$conf->{unprivileged}) {
413 $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Allocate']);
414 }
415 }
416 }
417 if ($storage_only_mode) {
418 if ($restore) {
419 if (!defined($orig_mp_param)) {
420 print "recovering backed-up configuration from '$archive'\n";
421 (undef, $orig_mp_param) = PVE::LXC::Create::recover_config($storage_cfg, $archive, $vmid);
422 }
423 $mp_param = $orig_mp_param;
424 die "rootfs configuration could not be recovered, please check and specify manually!\n"
425 if !defined($mp_param->{rootfs});
426 PVE::LXC::Config->foreach_volume($mp_param, sub {
427 my ($ms, $mountpoint) = @_;
428 my $type = $mountpoint->{type};
429 if ($type eq 'volume') {
430 die "unable to detect disk size - please specify $ms (size)\n"
431 if !defined($mountpoint->{size});
432 my $disksize = $mountpoint->{size} / (1024 * 1024 * 1024); # create_disks expects GB as unit size
433 delete $mountpoint->{size};
434 $mountpoint->{volume} = "$storage:$disksize";
435 $mp_param->{$ms} = PVE::LXC::Config->print_ct_mountpoint($mountpoint, $ms eq 'rootfs');
436 } else {
437 my $type = $mountpoint->{type};
438 die "restoring rootfs to $type mount is only possible by specifying -rootfs manually!\n"
439 if ($ms eq 'rootfs');
440 die "restoring '$ms' to $type mount is only possible for root\n"
441 if !$is_root;
442
443 if ($mountpoint->{backup}) {
444 warn "WARNING - unsupported configuration!\n";
445 warn "backup was enabled for $type mount point $ms ('$mountpoint->{mp}')\n";
446 warn "mount point configuration will be restored after archive extraction!\n";
447 warn "contained files will be restored to wrong directory!\n";
448 }
449 delete $mp_param->{$ms}; # actually delay bind/dev mps
450 $delayed_mp_param->{$ms} = PVE::LXC::Config->print_ct_mountpoint($mountpoint, $ms eq 'rootfs');
451 }
452 });
453 } else {
454 $mp_param->{rootfs} = "$storage:4"; # defaults to 4GB
455 }
456 }
457
458 # up until here we did not modify the container, besides the lock
459 $destroy_config_on_error = 1;
460
461 $vollist = PVE::LXC::create_disks($storage_cfg, $vmid, $mp_param, $conf);
462
463 # we always have the 'create' lock so check for more than 1 entry
464 if (scalar(keys %$old_conf) > 1) {
465 # destroy old container volumes
466 PVE::LXC::destroy_lxc_container($storage_cfg, $vmid, $old_conf, { lock => 'create' });
467 }
468
469 eval {
470 my $rootdir = PVE::LXC::mount_all($vmid, $storage_cfg, $conf, 1);
471 $bwlimit = PVE::Storage::get_bandwidth_limit('restore', [keys %used_storages], $bwlimit);
472 print "restoring '$archive' now..\n"
473 if $restore && $archive ne '-';
474 PVE::LXC::Create::restore_archive($storage_cfg, $archive, $rootdir, $conf, $ignore_unpack_errors, $bwlimit);
475
476 if ($restore) {
477 print "merging backed-up and given configuration..\n";
478 PVE::LXC::Create::restore_configuration($vmid, $storage_cfg, $archive, $rootdir, $conf, !$is_root, $unique, $skip_fw_config_restore);
479 PVE::LXC::create_ifaces_ipams_ips($conf, $vmid) if $unique;
480 my $lxc_setup = PVE::LXC::Setup->new($conf, $rootdir);
481 $lxc_setup->template_fixup($conf);
482 } else {
483 my $lxc_setup = PVE::LXC::Setup->new($conf, $rootdir); # detect OS
484 PVE::LXC::Config->write_config($vmid, $conf); # safe config (after OS detection)
485 $lxc_setup->post_create_hook($password, $ssh_keys);
486 }
487 };
488 my $err = $@;
489 PVE::LXC::umount_all($vmid, $storage_cfg, $conf, $err ? 1 : 0);
490 PVE::Storage::deactivate_volumes($storage_cfg, PVE::LXC::Config->get_vm_volumes($conf));
491 die $err if $err;
492 # set some defaults
493 $conf->{hostname} ||= "CT$vmid";
494 $conf->{memory} ||= 512;
495 $conf->{swap} //= 512;
496 foreach my $mp (keys %$delayed_mp_param) {
497 $conf->{$mp} = $delayed_mp_param->{$mp};
498 }
499 # If the template flag was set, we try to convert again to template after restore
500 if ($was_template) {
501 print STDERR "Convert restored container to template...\n";
502 PVE::LXC::template_create($vmid, $conf);
503 $conf->{template} = 1;
504 }
505 PVE::LXC::Config->write_config($vmid, $conf);
506 };
507 if (my $err = $@) {
508 eval { PVE::LXC::delete_ifaces_ipams_ips($conf, $vmid) };
509 warn $@ if $@;
510 PVE::LXC::destroy_disks($storage_cfg, $vollist);
511 if ($destroy_config_on_error) {
512 eval { PVE::LXC::Config->destroy_config($vmid) };
513 warn $@ if $@;
514
515 if (!$skip_fw_config_restore) { # Only if user has permission to change the fw
516 PVE::Firewall::remove_vmfw_conf($vmid);
517 warn $@ if $@;
518 }
519 }
520 die "$emsg $err";
521 }
522 PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
523 };
524
525 my $workername = $restore ? 'vzrestore' : 'vzcreate';
526 my $realcmd = sub {
527 eval {
528 PVE::LXC::Config->lock_config($vmid, $code);
529 };
530 if (my $err = $@) {
531 # if we aborted before changing the container, we must remove the create lock
532 if (!$destroy_config_on_error) {
533 PVE::LXC::Config->remove_lock($vmid, 'create');
534 }
535 die $err;
536 } elsif ($start_after_create) {
537 PVE::API2::LXC::Status->vm_start({ vmid => $vmid, node => $node });
538 }
539 };
540
541 return $rpcenv->fork_worker($workername, $vmid, $authuser, $realcmd);
542 }});
543
544 __PACKAGE__->register_method({
545 name => 'vmdiridx',
546 path => '{vmid}',
547 method => 'GET',
548 proxyto => 'node',
549 description => "Directory index",
550 permissions => {
551 user => 'all',
552 },
553 parameters => {
554 additionalProperties => 0,
555 properties => {
556 node => get_standard_option('pve-node'),
557 vmid => get_standard_option('pve-vmid'),
558 },
559 },
560 returns => {
561 type => 'array',
562 items => {
563 type => "object",
564 properties => {
565 subdir => { type => 'string' },
566 },
567 },
568 links => [ { rel => 'child', href => "{subdir}" } ],
569 },
570 code => sub {
571 my ($param) = @_;
572
573 # test if VM exists
574 my $conf = PVE::LXC::Config->load_config($param->{vmid});
575
576 my $res = [
577 { subdir => 'config' },
578 { subdir => 'pending' },
579 { subdir => 'status' },
580 { subdir => 'vncproxy' },
581 { subdir => 'termproxy' },
582 { subdir => 'vncwebsocket' },
583 { subdir => 'spiceproxy' },
584 { subdir => 'migrate' },
585 { subdir => 'clone' },
586 # { subdir => 'initlog' },
587 { subdir => 'rrd' },
588 { subdir => 'rrddata' },
589 { subdir => 'firewall' },
590 { subdir => 'snapshot' },
591 { subdir => 'resize' },
592 ];
593
594 return $res;
595 }});
596
597
598 __PACKAGE__->register_method({
599 name => 'rrd',
600 path => '{vmid}/rrd',
601 method => 'GET',
602 protected => 1, # fixme: can we avoid that?
603 permissions => {
604 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
605 },
606 description => "Read VM RRD statistics (returns PNG)",
607 parameters => {
608 additionalProperties => 0,
609 properties => {
610 node => get_standard_option('pve-node'),
611 vmid => get_standard_option('pve-vmid'),
612 timeframe => {
613 description => "Specify the time frame you are interested in.",
614 type => 'string',
615 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
616 },
617 ds => {
618 description => "The list of datasources you want to display.",
619 type => 'string', format => 'pve-configid-list',
620 },
621 cf => {
622 description => "The RRD consolidation function",
623 type => 'string',
624 enum => [ 'AVERAGE', 'MAX' ],
625 optional => 1,
626 },
627 },
628 },
629 returns => {
630 type => "object",
631 properties => {
632 filename => { type => 'string' },
633 },
634 },
635 code => sub {
636 my ($param) = @_;
637
638 return PVE::RRD::create_rrd_graph(
639 "pve2-vm/$param->{vmid}", $param->{timeframe},
640 $param->{ds}, $param->{cf});
641
642 }});
643
644 __PACKAGE__->register_method({
645 name => 'rrddata',
646 path => '{vmid}/rrddata',
647 method => 'GET',
648 protected => 1, # fixme: can we avoid that?
649 permissions => {
650 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
651 },
652 description => "Read VM RRD statistics",
653 parameters => {
654 additionalProperties => 0,
655 properties => {
656 node => get_standard_option('pve-node'),
657 vmid => get_standard_option('pve-vmid'),
658 timeframe => {
659 description => "Specify the time frame you are interested in.",
660 type => 'string',
661 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
662 },
663 cf => {
664 description => "The RRD consolidation function",
665 type => 'string',
666 enum => [ 'AVERAGE', 'MAX' ],
667 optional => 1,
668 },
669 },
670 },
671 returns => {
672 type => "array",
673 items => {
674 type => "object",
675 properties => {},
676 },
677 },
678 code => sub {
679 my ($param) = @_;
680
681 return PVE::RRD::create_rrd_data(
682 "pve2-vm/$param->{vmid}", $param->{timeframe}, $param->{cf});
683 }});
684
685 __PACKAGE__->register_method({
686 name => 'destroy_vm',
687 path => '{vmid}',
688 method => 'DELETE',
689 protected => 1,
690 proxyto => 'node',
691 description => "Destroy the container (also delete all uses files).",
692 permissions => {
693 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
694 },
695 parameters => {
696 additionalProperties => 0,
697 properties => {
698 node => get_standard_option('pve-node'),
699 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid_stopped }),
700 force => {
701 type => 'boolean',
702 description => "Force destroy, even if running.",
703 default => 0,
704 optional => 1,
705 },
706 purge => {
707 type => 'boolean',
708 description => "Remove container from all related configurations."
709 ." For example, backup jobs, replication jobs or HA."
710 ." Related ACLs and Firewall entries will *always* be removed.",
711 default => 0,
712 optional => 1,
713 },
714 'destroy-unreferenced-disks' => {
715 type => 'boolean',
716 description => "If set, destroy additionally all disks with the VMID from all"
717 ." enabled storages which are not referenced in the config.",
718 optional => 1,
719 },
720 },
721 },
722 returns => {
723 type => 'string',
724 },
725 code => sub {
726 my ($param) = @_;
727
728 my $rpcenv = PVE::RPCEnvironment::get();
729 my $authuser = $rpcenv->get_user();
730 my $vmid = $param->{vmid};
731
732 # test if container exists
733
734 my $conf = PVE::LXC::Config->load_config($vmid);
735 my $early_checks = sub {
736 my ($conf) = @_;
737 PVE::LXC::Config->check_protection($conf, "can't remove CT $vmid");
738 PVE::LXC::Config->check_lock($conf);
739
740 my $ha_managed = PVE::HA::Config::service_is_configured("ct:$vmid");
741
742 if (!$param->{purge}) {
743 die "unable to remove CT $vmid - used in HA resources and purge parameter not set.\n"
744 if $ha_managed;
745
746 # do not allow destroy if there are replication jobs without purge
747 my $repl_conf = PVE::ReplicationConfig->new();
748 $repl_conf->check_for_existing_jobs($vmid);
749 }
750
751 return $ha_managed;
752 };
753
754 $early_checks->($conf);
755
756 my $running_error_msg = "unable to destroy CT $vmid - container is running\n";
757 die $running_error_msg if !$param->{force} && PVE::LXC::check_running($vmid); # check early
758
759 my $code = sub {
760 # reload config after lock
761 $conf = PVE::LXC::Config->load_config($vmid);
762 my $ha_managed = $early_checks->($conf);
763
764 if (PVE::LXC::check_running($vmid)) {
765 die $running_error_msg if !$param->{force};
766 warn "forced to stop CT $vmid before destroying!\n";
767 if (!$ha_managed) {
768 PVE::LXC::vm_stop($vmid, 1);
769 } else {
770 run_command(['ha-manager', 'crm-command', 'stop', "ct:$vmid", '120']);
771 }
772 }
773
774 my $storage_cfg = cfs_read_file("storage.cfg");
775 PVE::LXC::destroy_lxc_container(
776 $storage_cfg,
777 $vmid,
778 $conf,
779 { lock => 'destroyed' },
780 $param->{'destroy-unreferenced-disks'},
781 );
782
783 PVE::AccessControl::remove_vm_access($vmid);
784 PVE::Firewall::remove_vmfw_conf($vmid);
785 if ($param->{purge}) {
786 print "purging CT $vmid from related configurations..\n";
787 PVE::ReplicationConfig::remove_vmid_jobs($vmid);
788 PVE::VZDump::Plugin::remove_vmid_from_backup_jobs($vmid);
789
790 if ($ha_managed) {
791 PVE::HA::Config::delete_service_from_config("ct:$vmid");
792 print "NOTE: removed CT $vmid from HA resource configuration.\n";
793 }
794 }
795
796 # only now remove the zombie config, else we can have reuse race
797 PVE::LXC::Config->destroy_config($vmid);
798 };
799
800 my $realcmd = sub { PVE::LXC::Config->lock_config($vmid, $code); };
801
802 return $rpcenv->fork_worker('vzdestroy', $vmid, $authuser, $realcmd);
803 }});
804
805 my $sslcert;
806
807 __PACKAGE__->register_method ({
808 name => 'vncproxy',
809 path => '{vmid}/vncproxy',
810 method => 'POST',
811 protected => 1,
812 permissions => {
813 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
814 },
815 description => "Creates a TCP VNC proxy connections.",
816 parameters => {
817 additionalProperties => 0,
818 properties => {
819 node => get_standard_option('pve-node'),
820 vmid => get_standard_option('pve-vmid'),
821 websocket => {
822 optional => 1,
823 type => 'boolean',
824 description => "use websocket instead of standard VNC.",
825 },
826 width => {
827 optional => 1,
828 description => "sets the width of the console in pixels.",
829 type => 'integer',
830 minimum => 16,
831 maximum => 4096,
832 },
833 height => {
834 optional => 1,
835 description => "sets the height of the console in pixels.",
836 type => 'integer',
837 minimum => 16,
838 maximum => 2160,
839 },
840 },
841 },
842 returns => {
843 additionalProperties => 0,
844 properties => {
845 user => { type => 'string' },
846 ticket => { type => 'string' },
847 cert => { type => 'string' },
848 port => { type => 'integer' },
849 upid => { type => 'string' },
850 },
851 },
852 code => sub {
853 my ($param) = @_;
854
855 my $rpcenv = PVE::RPCEnvironment::get();
856
857 my $authuser = $rpcenv->get_user();
858
859 my $vmid = $param->{vmid};
860 my $node = $param->{node};
861
862 my $authpath = "/vms/$vmid";
863
864 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
865
866 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
867 if !$sslcert;
868
869 my ($remip, $family);
870
871 if ($node ne PVE::INotify::nodename()) {
872 ($remip, $family) = PVE::Cluster::remote_node_ip($node);
873 } else {
874 $family = PVE::Tools::get_host_address_family($node);
875 }
876
877 my $port = PVE::Tools::next_vnc_port($family);
878
879 # NOTE: vncterm VNC traffic is already TLS encrypted,
880 # so we select the fastest chipher here (or 'none'?)
881 my $remcmd = $remip ?
882 ['/usr/bin/ssh', '-e', 'none', '-t', $remip] : [];
883
884 my $conf = PVE::LXC::Config->load_config($vmid, $node);
885 my $concmd = PVE::LXC::get_console_command($vmid, $conf, -1);
886
887 my $shcmd = [ '/usr/bin/dtach', '-A',
888 "/var/run/dtach/vzctlconsole$vmid",
889 '-r', 'winch', '-z', @$concmd];
890
891 my $realcmd = sub {
892 my $upid = shift;
893
894 syslog ('info', "starting lxc vnc proxy $upid\n");
895
896 my $timeout = 10;
897
898 my $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
899 '-timeout', $timeout, '-authpath', $authpath,
900 '-perm', 'VM.Console'];
901
902 if ($param->{width}) {
903 push @$cmd, '-width', $param->{width};
904 }
905
906 if ($param->{height}) {
907 push @$cmd, '-height', $param->{height};
908 }
909
910 if ($param->{websocket}) {
911 $ENV{PVE_VNC_TICKET} = $ticket; # pass ticket to vncterm
912 push @$cmd, '-notls', '-listen', 'localhost';
913 }
914
915 push @$cmd, '-c', @$remcmd, @$shcmd;
916
917 run_command($cmd, keeplocale => 1);
918
919 return;
920 };
921
922 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd);
923
924 PVE::Tools::wait_for_vnc_port($port);
925
926 return {
927 user => $authuser,
928 ticket => $ticket,
929 port => $port,
930 upid => $upid,
931 cert => $sslcert,
932 };
933 }});
934
935 __PACKAGE__->register_method ({
936 name => 'termproxy',
937 path => '{vmid}/termproxy',
938 method => 'POST',
939 protected => 1,
940 permissions => {
941 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
942 },
943 description => "Creates a TCP proxy connection.",
944 parameters => {
945 additionalProperties => 0,
946 properties => {
947 node => get_standard_option('pve-node'),
948 vmid => get_standard_option('pve-vmid'),
949 },
950 },
951 returns => {
952 additionalProperties => 0,
953 properties => {
954 user => { type => 'string' },
955 ticket => { type => 'string' },
956 port => { type => 'integer' },
957 upid => { type => 'string' },
958 },
959 },
960 code => sub {
961 my ($param) = @_;
962
963 my $rpcenv = PVE::RPCEnvironment::get();
964
965 my $authuser = $rpcenv->get_user();
966
967 my $vmid = $param->{vmid};
968 my $node = $param->{node};
969
970 my $authpath = "/vms/$vmid";
971
972 my $ticket = PVE::AccessControl::assemble_vnc_ticket($authuser, $authpath);
973
974 my ($remip, $family);
975
976 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
977 ($remip, $family) = PVE::Cluster::remote_node_ip($node);
978 } else {
979 $family = PVE::Tools::get_host_address_family($node);
980 }
981
982 my $port = PVE::Tools::next_vnc_port($family);
983
984 my $remcmd = $remip ?
985 ['/usr/bin/ssh', '-e', 'none', '-t', $remip, '--'] : [];
986
987 my $conf = PVE::LXC::Config->load_config($vmid, $node);
988 my $concmd = PVE::LXC::get_console_command($vmid, $conf, -1);
989
990 my $shcmd = [ '/usr/bin/dtach', '-A',
991 "/var/run/dtach/vzctlconsole$vmid",
992 '-r', 'winch', '-z', @$concmd];
993
994 my $realcmd = sub {
995 my $upid = shift;
996
997 syslog ('info', "starting lxc termproxy $upid\n");
998
999 my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath,
1000 '--perm', 'VM.Console', '--'];
1001 push @$cmd, @$remcmd, @$shcmd;
1002
1003 PVE::Tools::run_command($cmd);
1004 };
1005
1006 my $upid = $rpcenv->fork_worker('vncproxy', $vmid, $authuser, $realcmd, 1);
1007
1008 PVE::Tools::wait_for_vnc_port($port);
1009
1010 return {
1011 user => $authuser,
1012 ticket => $ticket,
1013 port => $port,
1014 upid => $upid,
1015 };
1016 }});
1017
1018 __PACKAGE__->register_method({
1019 name => 'vncwebsocket',
1020 path => '{vmid}/vncwebsocket',
1021 method => 'GET',
1022 permissions => {
1023 description => "You also need to pass a valid ticket (vncticket).",
1024 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1025 },
1026 description => "Opens a weksocket for VNC traffic.",
1027 parameters => {
1028 additionalProperties => 0,
1029 properties => {
1030 node => get_standard_option('pve-node'),
1031 vmid => get_standard_option('pve-vmid'),
1032 vncticket => {
1033 description => "Ticket from previous call to vncproxy.",
1034 type => 'string',
1035 maxLength => 512,
1036 },
1037 port => {
1038 description => "Port number returned by previous vncproxy call.",
1039 type => 'integer',
1040 minimum => 5900,
1041 maximum => 5999,
1042 },
1043 },
1044 },
1045 returns => {
1046 type => "object",
1047 properties => {
1048 port => { type => 'string' },
1049 },
1050 },
1051 code => sub {
1052 my ($param) = @_;
1053
1054 my $rpcenv = PVE::RPCEnvironment::get();
1055
1056 my $authuser = $rpcenv->get_user();
1057
1058 my $authpath = "/vms/$param->{vmid}";
1059
1060 PVE::AccessControl::verify_vnc_ticket($param->{vncticket}, $authuser, $authpath);
1061
1062 my $port = $param->{port};
1063
1064 return { port => $port };
1065 }});
1066
1067 __PACKAGE__->register_method ({
1068 name => 'spiceproxy',
1069 path => '{vmid}/spiceproxy',
1070 method => 'POST',
1071 protected => 1,
1072 proxyto => 'node',
1073 permissions => {
1074 check => ['perm', '/vms/{vmid}', [ 'VM.Console' ]],
1075 },
1076 description => "Returns a SPICE configuration to connect to the CT.",
1077 parameters => {
1078 additionalProperties => 0,
1079 properties => {
1080 node => get_standard_option('pve-node'),
1081 vmid => get_standard_option('pve-vmid'),
1082 proxy => get_standard_option('spice-proxy', { optional => 1 }),
1083 },
1084 },
1085 returns => get_standard_option('remote-viewer-config'),
1086 code => sub {
1087 my ($param) = @_;
1088
1089 my $vmid = $param->{vmid};
1090 my $node = $param->{node};
1091 my $proxy = $param->{proxy};
1092
1093 my $authpath = "/vms/$vmid";
1094 my $permissions = 'VM.Console';
1095
1096 my $conf = PVE::LXC::Config->load_config($vmid);
1097
1098 die "CT $vmid not running\n" if !PVE::LXC::check_running($vmid);
1099
1100 my $concmd = PVE::LXC::get_console_command($vmid, $conf);
1101
1102 my $shcmd = ['/usr/bin/dtach', '-A',
1103 "/var/run/dtach/vzctlconsole$vmid",
1104 '-r', 'winch', '-z', @$concmd];
1105
1106 my $title = "CT $vmid";
1107
1108 return PVE::API2Tools::run_spiceterm($authpath, $permissions, $vmid, $node, $proxy, $title, $shcmd);
1109 }});
1110
1111
1112 __PACKAGE__->register_method({
1113 name => 'remote_migrate_vm',
1114 path => '{vmid}/remote_migrate',
1115 method => 'POST',
1116 protected => 1,
1117 proxyto => 'node',
1118 description => "Migrate the container to another cluster. Creates a new migration task. EXPERIMENTAL feature!",
1119 permissions => {
1120 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
1121 },
1122 parameters => {
1123 additionalProperties => 0,
1124 properties => {
1125 node => get_standard_option('pve-node'),
1126 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
1127 'target-vmid' => get_standard_option('pve-vmid', { optional => 1 }),
1128 'target-endpoint' => get_standard_option('proxmox-remote', {
1129 description => "Remote target endpoint",
1130 }),
1131 online => {
1132 type => 'boolean',
1133 description => "Use online/live migration.",
1134 optional => 1,
1135 },
1136 restart => {
1137 type => 'boolean',
1138 description => "Use restart migration",
1139 optional => 1,
1140 },
1141 timeout => {
1142 type => 'integer',
1143 description => "Timeout in seconds for shutdown for restart migration",
1144 optional => 1,
1145 default => 180,
1146 },
1147 delete => {
1148 type => 'boolean',
1149 description => "Delete the original CT and related data after successful migration. By default the original CT is kept on the source cluster in a stopped state.",
1150 optional => 1,
1151 default => 0,
1152 },
1153 'target-storage' => get_standard_option('pve-targetstorage', {
1154 optional => 0,
1155 }),
1156 'target-bridge' => {
1157 type => 'string',
1158 description => "Mapping from source to target bridges. Providing only a single bridge ID maps all source bridges to that bridge. Providing the special value '1' will map each source bridge to itself.",
1159 format => 'bridge-pair-list',
1160 },
1161 bwlimit => {
1162 description => "Override I/O bandwidth limit (in KiB/s).",
1163 optional => 1,
1164 type => 'number',
1165 minimum => '0',
1166 default => 'migrate limit from datacenter or storage config',
1167 },
1168 },
1169 },
1170 returns => {
1171 type => 'string',
1172 description => "the task ID.",
1173 },
1174 code => sub {
1175 my ($param) = @_;
1176
1177 my $rpcenv = PVE::RPCEnvironment::get();
1178 my $authuser = $rpcenv->get_user();
1179
1180 my $source_vmid = extract_param($param, 'vmid');
1181 my $target_endpoint = extract_param($param, 'target-endpoint');
1182 my $target_vmid = extract_param($param, 'target-vmid') // $source_vmid;
1183
1184 my $delete = extract_param($param, 'delete') // 0;
1185
1186 PVE::Cluster::check_cfs_quorum();
1187
1188 # test if CT exists
1189 my $conf = PVE::LXC::Config->load_config($source_vmid);
1190 PVE::LXC::Config->check_lock($conf);
1191
1192 # try to detect errors early
1193 if (PVE::LXC::check_running($source_vmid)) {
1194 die "can't migrate running container without --online or --restart\n"
1195 if !$param->{online} && !$param->{restart};
1196 }
1197
1198 raise_param_exc({ vmid => "cannot migrate HA-managed CT to remote cluster" })
1199 if PVE::HA::Config::vm_is_ha_managed($source_vmid);
1200
1201 my $remote = PVE::JSONSchema::parse_property_string('proxmox-remote', $target_endpoint);
1202
1203 # TODO: move this as helper somewhere appropriate?
1204 my $conn_args = {
1205 protocol => 'https',
1206 host => $remote->{host},
1207 port => $remote->{port} // 8006,
1208 apitoken => $remote->{apitoken},
1209 };
1210
1211 my $fp;
1212 if ($fp = $remote->{fingerprint}) {
1213 $conn_args->{cached_fingerprints} = { uc($fp) => 1 };
1214 }
1215
1216 print "Establishing API connection with remote at '$remote->{host}'\n";
1217
1218 my $api_client = PVE::APIClient::LWP->new(%$conn_args);
1219
1220 if (!defined($fp)) {
1221 my $cert_info = $api_client->get("/nodes/localhost/certificates/info");
1222 foreach my $cert (@$cert_info) {
1223 my $filename = $cert->{filename};
1224 next if $filename ne 'pveproxy-ssl.pem' && $filename ne 'pve-ssl.pem';
1225 $fp = $cert->{fingerprint} if !$fp || $filename eq 'pveproxy-ssl.pem';
1226 }
1227 $conn_args->{cached_fingerprints} = { uc($fp) => 1 }
1228 if defined($fp);
1229 }
1230
1231 my $storecfg = PVE::Storage::config();
1232 my $target_storage = extract_param($param, 'target-storage');
1233 my $storagemap = eval { PVE::JSONSchema::parse_idmap($target_storage, 'pve-storage-id') };
1234 raise_param_exc({ 'target-storage' => "failed to parse storage map: $@" })
1235 if $@;
1236
1237 my $target_bridge = extract_param($param, 'target-bridge');
1238 my $bridgemap = eval { PVE::JSONSchema::parse_idmap($target_bridge, 'pve-bridge-id') };
1239 raise_param_exc({ 'target-bridge' => "failed to parse bridge map: $@" })
1240 if $@;
1241
1242 die "remote migration requires explicit storage mapping!\n"
1243 if $storagemap->{identity};
1244
1245 $param->{storagemap} = $storagemap;
1246 $param->{bridgemap} = $bridgemap;
1247 $param->{remote} = {
1248 conn => $conn_args, # re-use fingerprint for tunnel
1249 client => $api_client,
1250 vmid => $target_vmid,
1251 };
1252 $param->{migration_type} = 'websocket';
1253 $param->{delete} = $delete if $delete;
1254
1255 my $cluster_status = $api_client->get("/cluster/status");
1256 my $target_node;
1257 foreach my $entry (@$cluster_status) {
1258 next if $entry->{type} ne 'node';
1259 if ($entry->{local}) {
1260 $target_node = $entry->{name};
1261 last;
1262 }
1263 }
1264
1265 die "couldn't determine endpoint's node name\n"
1266 if !defined($target_node);
1267
1268 my $realcmd = sub {
1269 PVE::LXC::Migrate->migrate($target_node, $remote->{host}, $source_vmid, $param);
1270 };
1271
1272 my $worker = sub {
1273 return PVE::GuestHelpers::guest_migration_lock($source_vmid, 10, $realcmd);
1274 };
1275
1276 return $rpcenv->fork_worker('vzmigrate', $source_vmid, $authuser, $worker);
1277 }});
1278
1279
1280 __PACKAGE__->register_method({
1281 name => 'migrate_vm',
1282 path => '{vmid}/migrate',
1283 method => 'POST',
1284 protected => 1,
1285 proxyto => 'node',
1286 description => "Migrate the container to another node. Creates a new migration task.",
1287 permissions => {
1288 check => ['perm', '/vms/{vmid}', [ 'VM.Migrate' ]],
1289 },
1290 parameters => {
1291 additionalProperties => 0,
1292 properties => {
1293 node => get_standard_option('pve-node'),
1294 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
1295 target => get_standard_option('pve-node', {
1296 description => "Target node.",
1297 completion => \&PVE::Cluster::complete_migration_target,
1298 }),
1299 'target-storage' => get_standard_option('pve-targetstorage'),
1300 online => {
1301 type => 'boolean',
1302 description => "Use online/live migration.",
1303 optional => 1,
1304 },
1305 restart => {
1306 type => 'boolean',
1307 description => "Use restart migration",
1308 optional => 1,
1309 },
1310 timeout => {
1311 type => 'integer',
1312 description => "Timeout in seconds for shutdown for restart migration",
1313 optional => 1,
1314 default => 180,
1315 },
1316 bwlimit => {
1317 description => "Override I/O bandwidth limit (in KiB/s).",
1318 optional => 1,
1319 type => 'number',
1320 minimum => '0',
1321 default => 'migrate limit from datacenter or storage config',
1322 },
1323 },
1324 },
1325 returns => {
1326 type => 'string',
1327 description => "the task ID.",
1328 },
1329 code => sub {
1330 my ($param) = @_;
1331
1332 my $rpcenv = PVE::RPCEnvironment::get();
1333
1334 my $authuser = $rpcenv->get_user();
1335
1336 my $target = extract_param($param, 'target');
1337
1338 my $localnode = PVE::INotify::nodename();
1339 raise_param_exc({ target => "target is local node."}) if $target eq $localnode;
1340
1341 PVE::Cluster::check_cfs_quorum();
1342
1343 PVE::Cluster::check_node_exists($target);
1344
1345 my $targetip = PVE::Cluster::remote_node_ip($target);
1346
1347 my $vmid = extract_param($param, 'vmid');
1348
1349 # test if VM exists
1350 PVE::LXC::Config->load_config($vmid);
1351
1352 # try to detect errors early
1353 if (PVE::LXC::check_running($vmid)) {
1354 die "can't migrate running container without --online or --restart\n"
1355 if !$param->{online} && !$param->{restart};
1356 }
1357
1358 if (my $targetstorage = delete $param->{'target-storage'}) {
1359 my $storecfg = PVE::Storage::config();
1360 my $storagemap = eval { PVE::JSONSchema::parse_idmap($targetstorage, 'pve-storage-id') };
1361 raise_param_exc({ 'target-storage' => "failed to parse storage map: $@" })
1362 if $@;
1363
1364 $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Disk'])
1365 if !defined($storagemap->{identity});
1366
1367 foreach my $target_sid (values %{$storagemap->{entries}}) {
1368 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $target_sid, $target);
1369 }
1370
1371 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storagemap->{default}, $target)
1372 if $storagemap->{default};
1373
1374 $param->{storagemap} = $storagemap;
1375 }
1376
1377 if (PVE::HA::Config::vm_is_ha_managed($vmid) && $rpcenv->{type} ne 'ha') {
1378
1379 my $hacmd = sub {
1380 my $upid = shift;
1381
1382 my $service = "ct:$vmid";
1383
1384 my $cmd = ['ha-manager', 'migrate', $service, $target];
1385
1386 print "Requesting HA migration for CT $vmid to node $target\n";
1387
1388 PVE::Tools::run_command($cmd);
1389
1390 return;
1391 };
1392
1393 return $rpcenv->fork_worker('hamigrate', $vmid, $authuser, $hacmd);
1394
1395 } else {
1396
1397 my $realcmd = sub {
1398 PVE::LXC::Migrate->migrate($target, $targetip, $vmid, $param);
1399 };
1400
1401 my $worker = sub {
1402 return PVE::GuestHelpers::guest_migration_lock($vmid, 10, $realcmd);
1403 };
1404
1405 return $rpcenv->fork_worker('vzmigrate', $vmid, $authuser, $worker);
1406 }
1407 }});
1408
1409 __PACKAGE__->register_method({
1410 name => 'vm_feature',
1411 path => '{vmid}/feature',
1412 method => 'GET',
1413 proxyto => 'node',
1414 protected => 1,
1415 description => "Check if feature for virtual machine is available.",
1416 permissions => {
1417 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
1418 },
1419 parameters => {
1420 additionalProperties => 0,
1421 properties => {
1422 node => get_standard_option('pve-node'),
1423 vmid => get_standard_option('pve-vmid'),
1424 feature => {
1425 description => "Feature to check.",
1426 type => 'string',
1427 enum => [ 'snapshot', 'clone', 'copy' ],
1428 },
1429 snapname => get_standard_option('pve-snapshot-name', {
1430 optional => 1,
1431 }),
1432 },
1433 },
1434 returns => {
1435 type => "object",
1436 properties => {
1437 hasFeature => { type => 'boolean' },
1438 #nodes => {
1439 #type => 'array',
1440 #items => { type => 'string' },
1441 #}
1442 },
1443 },
1444 code => sub {
1445 my ($param) = @_;
1446
1447 my $node = extract_param($param, 'node');
1448
1449 my $vmid = extract_param($param, 'vmid');
1450
1451 my $snapname = extract_param($param, 'snapname');
1452
1453 my $feature = extract_param($param, 'feature');
1454
1455 my $conf = PVE::LXC::Config->load_config($vmid);
1456
1457 if($snapname){
1458 my $snap = $conf->{snapshots}->{$snapname};
1459 die "snapshot '$snapname' does not exist\n" if !defined($snap);
1460 $conf = $snap;
1461 }
1462 my $storage_cfg = PVE::Storage::config();
1463 #Maybe include later
1464 #my $nodelist = PVE::LXC::shared_nodes($conf, $storage_cfg);
1465 my $hasFeature = PVE::LXC::Config->has_feature($feature, $conf, $storage_cfg, $snapname);
1466
1467 return {
1468 hasFeature => $hasFeature,
1469 #nodes => [ keys %$nodelist ],
1470 };
1471 }});
1472
1473 __PACKAGE__->register_method({
1474 name => 'template',
1475 path => '{vmid}/template',
1476 method => 'POST',
1477 protected => 1,
1478 proxyto => 'node',
1479 description => "Create a Template.",
1480 permissions => {
1481 description => "You need 'VM.Allocate' permissions on /vms/{vmid}",
1482 check => [ 'perm', '/vms/{vmid}', ['VM.Allocate']],
1483 },
1484 parameters => {
1485 additionalProperties => 0,
1486 properties => {
1487 node => get_standard_option('pve-node'),
1488 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid_stopped }),
1489 },
1490 },
1491 returns => { type => 'null'},
1492 code => sub {
1493 my ($param) = @_;
1494
1495 my $rpcenv = PVE::RPCEnvironment::get();
1496
1497 my $authuser = $rpcenv->get_user();
1498
1499 my $node = extract_param($param, 'node');
1500
1501 my $vmid = extract_param($param, 'vmid');
1502
1503 my $updatefn = sub {
1504
1505 my $conf = PVE::LXC::Config->load_config($vmid);
1506 PVE::LXC::Config->check_lock($conf);
1507
1508 die "unable to create template, because CT contains snapshots\n"
1509 if $conf->{snapshots} && scalar(keys %{$conf->{snapshots}});
1510
1511 die "you can't convert a template to a template\n"
1512 if PVE::LXC::Config->is_template($conf);
1513
1514 die "you can't convert a CT to template if the CT is running\n"
1515 if PVE::LXC::check_running($vmid);
1516
1517 my $realcmd = sub {
1518 PVE::LXC::template_create($vmid, $conf);
1519
1520 $conf->{template} = 1;
1521
1522 PVE::LXC::Config->write_config($vmid, $conf);
1523 # and remove lxc config
1524 PVE::LXC::update_lxc_config($vmid, $conf);
1525 };
1526
1527 return $rpcenv->fork_worker('vztemplate', $vmid, $authuser, $realcmd);
1528 };
1529
1530 PVE::LXC::Config->lock_config($vmid, $updatefn);
1531
1532 return undef;
1533 }});
1534
1535 __PACKAGE__->register_method({
1536 name => 'clone_vm',
1537 path => '{vmid}/clone',
1538 method => 'POST',
1539 protected => 1,
1540 proxyto => 'node',
1541 description => "Create a container clone/copy",
1542 permissions => {
1543 description => "You need 'VM.Clone' permissions on /vms/{vmid}, " .
1544 "and 'VM.Allocate' permissions " .
1545 "on /vms/{newid} (or on the VM pool /pool/{pool}). You also need " .
1546 "'Datastore.AllocateSpace' on any used storage, and 'SDN.Use' on any bridge.",
1547 check =>
1548 [ 'and',
1549 ['perm', '/vms/{vmid}', [ 'VM.Clone' ]],
1550 [ 'or',
1551 [ 'perm', '/vms/{newid}', ['VM.Allocate']],
1552 [ 'perm', '/pool/{pool}', ['VM.Allocate'], require_param => 'pool'],
1553 ],
1554 ]
1555 },
1556 parameters => {
1557 additionalProperties => 0,
1558 properties => {
1559 node => get_standard_option('pve-node'),
1560 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
1561 newid => get_standard_option('pve-vmid', {
1562 completion => \&PVE::Cluster::complete_next_vmid,
1563 description => 'VMID for the clone.' }),
1564 hostname => {
1565 optional => 1,
1566 type => 'string', format => 'dns-name',
1567 description => "Set a hostname for the new CT.",
1568 },
1569 description => {
1570 optional => 1,
1571 type => 'string',
1572 description => "Description for the new CT.",
1573 },
1574 pool => {
1575 optional => 1,
1576 type => 'string', format => 'pve-poolid',
1577 description => "Add the new CT to the specified pool.",
1578 },
1579 snapname => get_standard_option('pve-snapshot-name', {
1580 optional => 1,
1581 }),
1582 storage => get_standard_option('pve-storage-id', {
1583 description => "Target storage for full clone.",
1584 optional => 1,
1585 }),
1586 full => {
1587 optional => 1,
1588 type => 'boolean',
1589 description => "Create a full copy of all disks. This is always done when " .
1590 "you clone a normal CT. For CT templates, we try to create a linked clone by default.",
1591 },
1592 target => get_standard_option('pve-node', {
1593 description => "Target node. Only allowed if the original VM is on shared storage.",
1594 optional => 1,
1595 }),
1596 bwlimit => {
1597 description => "Override I/O bandwidth limit (in KiB/s).",
1598 optional => 1,
1599 type => 'number',
1600 minimum => '0',
1601 default => 'clone limit from datacenter or storage config',
1602 },
1603 },
1604 },
1605 returns => {
1606 type => 'string',
1607 },
1608 code => sub {
1609 my ($param) = @_;
1610
1611 my $rpcenv = PVE::RPCEnvironment::get();
1612 my $authuser = $rpcenv->get_user();
1613
1614 my $node = extract_param($param, 'node');
1615 my $vmid = extract_param($param, 'vmid');
1616 my $newid = extract_param($param, 'newid');
1617 my $pool = extract_param($param, 'pool');
1618 if (defined($pool)) {
1619 $rpcenv->check_pool_exist($pool);
1620 }
1621 my $snapname = extract_param($param, 'snapname');
1622 my $storage = extract_param($param, 'storage');
1623 my $target = extract_param($param, 'target');
1624 my $localnode = PVE::INotify::nodename();
1625
1626 undef $target if $target && ($target eq $localnode || $target eq 'localhost');
1627
1628 PVE::Cluster::check_node_exists($target) if $target;
1629
1630 my $storecfg = PVE::Storage::config();
1631
1632 if ($storage) {
1633 # check if storage is enabled on local node
1634 PVE::Storage::storage_check_enabled($storecfg, $storage);
1635 if ($target) {
1636 # check if storage is available on target node
1637 PVE::Storage::storage_check_enabled($storecfg, $storage, $target);
1638 # clone only works if target storage is shared
1639 my $scfg = PVE::Storage::storage_config($storecfg, $storage);
1640 die "can't clone to non-shared storage '$storage'\n" if !$scfg->{shared};
1641 }
1642 }
1643
1644 PVE::Cluster::check_cfs_quorum();
1645
1646 my $newconf = {};
1647 my $mountpoints = {};
1648 my $fullclone = {};
1649 my $vollist = [];
1650 my $running;
1651
1652 my $lock_and_reload = sub {
1653 my ($vmid, $code) = @_;
1654 return PVE::LXC::Config->lock_config($vmid, sub {
1655 my $conf = PVE::LXC::Config->load_config($vmid);
1656 die "Lost 'create' config lock, aborting.\n"
1657 if !PVE::LXC::Config->has_lock($conf, 'create');
1658
1659 return $code->($conf);
1660 });
1661 };
1662
1663 my $src_conf = PVE::LXC::Config->set_lock($vmid, 'disk');
1664
1665 eval {
1666 PVE::LXC::Config->create_and_lock_config($newid, 0);
1667 };
1668 if (my $err = $@) {
1669 eval { PVE::LXC::Config->remove_lock($vmid, 'disk') };
1670 warn "Failed to remove source CT config lock - $@\n" if $@;
1671
1672 die $err;
1673 }
1674
1675 eval {
1676 $running = PVE::LXC::check_running($vmid) || 0;
1677
1678 my $full = extract_param($param, 'full');
1679 if (!defined($full)) {
1680 $full = !PVE::LXC::Config->is_template($src_conf);
1681 }
1682
1683 PVE::Firewall::clone_vmfw_conf($vmid, $newid);
1684
1685 die "parameter 'storage' not allowed for linked clones\n"
1686 if defined($storage) && !$full;
1687
1688 die "snapshot '$snapname' does not exist\n"
1689 if $snapname && !defined($src_conf->{snapshots}->{$snapname});
1690
1691 my $src_conf = $snapname ? $src_conf->{snapshots}->{$snapname} : $src_conf;
1692
1693 my $sharedvm = 1;
1694 for my $opt (sort keys %$src_conf) {
1695 next if $opt =~ m/^unused\d+$/;
1696
1697 my $value = $src_conf->{$opt};
1698
1699 if (($opt eq 'rootfs') || ($opt =~ m/^mp\d+$/)) {
1700 my $mp = PVE::LXC::Config->parse_volume($opt, $value);
1701
1702 if ($mp->{type} eq 'volume') {
1703 my $volid = $mp->{volume};
1704
1705 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid);
1706 $sid = $storage if defined($storage);
1707 my $scfg = PVE::Storage::storage_config($storecfg, $sid);
1708 if (!$scfg->{shared}) {
1709 $sharedvm = 0;
1710 warn "found non-shared volume: $volid\n" if $target;
1711 }
1712
1713 $rpcenv->check($authuser, "/storage/$sid", ['Datastore.AllocateSpace']);
1714
1715 if ($full) {
1716 die "Cannot do full clones on a running container without snapshots\n"
1717 if $running && !defined($snapname);
1718 $fullclone->{$opt} = 1;
1719 } else {
1720 # not full means clone instead of copy
1721 die "Linked clone feature for '$volid' is not available\n"
1722 if !PVE::Storage::volume_has_feature($storecfg, 'clone', $volid, $snapname, $running, {'valid_target_formats' => ['raw', 'subvol']});
1723 }
1724
1725 $mountpoints->{$opt} = $mp;
1726 push @$vollist, $volid;
1727
1728 } else {
1729 # TODO: allow bind mounts?
1730 die "unable to clone mountpoint '$opt' (type $mp->{type})\n";
1731 }
1732 } elsif ($opt =~ m/^net(\d+)$/) {
1733 # always change MAC! address
1734 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
1735 my $net = PVE::LXC::Config->parse_lxc_network($value);
1736 $net->{hwaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
1737 $newconf->{$opt} = PVE::LXC::Config->print_lxc_network($net);
1738
1739 PVE::LXC::check_bridge_access($rpcenv, $authuser, $newconf->{$opt});
1740 } else {
1741 # copy everything else
1742 $newconf->{$opt} = $value;
1743 }
1744 }
1745 die "can't clone CT to node '$target' (CT uses local storage)\n"
1746 if $target && !$sharedvm;
1747
1748 # Replace the 'disk' lock with a 'create' lock.
1749 $newconf->{lock} = 'create';
1750
1751 # delete all snapshot related config options
1752 delete $newconf->@{qw(snapshots parent snaptime snapstate)};
1753
1754 delete $newconf->{pending};
1755 delete $newconf->{template};
1756
1757 $newconf->{hostname} = $param->{hostname} if $param->{hostname};
1758 $newconf->{description} = $param->{description} if $param->{description};
1759
1760 $lock_and_reload->($newid, sub {
1761 PVE::LXC::Config->write_config($newid, $newconf);
1762 });
1763 };
1764 if (my $err = $@) {
1765 eval { PVE::LXC::Config->remove_lock($vmid, 'disk') };
1766 warn "Failed to remove source CT config lock - $@\n" if $@;
1767
1768 eval {
1769 $lock_and_reload->($newid, sub {
1770 PVE::LXC::Config->destroy_config($newid);
1771 PVE::Firewall::remove_vmfw_conf($newid);
1772 });
1773 };
1774 warn "Failed to remove target CT config - $@\n" if $@;
1775
1776 die $err;
1777 }
1778
1779 my $update_conf = sub {
1780 my ($key, $value) = @_;
1781 return $lock_and_reload->($newid, sub {
1782 my $conf = shift;
1783 $conf->{$key} = $value;
1784 PVE::LXC::Config->write_config($newid, $conf);
1785 });
1786 };
1787
1788 my $realcmd = sub {
1789 my ($upid) = @_;
1790
1791 my $newvollist = [];
1792
1793 my $verify_running = PVE::LXC::check_running($vmid) || 0;
1794 die "unexpected state change\n" if $verify_running != $running;
1795
1796 eval {
1797 local $SIG{INT} =
1798 local $SIG{TERM} =
1799 local $SIG{QUIT} =
1800 local $SIG{HUP} = sub { die "interrupted by signal\n"; };
1801
1802 PVE::Storage::activate_volumes($storecfg, $vollist, $snapname);
1803 my $bwlimit = extract_param($param, 'bwlimit');
1804
1805 foreach my $opt (keys %$mountpoints) {
1806 my $mp = $mountpoints->{$opt};
1807 my $volid = $mp->{volume};
1808
1809 my $newvolid;
1810 if ($fullclone->{$opt}) {
1811 print "create full clone of mountpoint $opt ($volid)\n";
1812 my $source_storage = PVE::Storage::parse_volume_id($volid);
1813 my $target_storage = $storage // $source_storage;
1814 my $clonelimit = PVE::Storage::get_bandwidth_limit('clone', [$source_storage, $target_storage], $bwlimit);
1815 $newvolid = PVE::LXC::copy_volume($mp, $newid, $target_storage, $storecfg, $newconf, $snapname, $clonelimit);
1816 } else {
1817 print "create linked clone of mount point $opt ($volid)\n";
1818 $newvolid = PVE::Storage::vdisk_clone($storecfg, $volid, $newid, $snapname);
1819 }
1820
1821 push @$newvollist, $newvolid;
1822 $mp->{volume} = $newvolid;
1823
1824 $update_conf->($opt, PVE::LXC::Config->print_ct_mountpoint($mp, $opt eq 'rootfs'));
1825 }
1826
1827 PVE::AccessControl::add_vm_to_pool($newid, $pool) if $pool;
1828
1829 $lock_and_reload->($newid, sub {
1830 my $conf = shift;
1831 my $rootdir = PVE::LXC::mount_all($newid, $storecfg, $conf, 1);
1832
1833 eval {
1834 PVE::LXC::create_ifaces_ipams_ips($conf, $vmid);
1835 my $lxc_setup = PVE::LXC::Setup->new($conf, $rootdir);
1836 $lxc_setup->post_clone_hook($conf);
1837 };
1838 my $err = $@;
1839 eval { PVE::LXC::umount_all($newid, $storecfg, $conf, 1); };
1840 if ($err) {
1841 warn "$@\n" if $@;
1842 die $err;
1843 } else {
1844 die $@ if $@;
1845 }
1846 });
1847 };
1848 my $err = $@;
1849 # Unlock the source config in any case:
1850 eval { PVE::LXC::Config->remove_lock($vmid, 'disk') };
1851 warn $@ if $@;
1852
1853 if ($err) {
1854 # Now cleanup the config & disks & ipam:
1855 sleep 1; # some storages like rbd need to wait before release volume - really?
1856
1857 foreach my $volid (@$newvollist) {
1858 eval { PVE::Storage::vdisk_free($storecfg, $volid); };
1859 warn $@ if $@;
1860 }
1861
1862 eval {
1863 $lock_and_reload->($newid, sub {
1864 my $conf = shift;
1865 PVE::LXC::delete_ifaces_ipams_ips($conf, $newid);
1866 PVE::LXC::Config->destroy_config($newid);
1867 PVE::Firewall::remove_vmfw_conf($newid);
1868 });
1869 };
1870 warn "Failed to remove target CT config - $@\n" if $@;
1871
1872 die "clone failed: $err";
1873 }
1874
1875 $lock_and_reload->($newid, sub {
1876 PVE::LXC::Config->remove_lock($newid, 'create');
1877
1878 if ($target) {
1879 # always deactivate volumes - avoid lvm LVs to be active on several nodes
1880 PVE::Storage::deactivate_volumes($storecfg, $vollist, $snapname) if !$running;
1881 PVE::Storage::deactivate_volumes($storecfg, $newvollist);
1882
1883 PVE::LXC::Config->move_config_to_node($newid, $target);
1884 }
1885 });
1886
1887 return;
1888 };
1889
1890 return $rpcenv->fork_worker('vzclone', $vmid, $authuser, $realcmd);
1891 }});
1892
1893
1894 __PACKAGE__->register_method({
1895 name => 'resize_vm',
1896 path => '{vmid}/resize',
1897 method => 'PUT',
1898 protected => 1,
1899 proxyto => 'node',
1900 description => "Resize a container mount point.",
1901 permissions => {
1902 check => ['perm', '/vms/{vmid}', ['VM.Config.Disk'], any => 1],
1903 },
1904 parameters => {
1905 additionalProperties => 0,
1906 properties => {
1907 node => get_standard_option('pve-node'),
1908 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
1909 disk => {
1910 type => 'string',
1911 description => "The disk you want to resize.",
1912 enum => [PVE::LXC::Config->valid_volume_keys()],
1913 },
1914 size => {
1915 type => 'string',
1916 pattern => '\+?\d+(\.\d+)?[KMGT]?',
1917 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.",
1918 },
1919 digest => {
1920 type => 'string',
1921 description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
1922 maxLength => 40,
1923 optional => 1,
1924 }
1925 },
1926 },
1927 returns => {
1928 type => 'string',
1929 description => "the task ID.",
1930 },
1931 code => sub {
1932 my ($param) = @_;
1933
1934 my $rpcenv = PVE::RPCEnvironment::get();
1935
1936 my $authuser = $rpcenv->get_user();
1937
1938 my $node = extract_param($param, 'node');
1939
1940 my $vmid = extract_param($param, 'vmid');
1941
1942 my $digest = extract_param($param, 'digest');
1943
1944 my $sizestr = extract_param($param, 'size');
1945 my $ext = ($sizestr =~ s/^\+//);
1946 my $request_size = PVE::JSONSchema::parse_size($sizestr);
1947 die "invalid size string" if !defined($request_size);
1948
1949 die "no options specified\n" if !scalar(keys %$param);
1950
1951 my $storage_cfg = cfs_read_file("storage.cfg");
1952
1953 my $load_and_check = sub {
1954 my $conf = PVE::LXC::Config->load_config($vmid);
1955 PVE::LXC::Config->check_lock($conf);
1956
1957 PVE::LXC::check_ct_modify_config_perm($rpcenv, $authuser, $vmid, undef, $conf, $param, [], $conf->{unprivileged});
1958
1959 PVE::Tools::assert_if_modified($digest, $conf->{digest});
1960
1961 my $disk = $param->{disk};
1962 my $mp = PVE::LXC::Config->parse_volume($disk, $conf->{$disk});
1963
1964 my $volid = $mp->{volume};
1965
1966 my (undef, undef, $owner, undef, undef, undef, $format) =
1967 PVE::Storage::parse_volname($storage_cfg, $volid);
1968
1969 die "can't resize mount point owned by another container ($owner)"
1970 if $vmid != $owner;
1971
1972 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
1973
1974 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
1975
1976 PVE::Storage::activate_volumes($storage_cfg, [$volid]);
1977
1978 my $size = PVE::Storage::volume_size_info($storage_cfg, $volid, 5);
1979
1980 die "Could not determine current size of volume '$volid'\n" if !defined($size);
1981
1982 my $newsize = $ext ? $size + $request_size : $request_size;
1983 $newsize = int($newsize);
1984
1985 die "unable to shrink disk size\n" if $newsize < $size;
1986
1987 die "disk is already at specified size\n" if $size == $newsize;
1988
1989 return ($conf, $disk, $mp, $volid, $format, $newsize);
1990 };
1991
1992 my $code = sub {
1993 my ($conf, $disk, $mp, $volid, $format, $newsize) = $load_and_check->();
1994
1995 my $running = PVE::LXC::check_running($vmid);
1996
1997 PVE::Cluster::log_msg('info', $authuser, "update CT $vmid: resize --disk $disk --size $sizestr");
1998
1999 # Note: PVE::Storage::volume_resize doesn't do anything if $running=1, so
2000 # we pass 0 here (parameter only makes sense for qemu)
2001 PVE::Storage::volume_resize($storage_cfg, $volid, $newsize, 0);
2002
2003 $mp->{size} = $newsize;
2004 $conf->{$disk} = PVE::LXC::Config->print_ct_mountpoint($mp, $disk eq 'rootfs');
2005
2006 PVE::LXC::Config->write_config($vmid, $conf);
2007
2008 if ($format eq 'raw') {
2009 # we need to ensure that the volume is mapped, if not needed this is a NOP
2010 my $path = PVE::Storage::map_volume($storage_cfg, $volid);
2011 $path = PVE::Storage::path($storage_cfg, $volid) if !defined($path);
2012 if ($running) {
2013
2014 $mp->{mp} = '/';
2015 my $use_loopdev = (PVE::LXC::mountpoint_mount_path($mp, $storage_cfg))[1];
2016 $path = PVE::LXC::query_loopdev($path) if $use_loopdev;
2017 die "internal error: CT running but mount point not attached to a loop device"
2018 if !$path;
2019 PVE::Tools::run_command(['losetup', '--set-capacity', $path]) if $use_loopdev;
2020
2021 # In order for resize2fs to know that we need online-resizing a mountpoint needs
2022 # to be visible to it in its namespace.
2023 # To not interfere with the rest of the system we unshare the current mount namespace,
2024 # mount over /tmp and then run resize2fs.
2025
2026 # interestingly we don't need to e2fsck on mounted systems...
2027 my $quoted = PVE::Tools::shellquote($path);
2028 my $cmd = "mount --make-rprivate / && mount $quoted /tmp && resize2fs $quoted";
2029 eval {
2030 PVE::Tools::run_command(['unshare', '-m', '--', 'sh', '-c', $cmd]);
2031 };
2032 warn "Failed to update the container's filesystem: $@\n" if $@;
2033 } else {
2034 eval {
2035 PVE::Tools::run_command(['e2fsck', '-f', '-y', $path]);
2036 PVE::Tools::run_command(['resize2fs', $path]);
2037 };
2038 warn "Failed to update the container's filesystem: $@\n" if $@;
2039
2040 # always un-map if not running, this is a NOP if not needed
2041 PVE::Storage::unmap_volume($storage_cfg, $volid);
2042 }
2043 }
2044 };
2045
2046 my $worker = sub {
2047 PVE::LXC::Config->lock_config($vmid, $code);;
2048 };
2049
2050 $load_and_check->(); # early checks before forking+locking
2051
2052 return $rpcenv->fork_worker('resize', $vmid, $authuser, $worker);
2053 }});
2054
2055 __PACKAGE__->register_method({
2056 name => 'move_volume',
2057 path => '{vmid}/move_volume',
2058 method => 'POST',
2059 protected => 1,
2060 proxyto => 'node',
2061 description => "Move a rootfs-/mp-volume to a different storage or to a different container.",
2062 permissions => {
2063 description => "You need 'VM.Config.Disk' permissions on /vms/{vmid}, " .
2064 "and 'Datastore.AllocateSpace' permissions on the storage. To move ".
2065 "a volume to another container, you need the permissions on the ".
2066 "target container as well.",
2067 check => ['perm', '/vms/{vmid}', [ 'VM.Config.Disk' ]],
2068 },
2069 parameters => {
2070 additionalProperties => 0,
2071 properties => {
2072 node => get_standard_option('pve-node'),
2073 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
2074 'target-vmid' => get_standard_option('pve-vmid', {
2075 completion => \&PVE::LXC::complete_ctid,
2076 optional => 1,
2077 }),
2078 volume => {
2079 type => 'string',
2080 #TODO: check how to handle unused mount points as the mp parameter is not configured
2081 enum => [ PVE::LXC::Config->valid_volume_keys_with_unused() ],
2082 description => "Volume which will be moved.",
2083 },
2084 storage => get_standard_option('pve-storage-id', {
2085 description => "Target Storage.",
2086 completion => \&PVE::Storage::complete_storage_enabled,
2087 optional => 1,
2088 }),
2089 delete => {
2090 type => 'boolean',
2091 description => "Delete the original volume after successful copy. By default the " .
2092 "original is kept as an unused volume entry.",
2093 optional => 1,
2094 default => 0,
2095 },
2096 digest => {
2097 type => 'string',
2098 description => 'Prevent changes if current configuration file has different SHA1 " .
2099 "digest. This can be used to prevent concurrent modifications.',
2100 maxLength => 40,
2101 optional => 1,
2102 },
2103 bwlimit => {
2104 description => "Override I/O bandwidth limit (in KiB/s).",
2105 optional => 1,
2106 type => 'number',
2107 minimum => '0',
2108 default => 'clone limit from datacenter or storage config',
2109 },
2110 'target-volume' => {
2111 type => 'string',
2112 description => "The config key the volume will be moved to. Default is the " .
2113 "source volume key.",
2114 enum => [PVE::LXC::Config->valid_volume_keys_with_unused()],
2115 optional => 1,
2116 },
2117 'target-digest' => {
2118 type => 'string',
2119 description => 'Prevent changes if current configuration file of the target " .
2120 "container has a different SHA1 digest. This can be used to prevent " .
2121 "concurrent modifications.',
2122 maxLength => 40,
2123 optional => 1,
2124 },
2125 },
2126 },
2127 returns => {
2128 type => 'string',
2129 },
2130 code => sub {
2131 my ($param) = @_;
2132
2133 my $rpcenv = PVE::RPCEnvironment::get();
2134
2135 my $authuser = $rpcenv->get_user();
2136
2137 my $vmid = extract_param($param, 'vmid');
2138
2139 my $target_vmid = extract_param($param, 'target-vmid');
2140
2141 my $storage = extract_param($param, 'storage');
2142
2143 my $mpkey = extract_param($param, 'volume');
2144
2145 my $target_mpkey = extract_param($param, 'target-volume') // $mpkey;
2146
2147 my $digest = extract_param($param, 'digest');
2148
2149 my $target_digest = extract_param($param, 'target-digest');
2150
2151 my $lockname = 'disk';
2152
2153 my ($mpdata, $old_volid);
2154
2155 die "either set storage or target-vmid, but not both\n"
2156 if $storage && $target_vmid;
2157
2158 my $storecfg = PVE::Storage::config();
2159
2160 my $move_to_storage_checks = sub {
2161 PVE::LXC::Config->lock_config($vmid, sub {
2162 my $conf = PVE::LXC::Config->load_config($vmid);
2163 PVE::LXC::Config->check_lock($conf);
2164
2165 die "cannot move volumes of a running container\n"
2166 if PVE::LXC::check_running($vmid);
2167
2168 if ($mpkey =~ m/^unused\d+$/) {
2169 die "cannot move volume '$mpkey', only configured volumes can be moved to ".
2170 "another storage\n";
2171 }
2172
2173 $mpdata = PVE::LXC::Config->parse_volume($mpkey, $conf->{$mpkey});
2174 $old_volid = $mpdata->{volume};
2175
2176 die "you can't move a volume with snapshots and delete the source\n"
2177 if $param->{delete} && PVE::LXC::Config->is_volume_in_use_by_snapshots($conf, $old_volid);
2178
2179 PVE::Tools::assert_if_modified($digest, $conf->{digest});
2180
2181 PVE::LXC::Config->set_lock($vmid, $lockname);
2182 });
2183 };
2184
2185 my $storage_realcmd = sub {
2186 eval {
2187 PVE::Cluster::log_msg(
2188 'info',
2189 $authuser,
2190 "move volume CT $vmid: move --volume $mpkey --storage $storage"
2191 );
2192
2193 my $conf = PVE::LXC::Config->load_config($vmid);
2194 my $storage_cfg = PVE::Storage::config();
2195
2196 my $new_volid;
2197
2198 eval {
2199 PVE::Storage::activate_volumes($storage_cfg, [ $old_volid ]);
2200 my $bwlimit = extract_param($param, 'bwlimit');
2201 my $source_storage = PVE::Storage::parse_volume_id($old_volid);
2202 my $movelimit = PVE::Storage::get_bandwidth_limit(
2203 'move',
2204 [$source_storage, $storage],
2205 $bwlimit
2206 );
2207 $new_volid = PVE::LXC::copy_volume(
2208 $mpdata,
2209 $vmid,
2210 $storage,
2211 $storage_cfg,
2212 $conf,
2213 undef,
2214 $movelimit
2215 );
2216 if (PVE::LXC::Config->is_template($conf)) {
2217 PVE::Storage::activate_volumes($storage_cfg, [ $new_volid ]);
2218 my $template_volid = PVE::Storage::vdisk_create_base($storage_cfg, $new_volid);
2219 $mpdata->{volume} = $template_volid;
2220 } else {
2221 $mpdata->{volume} = $new_volid;
2222 }
2223
2224 PVE::LXC::Config->lock_config($vmid, sub {
2225 my $digest = $conf->{digest};
2226 $conf = PVE::LXC::Config->load_config($vmid);
2227 PVE::Tools::assert_if_modified($digest, $conf->{digest});
2228
2229 $conf->{$mpkey} = PVE::LXC::Config->print_ct_mountpoint(
2230 $mpdata,
2231 $mpkey eq 'rootfs'
2232 );
2233
2234 PVE::LXC::Config->add_unused_volume($conf, $old_volid) if !$param->{delete};
2235
2236 PVE::LXC::Config->write_config($vmid, $conf);
2237 });
2238
2239 eval {
2240 # try to deactivate volumes - avoid lvm LVs to be active on several nodes
2241 PVE::Storage::deactivate_volumes($storage_cfg, [ $new_volid ])
2242 };
2243 warn $@ if $@;
2244 };
2245 if (my $err = $@) {
2246 eval {
2247 PVE::Storage::vdisk_free($storage_cfg, $new_volid)
2248 if defined($new_volid);
2249 };
2250 warn $@ if $@;
2251 die $err;
2252 }
2253
2254 my $deactivated = 0;
2255 eval {
2256 PVE::Storage::deactivate_volumes($storage_cfg, [ $old_volid ]);
2257 $deactivated = 1;
2258 };
2259 warn $@ if $@;
2260
2261 if ($param->{delete}) {
2262 my $removed = 0;
2263 if ($deactivated) {
2264 eval {
2265 PVE::Storage::vdisk_free($storage_cfg, $old_volid);
2266 $removed = 1;
2267 };
2268 warn $@ if $@;
2269 }
2270 if (!$removed) {
2271 PVE::LXC::Config->lock_config($vmid, sub {
2272 my $conf = PVE::LXC::Config->load_config($vmid);
2273 PVE::LXC::Config->add_unused_volume($conf, $old_volid);
2274 PVE::LXC::Config->write_config($vmid, $conf);
2275 });
2276 }
2277 }
2278 };
2279 my $err = $@;
2280 eval { PVE::LXC::Config->remove_lock($vmid, $lockname) };
2281 warn $@ if $@;
2282 die $err if $err;
2283 };
2284
2285 my $load_and_check_reassign_configs = sub {
2286 my $vmlist = PVE::Cluster::get_vmlist()->{ids};
2287
2288 die "Cannot move to/from 'rootfs'\n" if $mpkey eq "rootfs" || $target_mpkey eq "rootfs";
2289
2290 if ($mpkey =~ m/^unused\d+$/ && $target_mpkey !~ m/^unused\d+$/) {
2291 die "Moving an unused volume to a used one is not possible\n";
2292 }
2293 die "could not find CT ${vmid}\n" if !exists($vmlist->{$vmid});
2294 die "could not find CT ${target_vmid}\n" if !exists($vmlist->{$target_vmid});
2295
2296 my $source_node = $vmlist->{$vmid}->{node};
2297 my $target_node = $vmlist->{$target_vmid}->{node};
2298
2299 die "Both containers need to be on the same node ($source_node != $target_node)\n"
2300 if $source_node ne $target_node;
2301
2302 my $source_conf = PVE::LXC::Config->load_config($vmid);
2303 PVE::LXC::Config->check_lock($source_conf);
2304 my $target_conf;
2305 if ($target_vmid eq $vmid) {
2306 $target_conf = $source_conf;
2307 } else {
2308 $target_conf = PVE::LXC::Config->load_config($target_vmid);
2309 PVE::LXC::Config->check_lock($target_conf);
2310 }
2311
2312 die "Can't move volumes from or to template CT\n"
2313 if ($source_conf->{template} || $target_conf->{template});
2314
2315 if ($digest) {
2316 eval { PVE::Tools::assert_if_modified($digest, $source_conf->{digest}) };
2317 die "Container ${vmid}: $@" if $@;
2318 }
2319
2320 if ($target_digest) {
2321 eval { PVE::Tools::assert_if_modified($target_digest, $target_conf->{digest}) };
2322 die "Container ${target_vmid}: $@" if $@;
2323 }
2324
2325 die "volume '${mpkey}' for container '$vmid' does not exist\n"
2326 if !defined($source_conf->{$mpkey});
2327
2328 die "Target volume key '${target_mpkey}' is already in use for container '$target_vmid'\n"
2329 if exists $target_conf->{$target_mpkey};
2330
2331 my $drive = PVE::LXC::Config->parse_volume($mpkey, $source_conf->{$mpkey});
2332 my $source_volid = $drive->{volume} or die "Volume '${mpkey}' has no associated image\n";
2333 die "Cannot move volume used by a snapshot to another container\n"
2334 if PVE::LXC::Config->is_volume_in_use_by_snapshots($source_conf, $source_volid);
2335 die "Storage does not support moving of this disk to another container\n"
2336 if !PVE::Storage::volume_has_feature($storecfg, 'rename', $source_volid);
2337 die "Cannot move a bindmount or device mount to another container\n"
2338 if $drive->{type} ne "volume";
2339 die "Cannot move in-use volume while the source CT is running - detach or shutdown first\n"
2340 if PVE::LXC::check_running($vmid) && $mpkey !~ m/^unused\d+$/;
2341
2342 my $repl_conf = PVE::ReplicationConfig->new();
2343 if ($repl_conf->check_for_existing_jobs($target_vmid, 1)) {
2344 my ($storeid, undef) = PVE::Storage::parse_volume_id($source_volid);
2345 my $format = (PVE::Storage::parse_volname($storecfg, $source_volid))[6];
2346
2347 die "Cannot move volume on storage '$storeid' to a replicated container - missing replication support\n"
2348 if !PVE::Storage::storage_can_replicate($storecfg, $storeid, $format);
2349 }
2350
2351 return ($source_conf, $target_conf, $drive);
2352 };
2353
2354 my $logfunc = sub { print STDERR "$_[0]\n"; };
2355
2356 my $volume_reassignfn = sub {
2357 return PVE::LXC::Config->lock_config($vmid, sub {
2358 return PVE::LXC::Config->lock_config($target_vmid, sub {
2359 my ($source_conf, $target_conf, $drive) = $load_and_check_reassign_configs->();
2360 my $source_volid = $drive->{volume};
2361
2362 my $target_unused = $target_mpkey =~ m/^unused\d+$/;
2363
2364 print "moving volume '$mpkey' from container '$vmid' to '$target_vmid'\n";
2365
2366 my ($storage, $source_volname) = PVE::Storage::parse_volume_id($source_volid);
2367
2368 my $fmt = (PVE::Storage::parse_volname($storecfg, $source_volid))[6];
2369
2370 my $new_volid = PVE::Storage::rename_volume(
2371 $storecfg,
2372 $source_volid,
2373 $target_vmid,
2374 );
2375
2376 $drive->{volume} = $new_volid;
2377
2378 delete $source_conf->{$mpkey};
2379 print "removing volume '${mpkey}' from container '${vmid}' config\n";
2380 PVE::LXC::Config->write_config($vmid, $source_conf);
2381
2382 my $drive_string;
2383 if ($target_unused) {
2384 $drive_string = $new_volid;
2385 } else {
2386 $drive_string = PVE::LXC::Config->print_volume($target_mpkey, $drive);
2387 }
2388
2389 if ($target_unused) {
2390 $target_conf->{$target_mpkey} = $drive_string;
2391 } else {
2392 my $running = PVE::LXC::check_running($target_vmid);
2393 my $param = { $target_mpkey => $drive_string };
2394 my $errors = PVE::LXC::Config->update_pct_config(
2395 $target_vmid,
2396 $target_conf,
2397 $running,
2398 $param
2399 );
2400 $rpcenv->warn($errors->{$_}) for keys $errors->%*;
2401 }
2402
2403 PVE::LXC::Config->write_config($target_vmid, $target_conf);
2404 $target_conf = PVE::LXC::Config->load_config($target_vmid);
2405
2406 PVE::LXC::update_lxc_config($target_vmid, $target_conf) if !$target_unused;
2407 print "target container '$target_vmid' updated with '$target_mpkey'\n";
2408
2409 # remove possible replication snapshots
2410 if (PVE::Storage::volume_has_feature($storecfg,'replicate', $source_volid)) {
2411 eval {
2412 PVE::Replication::prepare(
2413 $storecfg,
2414 [$new_volid],
2415 undef,
2416 1,
2417 undef,
2418 $logfunc,
2419 )
2420 };
2421 if (my $err = $@) {
2422 $rpcenv->warn("Failed to remove replication snapshots on volume ".
2423 "'${target_mpkey}'. Manual cleanup could be necessary. " .
2424 "Error: ${err}\n");
2425 }
2426 }
2427 });
2428 });
2429 };
2430
2431 if ($target_vmid && $storage) {
2432 my $msg = "either set 'storage' or 'target-vmid', but not both";
2433 raise_param_exc({ 'target-vmid' => $msg, 'storage' => $msg });
2434 } elsif ($target_vmid) {
2435 $rpcenv->check_vm_perm($authuser, $target_vmid, undef, ['VM.Config.Disk'])
2436 if $authuser ne 'root@pam';
2437
2438 my (undef, undef, $drive) = $load_and_check_reassign_configs->();
2439 my $storeid = PVE::Storage::parse_volume_id($drive->{volume});
2440 $rpcenv->check($authuser, "/storage/$storeid", ['Datastore.AllocateSpace']);
2441 return $rpcenv->fork_worker(
2442 'move_volume',
2443 "${vmid}-${mpkey}>${target_vmid}-${target_mpkey}",
2444 $authuser,
2445 $volume_reassignfn
2446 );
2447 } elsif ($storage) {
2448 $rpcenv->check($authuser, "/storage/$storage", ['Datastore.AllocateSpace']);
2449 &$move_to_storage_checks();
2450 my $task = eval {
2451 $rpcenv->fork_worker('move_volume', $vmid, $authuser, $storage_realcmd);
2452 };
2453 if (my $err = $@) {
2454 eval { PVE::LXC::Config->remove_lock($vmid, $lockname) };
2455 warn $@ if $@;
2456 die $err;
2457 }
2458 return $task;
2459 } else {
2460 my $msg = "both 'storage' and 'target-vmid' missing, either needs to be set";
2461 raise_param_exc({ 'target-vmid' => $msg, 'storage' => $msg });
2462 }
2463 }});
2464
2465 __PACKAGE__->register_method({
2466 name => 'vm_pending',
2467 path => '{vmid}/pending',
2468 method => 'GET',
2469 proxyto => 'node',
2470 description => 'Get container configuration, including pending changes.',
2471 permissions => {
2472 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2473 },
2474 parameters => {
2475 additionalProperties => 0,
2476 properties => {
2477 node => get_standard_option('pve-node'),
2478 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
2479 },
2480 },
2481 returns => {
2482 type => "array",
2483 items => {
2484 type => "object",
2485 properties => {
2486 key => {
2487 description => 'Configuration option name.',
2488 type => 'string',
2489 },
2490 value => {
2491 description => 'Current value.',
2492 type => 'string',
2493 optional => 1,
2494 },
2495 pending => {
2496 description => 'Pending value.',
2497 type => 'string',
2498 optional => 1,
2499 },
2500 delete => {
2501 description => "Indicates a pending delete request if present and not 0.",
2502 type => 'integer',
2503 minimum => 0,
2504 maximum => 2,
2505 optional => 1,
2506 },
2507 },
2508 },
2509 },
2510 code => sub {
2511 my ($param) = @_;
2512
2513 my $conf = PVE::LXC::Config->load_config($param->{vmid});
2514
2515 my $pending_delete_hash = PVE::LXC::Config->parse_pending_delete($conf->{pending}->{delete});
2516
2517 return PVE::GuestHelpers::config_with_pending_array($conf, $pending_delete_hash);
2518 }});
2519
2520 __PACKAGE__->register_method({
2521 name => 'ip',
2522 path => '{vmid}/interfaces',
2523 method => 'GET',
2524 protected => 1,
2525 permissions => {
2526 check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
2527 },
2528 description => 'Get IP addresses of the specified container interface.',
2529 parameters => {
2530 additionalProperties => 0,
2531 properties => {
2532 node => get_standard_option('pve-node'),
2533 vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
2534 },
2535 },
2536 returns => {
2537 type => "array",
2538 items => {
2539 type => 'object',
2540 properties => {
2541 name => {
2542 type => 'string',
2543 description => 'The name of the interface',
2544 optional => 0,
2545 },
2546 hwaddr => {
2547 type => 'string',
2548 description => 'The MAC address of the interface',
2549 optional => 0,
2550 },
2551 inet => {
2552 type => 'string',
2553 description => 'The IPv4 address of the interface',
2554 optional => 1,
2555 },
2556 inet6 => {
2557 type => 'string',
2558 description => 'The IPv6 address of the interface',
2559 optional => 1,
2560 },
2561 }
2562 },
2563 },
2564 code => sub {
2565 my ($param) = @_;
2566
2567 return PVE::LXC::get_interfaces($param->{vmid});
2568 }});
2569
2570 __PACKAGE__->register_method({
2571 name => 'mtunnel',
2572 path => '{vmid}/mtunnel',
2573 method => 'POST',
2574 protected => 1,
2575 description => 'Migration tunnel endpoint - only for internal use by CT migration.',
2576 permissions => {
2577 check =>
2578 [ 'and',
2579 ['perm', '/vms/{vmid}', [ 'VM.Allocate' ]],
2580 ['perm', '/', [ 'Sys.Incoming' ]],
2581 ],
2582 description => "You need 'VM.Allocate' permissions on '/vms/{vmid}' and Sys.Incoming" .
2583 " on '/'. Further permission checks happen during the actual migration.",
2584 },
2585 parameters => {
2586 additionalProperties => 0,
2587 properties => {
2588 node => get_standard_option('pve-node'),
2589 vmid => get_standard_option('pve-vmid'),
2590 storages => {
2591 type => 'string',
2592 format => 'pve-storage-id-list',
2593 optional => 1,
2594 description => 'List of storages to check permission and availability. Will be checked again for all actually used storages during migration.',
2595 },
2596 bridges => {
2597 type => 'string',
2598 format => 'pve-bridge-id-list',
2599 optional => 1,
2600 description => 'List of network bridges to check availability. Will be checked again for actually used bridges during migration.',
2601 },
2602 },
2603 },
2604 returns => {
2605 additionalProperties => 0,
2606 properties => {
2607 upid => { type => 'string' },
2608 ticket => { type => 'string' },
2609 socket => { type => 'string' },
2610 },
2611 },
2612 code => sub {
2613 my ($param) = @_;
2614
2615 my $rpcenv = PVE::RPCEnvironment::get();
2616 my $authuser = $rpcenv->get_user();
2617
2618 my $node = extract_param($param, 'node');
2619 my $vmid = extract_param($param, 'vmid');
2620
2621 my $storages = extract_param($param, 'storages');
2622 my $bridges = extract_param($param, 'bridges');
2623
2624 my $nodename = PVE::INotify::nodename();
2625
2626 raise_param_exc({ node => "node needs to be 'localhost' or local hostname '$nodename'" })
2627 if $node ne 'localhost' && $node ne $nodename;
2628
2629 $node = $nodename;
2630
2631 my $storecfg = PVE::Storage::config();
2632 foreach my $storeid (PVE::Tools::split_list($storages)) {
2633 $check_storage_access_migrate->($rpcenv, $authuser, $storecfg, $storeid, $node);
2634 }
2635
2636 foreach my $bridge (PVE::Tools::split_list($bridges)) {
2637 PVE::Network::read_bridge_mtu($bridge);
2638 }
2639
2640 PVE::Cluster::check_cfs_quorum();
2641
2642 my $socket_addr = "/run/pve/ct-$vmid.mtunnel";
2643
2644 my $lock = 'create';
2645 eval { PVE::LXC::Config->create_and_lock_config($vmid, 0, $lock); };
2646
2647 raise_param_exc({ vmid => "unable to create empty CT config - $@"})
2648 if $@;
2649
2650 my $realcmd = sub {
2651 my $state = {
2652 storecfg => PVE::Storage::config(),
2653 lock => $lock,
2654 vmid => $vmid,
2655 };
2656
2657 my $run_locked = sub {
2658 my ($code, $params) = @_;
2659 return PVE::LXC::Config->lock_config($state->{vmid}, sub {
2660 my $conf = PVE::LXC::Config->load_config($state->{vmid});
2661
2662 $state->{conf} = $conf;
2663
2664 die "Encountered wrong lock - aborting mtunnel command handling.\n"
2665 if $state->{lock} && !PVE::LXC::Config->has_lock($conf, $state->{lock});
2666
2667 return $code->($params);
2668 });
2669 };
2670
2671 my $cmd_desc = {
2672 config => {
2673 conf => {
2674 type => 'string',
2675 description => 'Full CT config, adapted for target cluster/node',
2676 },
2677 'firewall-config' => {
2678 type => 'string',
2679 description => 'CT firewall config',
2680 optional => 1,
2681 },
2682 },
2683 ticket => {
2684 path => {
2685 type => 'string',
2686 description => 'socket path for which the ticket should be valid. must be known to current mtunnel instance.',
2687 },
2688 },
2689 quit => {
2690 cleanup => {
2691 type => 'boolean',
2692 description => 'remove CT config and volumes, aborting migration',
2693 default => 0,
2694 },
2695 },
2696 'disk-import' => $PVE::StorageTunnel::cmd_schema->{'disk-import'},
2697 'query-disk-import' => $PVE::StorageTunnel::cmd_schema->{'query-disk-import'},
2698 bwlimit => $PVE::StorageTunnel::cmd_schema->{bwlimit},
2699 };
2700
2701 my $cmd_handlers = {
2702 'version' => sub {
2703 # compared against other end's version
2704 # bump/reset for breaking changes
2705 # bump/bump for opt-in changes
2706 return {
2707 api => $PVE::LXC::Migrate::WS_TUNNEL_VERSION,
2708 age => 0,
2709 };
2710 },
2711 'config' => sub {
2712 my ($params) = @_;
2713
2714 # parse and write out VM FW config if given
2715 if (my $fw_conf = $params->{'firewall-config'}) {
2716 my ($path, $fh) = PVE::Tools::tempfile_contents($fw_conf, 700);
2717
2718 my $empty_conf = {
2719 rules => [],
2720 options => {},
2721 aliases => {},
2722 ipset => {} ,
2723 ipset_comments => {},
2724 };
2725 my $cluster_fw_conf = PVE::Firewall::load_clusterfw_conf();
2726
2727 # TODO: add flag for strict parsing?
2728 # TODO: add import sub that does all this given raw content?
2729 my $vmfw_conf = PVE::Firewall::generic_fw_config_parser($path, $cluster_fw_conf, $empty_conf, 'vm');
2730 $vmfw_conf->{vmid} = $state->{vmid};
2731 PVE::Firewall::save_vmfw_conf($state->{vmid}, $vmfw_conf);
2732
2733 $state->{cleanup}->{fw} = 1;
2734 }
2735
2736 my $conf_fn = "incoming/lxc/$state->{vmid}.conf";
2737 my $new_conf = PVE::LXC::Config::parse_pct_config($conf_fn, $params->{conf}, 1);
2738 delete $new_conf->{lock};
2739 delete $new_conf->{digest};
2740
2741 my $unprivileged = delete $new_conf->{unprivileged};
2742 my $arch = delete $new_conf->{arch};
2743
2744 # TODO handle properly?
2745 delete $new_conf->{snapshots};
2746 delete $new_conf->{parent};
2747 delete $new_conf->{pending};
2748 delete $new_conf->{lxc};
2749
2750 PVE::LXC::Config->remove_lock($state->{vmid}, 'create');
2751
2752 eval {
2753 my $conf = {
2754 unprivileged => $unprivileged,
2755 arch => $arch,
2756 };
2757 PVE::LXC::check_ct_modify_config_perm(
2758 $rpcenv,
2759 $authuser,
2760 $state->{vmid},
2761 undef,
2762 $conf,
2763 $new_conf,
2764 undef,
2765 $unprivileged,
2766 );
2767 my $errors = PVE::LXC::Config->update_pct_config(
2768 $state->{vmid},
2769 $conf,
2770 0,
2771 $new_conf,
2772 [],
2773 [],
2774 );
2775 raise_param_exc($errors) if scalar(keys %$errors);
2776 PVE::LXC::Config->write_config($state->{vmid}, $conf);
2777 PVE::LXC::update_lxc_config($vmid, $conf);
2778 };
2779 if (my $err = $@) {
2780 # revert to locked previous config
2781 my $conf = PVE::LXC::Config->load_config($state->{vmid});
2782 $conf->{lock} = 'create';
2783 PVE::LXC::Config->write_config($state->{vmid}, $conf);
2784
2785 die $err;
2786 }
2787
2788 my $conf = PVE::LXC::Config->load_config($state->{vmid});
2789 $conf->{lock} = 'migrate';
2790 PVE::LXC::Config->write_config($state->{vmid}, $conf);
2791
2792 $state->{lock} = 'migrate';
2793
2794 return;
2795 },
2796 'bwlimit' => sub {
2797 my ($params) = @_;
2798 return PVE::StorageTunnel::handle_bwlimit($params);
2799 },
2800 'disk-import' => sub {
2801 my ($params) = @_;
2802
2803 $check_storage_access_migrate->(
2804 $rpcenv,
2805 $authuser,
2806 $state->{storecfg},
2807 $params->{storage},
2808 $node
2809 );
2810
2811 $params->{unix} = "/run/pve/ct-$state->{vmid}.storage";
2812
2813 return PVE::StorageTunnel::handle_disk_import($state, $params);
2814 },
2815 'query-disk-import' => sub {
2816 my ($params) = @_;
2817
2818 return PVE::StorageTunnel::handle_query_disk_import($state, $params);
2819 },
2820 'unlock' => sub {
2821 PVE::LXC::Config->remove_lock($state->{vmid}, $state->{lock});
2822 delete $state->{lock};
2823 return;
2824 },
2825 'start' => sub {
2826 PVE::LXC::vm_start(
2827 $state->{vmid},
2828 $state->{conf},
2829 0
2830 );
2831
2832 return;
2833 },
2834 'stop' => sub {
2835 PVE::LXC::vm_stop($state->{vmid}, 1, 10, 1);
2836 return;
2837 },
2838 'ticket' => sub {
2839 my ($params) = @_;
2840
2841 my $path = $params->{path};
2842
2843 die "Not allowed to generate ticket for unknown socket '$path'\n"
2844 if !defined($state->{sockets}->{$path});
2845
2846 return { ticket => PVE::AccessControl::assemble_tunnel_ticket($authuser, "/socket/$path") };
2847 },
2848 'quit' => sub {
2849 my ($params) = @_;
2850
2851 if ($params->{cleanup}) {
2852 if ($state->{cleanup}->{fw}) {
2853 PVE::Firewall::remove_vmfw_conf($state->{vmid});
2854 }
2855
2856 for my $volid (keys $state->{cleanup}->{volumes}->%*) {
2857 print "freeing volume '$volid' as part of cleanup\n";
2858 eval { PVE::Storage::vdisk_free($state->{storecfg}, $volid) };
2859 warn $@ if $@;
2860 }
2861
2862 PVE::LXC::destroy_lxc_container(
2863 $state->{storecfg},
2864 $state->{vmid},
2865 $state->{conf},
2866 undef,
2867 0,
2868 );
2869 }
2870
2871 print "switching to exit-mode, waiting for client to disconnect\n";
2872 $state->{exit} = 1;
2873 return;
2874 },
2875 };
2876
2877 $run_locked->(sub {
2878 my $socket_addr = "/run/pve/ct-$state->{vmid}.mtunnel";
2879 unlink $socket_addr;
2880
2881 $state->{socket} = IO::Socket::UNIX->new(
2882 Type => SOCK_STREAM(),
2883 Local => $socket_addr,
2884 Listen => 1,
2885 );
2886
2887 $state->{socket_uid} = getpwnam('www-data')
2888 or die "Failed to resolve user 'www-data' to numeric UID\n";
2889 chown $state->{socket_uid}, -1, $socket_addr;
2890 });
2891
2892 print "mtunnel started\n";
2893
2894 my $conn = eval { PVE::Tools::run_with_timeout(300, sub { $state->{socket}->accept() }) };
2895 if ($@) {
2896 warn "Failed to accept tunnel connection - $@\n";
2897
2898 warn "Removing tunnel socket..\n";
2899 unlink $state->{socket};
2900
2901 warn "Removing temporary VM config..\n";
2902 $run_locked->(sub {
2903 PVE::LXC::destroy_config($state->{vmid});
2904 });
2905
2906 die "Exiting mtunnel\n";
2907 }
2908
2909 $state->{conn} = $conn;
2910
2911 my $reply_err = sub {
2912 my ($msg) = @_;
2913
2914 my $reply = JSON::encode_json({
2915 success => JSON::false,
2916 msg => $msg,
2917 });
2918 $conn->print("$reply\n");
2919 $conn->flush();
2920 };
2921
2922 my $reply_ok = sub {
2923 my ($res) = @_;
2924
2925 $res->{success} = JSON::true;
2926 my $reply = JSON::encode_json($res);
2927 $conn->print("$reply\n");
2928 $conn->flush();
2929 };
2930
2931 while (my $line = <$conn>) {
2932 chomp $line;
2933
2934 # untaint, we validate below if needed
2935 ($line) = $line =~ /^(.*)$/;
2936 my $parsed = eval { JSON::decode_json($line) };
2937 if ($@) {
2938 $reply_err->("failed to parse command - $@");
2939 next;
2940 }
2941
2942 my $cmd = delete $parsed->{cmd};
2943 if (!defined($cmd)) {
2944 $reply_err->("'cmd' missing");
2945 } elsif ($state->{exit}) {
2946 $reply_err->("tunnel is in exit-mode, processing '$cmd' cmd not possible");
2947 next;
2948 } elsif (my $handler = $cmd_handlers->{$cmd}) {
2949 print "received command '$cmd'\n";
2950 eval {
2951 if ($cmd_desc->{$cmd}) {
2952 PVE::JSONSchema::validate($parsed, $cmd_desc->{$cmd});
2953 } else {
2954 $parsed = {};
2955 }
2956 my $res = $run_locked->($handler, $parsed);
2957 $reply_ok->($res);
2958 };
2959 $reply_err->("failed to handle '$cmd' command - $@")
2960 if $@;
2961 } else {
2962 $reply_err->("unknown command '$cmd' given");
2963 }
2964 }
2965
2966 if ($state->{exit}) {
2967 print "mtunnel exited\n";
2968 } else {
2969 die "mtunnel exited unexpectedly\n";
2970 }
2971 };
2972
2973 my $ticket = PVE::AccessControl::assemble_tunnel_ticket($authuser, "/socket/$socket_addr");
2974 my $upid = $rpcenv->fork_worker('vzmtunnel', $vmid, $authuser, $realcmd);
2975
2976 return {
2977 ticket => $ticket,
2978 upid => $upid,
2979 socket => $socket_addr,
2980 };
2981 }});
2982
2983 __PACKAGE__->register_method({
2984 name => 'mtunnelwebsocket',
2985 path => '{vmid}/mtunnelwebsocket',
2986 method => 'GET',
2987 permissions => {
2988 description => "You need to pass a ticket valid for the selected socket. Tickets can be created via the mtunnel API call, which will check permissions accordingly.",
2989 user => 'all', # check inside
2990 },
2991 description => 'Migration tunnel endpoint for websocket upgrade - only for internal use by VM migration.',
2992 parameters => {
2993 additionalProperties => 0,
2994 properties => {
2995 node => get_standard_option('pve-node'),
2996 vmid => get_standard_option('pve-vmid'),
2997 socket => {
2998 type => "string",
2999 description => "unix socket to forward to",
3000 },
3001 ticket => {
3002 type => "string",
3003 description => "ticket return by initial 'mtunnel' API call, or retrieved via 'ticket' tunnel command",
3004 },
3005 },
3006 },
3007 returns => {
3008 type => "object",
3009 properties => {
3010 port => { type => 'string', optional => 1 },
3011 socket => { type => 'string', optional => 1 },
3012 },
3013 },
3014 code => sub {
3015 my ($param) = @_;
3016
3017 my $rpcenv = PVE::RPCEnvironment::get();
3018 my $authuser = $rpcenv->get_user();
3019
3020 my $nodename = PVE::INotify::nodename();
3021 my $node = extract_param($param, 'node');
3022
3023 raise_param_exc({ node => "node needs to be 'localhost' or local hostname '$nodename'" })
3024 if $node ne 'localhost' && $node ne $nodename;
3025
3026 my $vmid = $param->{vmid};
3027 # check VM exists
3028 PVE::LXC::Config->load_config($vmid);
3029
3030 my $socket = $param->{socket};
3031 PVE::AccessControl::verify_tunnel_ticket($param->{ticket}, $authuser, "/socket/$socket");
3032
3033 return { socket => $socket };
3034 }});
3035 1;