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