]> git.proxmox.com Git - pve-container.git/blob - src/PVE/LXC/Config.pm
eec6b387c4d73b199f611b6976834703f7a8815b
[pve-container.git] / src / PVE / LXC / Config.pm
1 package PVE::LXC::Config;
2
3 use strict;
4 use warnings;
5
6 use PVE::AbstractConfig;
7 use PVE::Cluster qw(cfs_register_file);
8 use PVE::DataCenterConfig;
9 use PVE::GuestHelpers;
10 use PVE::INotify;
11 use PVE::JSONSchema qw(get_standard_option);
12 use PVE::Tools;
13
14 use base qw(PVE::AbstractConfig);
15
16 my $nodename = PVE::INotify::nodename();
17 my $lock_handles = {};
18 my $lockdir = "/run/lock/lxc";
19 mkdir $lockdir;
20 mkdir "/etc/pve/nodes/$nodename/lxc";
21 my $MAX_MOUNT_POINTS = 256;
22 my $MAX_UNUSED_DISKS = $MAX_MOUNT_POINTS;
23
24 # BEGIN implemented abstract methods from PVE::AbstractConfig
25
26 sub guest_type {
27 return "CT";
28 }
29
30 sub __config_max_unused_disks {
31 my ($class) = @_;
32
33 return $MAX_UNUSED_DISKS;
34 }
35
36 sub config_file_lock {
37 my ($class, $vmid) = @_;
38
39 return "$lockdir/pve-config-${vmid}.lock";
40 }
41
42 sub cfs_config_path {
43 my ($class, $vmid, $node) = @_;
44
45 $node = $nodename if !$node;
46 return "nodes/$node/lxc/$vmid.conf";
47 }
48
49 sub mountpoint_backup_enabled {
50 my ($class, $mp_key, $mountpoint) = @_;
51
52 return 1 if $mp_key eq 'rootfs';
53
54 return 0 if $mountpoint->{type} ne 'volume';
55
56 return 1 if $mountpoint->{backup};
57
58 return 0;
59 }
60
61 sub has_feature {
62 my ($class, $feature, $conf, $storecfg, $snapname, $running, $backup_only) = @_;
63 my $err;
64
65 $class->foreach_mountpoint($conf, sub {
66 my ($ms, $mountpoint) = @_;
67
68 return if $err; # skip further test
69 return if $backup_only && !$class->mountpoint_backup_enabled($ms, $mountpoint);
70
71 $err = 1
72 if !PVE::Storage::volume_has_feature($storecfg, $feature,
73 $mountpoint->{volume},
74 $snapname, $running);
75 });
76
77 return $err ? 0 : 1;
78 }
79
80 sub __snapshot_save_vmstate {
81 my ($class, $vmid, $conf, $snapname, $storecfg) = @_;
82 die "implement me - snapshot_save_vmstate\n";
83 }
84
85 sub __snapshot_check_running {
86 my ($class, $vmid) = @_;
87 return PVE::LXC::check_running($vmid);
88 }
89
90 sub __snapshot_check_freeze_needed {
91 my ($class, $vmid, $config, $save_vmstate) = @_;
92
93 my $ret = $class->__snapshot_check_running($vmid);
94 return ($ret, $ret);
95 }
96
97 sub __snapshot_freeze {
98 my ($class, $vmid, $unfreeze) = @_;
99
100 if ($unfreeze) {
101 eval { PVE::Tools::run_command(['/usr/bin/lxc-unfreeze', '-n', $vmid]); };
102 warn $@ if $@;
103 } else {
104 PVE::Tools::run_command(['/usr/bin/lxc-freeze', '-n', $vmid]);
105 PVE::LXC::sync_container_namespace($vmid);
106 }
107 }
108
109 sub __snapshot_create_vol_snapshot {
110 my ($class, $vmid, $ms, $mountpoint, $snapname) = @_;
111
112 my $storecfg = PVE::Storage::config();
113
114 return if $snapname eq 'vzdump' &&
115 !$class->mountpoint_backup_enabled($ms, $mountpoint);
116
117 PVE::Storage::volume_snapshot($storecfg, $mountpoint->{volume}, $snapname);
118 }
119
120 sub __snapshot_delete_remove_drive {
121 my ($class, $snap, $remove_drive) = @_;
122
123 if ($remove_drive eq 'vmstate') {
124 die "implement me - saving vmstate\n";
125 } else {
126 my $value = $snap->{$remove_drive};
127 my $mountpoint = $remove_drive eq 'rootfs' ? $class->parse_ct_rootfs($value, 1) : $class->parse_ct_mountpoint($value, 1);
128 delete $snap->{$remove_drive};
129
130 $class->add_unused_volume($snap, $mountpoint->{volume})
131 if ($mountpoint->{type} eq 'volume');
132 }
133 }
134
135 sub __snapshot_delete_vmstate_file {
136 my ($class, $snap, $force) = @_;
137
138 die "implement me - saving vmstate\n";
139 }
140
141 sub __snapshot_delete_vol_snapshot {
142 my ($class, $vmid, $ms, $mountpoint, $snapname, $unused) = @_;
143
144 return if $snapname eq 'vzdump' &&
145 !$class->mountpoint_backup_enabled($ms, $mountpoint);
146
147 my $storecfg = PVE::Storage::config();
148 PVE::Storage::volume_snapshot_delete($storecfg, $mountpoint->{volume}, $snapname);
149 push @$unused, $mountpoint->{volume};
150 }
151
152 sub __snapshot_rollback_vol_possible {
153 my ($class, $mountpoint, $snapname) = @_;
154
155 my $storecfg = PVE::Storage::config();
156 PVE::Storage::volume_rollback_is_possible($storecfg, $mountpoint->{volume}, $snapname);
157 }
158
159 sub __snapshot_rollback_vol_rollback {
160 my ($class, $mountpoint, $snapname) = @_;
161
162 my $storecfg = PVE::Storage::config();
163 PVE::Storage::volume_snapshot_rollback($storecfg, $mountpoint->{volume}, $snapname);
164 }
165
166 sub __snapshot_rollback_vm_stop {
167 my ($class, $vmid) = @_;
168
169 PVE::LXC::vm_stop($vmid, 1)
170 if $class->__snapshot_check_running($vmid);
171 }
172
173 sub __snapshot_rollback_vm_start {
174 my ($class, $vmid, $vmstate, $data);
175
176 die "implement me - save vmstate\n";
177 }
178
179 sub __snapshot_rollback_get_unused {
180 my ($class, $conf, $snap) = @_;
181
182 my $unused = [];
183
184 $class->__snapshot_foreach_volume($conf, sub {
185 my ($vs, $volume) = @_;
186
187 return if $volume->{type} ne 'volume';
188
189 my $found = 0;
190 my $volid = $volume->{volume};
191
192 $class->__snapshot_foreach_volume($snap, sub {
193 my ($ms, $mountpoint) = @_;
194
195 return if $found;
196 return if ($mountpoint->{type} ne 'volume');
197
198 $found = 1
199 if ($mountpoint->{volume} && $mountpoint->{volume} eq $volid);
200 });
201
202 push @$unused, $volid if !$found;
203 });
204
205 return $unused;
206 }
207
208 sub __snapshot_foreach_volume {
209 my ($class, $conf, $func) = @_;
210
211 $class->foreach_mountpoint($conf, $func);
212 }
213
214 # END implemented abstract methods from PVE::AbstractConfig
215
216 # BEGIN JSON config code
217
218 cfs_register_file('/lxc/', \&parse_pct_config, \&write_pct_config);
219
220
221 my $valid_mount_option_re = qr/(noatime|nodev|nosuid|noexec)/;
222
223 sub is_valid_mount_option {
224 my ($option) = @_;
225 return $option =~ $valid_mount_option_re;
226 }
227
228 my $rootfs_desc = {
229 volume => {
230 type => 'string',
231 default_key => 1,
232 format => 'pve-lxc-mp-string',
233 format_description => 'volume',
234 description => 'Volume, device or directory to mount into the container.',
235 },
236 size => {
237 type => 'string',
238 format => 'disk-size',
239 format_description => 'DiskSize',
240 description => 'Volume size (read only value).',
241 optional => 1,
242 },
243 acl => {
244 type => 'boolean',
245 description => 'Explicitly enable or disable ACL support.',
246 optional => 1,
247 },
248 mountoptions => {
249 optional => 1,
250 type => 'string',
251 description => 'Extra mount options for rootfs/mps.',
252 format_description => 'opt[;opt...]',
253 pattern => qr/$valid_mount_option_re(;$valid_mount_option_re)*/,
254 },
255 ro => {
256 type => 'boolean',
257 description => 'Read-only mount point',
258 optional => 1,
259 },
260 quota => {
261 type => 'boolean',
262 description => 'Enable user quotas inside the container (not supported with zfs subvolumes)',
263 optional => 1,
264 },
265 replicate => {
266 type => 'boolean',
267 description => 'Will include this volume to a storage replica job.',
268 optional => 1,
269 default => 1,
270 },
271 shared => {
272 type => 'boolean',
273 description => 'Mark this non-volume mount point as available on multiple nodes (see \'nodes\')',
274 verbose_description => "Mark this non-volume mount point as available on all nodes.\n\nWARNING: This option does not share the mount point automatically, it assumes it is shared already!",
275 optional => 1,
276 default => 0,
277 },
278 };
279
280 PVE::JSONSchema::register_standard_option('pve-ct-rootfs', {
281 type => 'string', format => $rootfs_desc,
282 description => "Use volume as container root.",
283 optional => 1,
284 });
285
286 my $features_desc = {
287 mount => {
288 optional => 1,
289 type => 'string',
290 description => "Allow mounting file systems of specific types."
291 ." This should be a list of file system types as used with the mount command."
292 ." Note that this can have negative effects on the container's security."
293 ." With access to a loop device, mounting a file can circumvent the mknod"
294 ." permission of the devices cgroup, mounting an NFS file system can"
295 ." block the host's I/O completely and prevent it from rebooting, etc.",
296 format_description => 'fstype;fstype;...',
297 pattern => qr/[a-zA-Z0-9_; ]+/,
298 },
299 nesting => {
300 optional => 1,
301 type => 'boolean',
302 default => 0,
303 description => "Allow nesting."
304 ." Best used with unprivileged containers with additional id mapping."
305 ." Note that this will expose procfs and sysfs contents of the host"
306 ." to the guest.",
307 },
308 keyctl => {
309 optional => 1,
310 type => 'boolean',
311 default => 0,
312 description => "For unprivileged containers only: Allow the use of the keyctl() system call."
313 ." This is required to use docker inside a container."
314 ." By default unprivileged containers will see this system call as non-existent."
315 ." This is mostly a workaround for systemd-networkd, as it will treat it as a fatal"
316 ." error when some keyctl() operations are denied by the kernel due to lacking permissions."
317 ." Essentially, you can choose between running systemd-networkd or docker.",
318 },
319 fuse => {
320 optional => 1,
321 type => 'boolean',
322 default => 0,
323 description => "Allow using 'fuse' file systems in a container."
324 ." Note that interactions between fuse and the freezer cgroup can potentially cause I/O deadlocks.",
325 },
326 };
327
328 my $confdesc = {
329 lock => {
330 optional => 1,
331 type => 'string',
332 description => "Lock/unlock the VM.",
333 enum => [qw(backup create destroyed disk fstrim migrate mounted rollback snapshot snapshot-delete)],
334 },
335 onboot => {
336 optional => 1,
337 type => 'boolean',
338 description => "Specifies whether a VM will be started during system bootup.",
339 default => 0,
340 },
341 startup => get_standard_option('pve-startup-order'),
342 template => {
343 optional => 1,
344 type => 'boolean',
345 description => "Enable/disable Template.",
346 default => 0,
347 },
348 arch => {
349 optional => 1,
350 type => 'string',
351 enum => ['amd64', 'i386', 'arm64', 'armhf'],
352 description => "OS architecture type.",
353 default => 'amd64',
354 },
355 ostype => {
356 optional => 1,
357 type => 'string',
358 enum => [qw(debian ubuntu centos fedora opensuse archlinux alpine gentoo unmanaged)],
359 description => "OS type. This is used to setup configuration inside the container, and corresponds to lxc setup scripts in /usr/share/lxc/config/<ostype>.common.conf. Value 'unmanaged' can be used to skip and OS specific setup.",
360 },
361 console => {
362 optional => 1,
363 type => 'boolean',
364 description => "Attach a console device (/dev/console) to the container.",
365 default => 1,
366 },
367 tty => {
368 optional => 1,
369 type => 'integer',
370 description => "Specify the number of tty available to the container",
371 minimum => 0,
372 maximum => 6,
373 default => 2,
374 },
375 cores => {
376 optional => 1,
377 type => 'integer',
378 description => "The number of cores assigned to the container. A container can use all available cores by default.",
379 minimum => 1,
380 maximum => 128,
381 },
382 cpulimit => {
383 optional => 1,
384 type => 'number',
385 description => "Limit of CPU usage.\n\nNOTE: If the computer has 2 CPUs, it has a total of '2' CPU time. Value '0' indicates no CPU limit.",
386 minimum => 0,
387 maximum => 128,
388 default => 0,
389 },
390 cpuunits => {
391 optional => 1,
392 type => 'integer',
393 description => "CPU weight for a VM. Argument is used in the kernel fair scheduler. The larger the number is, the more CPU time this VM gets. Number is relative to the weights of all the other running VMs.\n\nNOTE: You can disable fair-scheduler configuration by setting this to 0.",
394 minimum => 0,
395 maximum => 500000,
396 default => 1024,
397 },
398 memory => {
399 optional => 1,
400 type => 'integer',
401 description => "Amount of RAM for the VM in MB.",
402 minimum => 16,
403 default => 512,
404 },
405 swap => {
406 optional => 1,
407 type => 'integer',
408 description => "Amount of SWAP for the VM in MB.",
409 minimum => 0,
410 default => 512,
411 },
412 hostname => {
413 optional => 1,
414 description => "Set a host name for the container.",
415 type => 'string', format => 'dns-name',
416 maxLength => 255,
417 },
418 description => {
419 optional => 1,
420 type => 'string',
421 description => "Container description. Only used on the configuration web interface.",
422 },
423 searchdomain => {
424 optional => 1,
425 type => 'string', format => 'dns-name-list',
426 description => "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.",
427 },
428 nameserver => {
429 optional => 1,
430 type => 'string', format => 'address-list',
431 description => "Sets DNS server IP address for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.",
432 },
433 rootfs => get_standard_option('pve-ct-rootfs'),
434 parent => {
435 optional => 1,
436 type => 'string', format => 'pve-configid',
437 maxLength => 40,
438 description => "Parent snapshot name. This is used internally, and should not be modified.",
439 },
440 snaptime => {
441 optional => 1,
442 description => "Timestamp for snapshots.",
443 type => 'integer',
444 minimum => 0,
445 },
446 cmode => {
447 optional => 1,
448 description => "Console mode. By default, the console command tries to open a connection to one of the available tty devices. By setting cmode to 'console' it tries to attach to /dev/console instead. If you set cmode to 'shell', it simply invokes a shell inside the container (no login).",
449 type => 'string',
450 enum => ['shell', 'console', 'tty'],
451 default => 'tty',
452 },
453 protection => {
454 optional => 1,
455 type => 'boolean',
456 description => "Sets the protection flag of the container. This will prevent the CT or CT's disk remove/update operation.",
457 default => 0,
458 },
459 unprivileged => {
460 optional => 1,
461 type => 'boolean',
462 description => "Makes the container run as unprivileged user. (Should not be modified manually.)",
463 default => 0,
464 },
465 features => {
466 optional => 1,
467 type => 'string',
468 format => $features_desc,
469 description => "Allow containers access to advanced features.",
470 },
471 hookscript => {
472 optional => 1,
473 type => 'string',
474 format => 'pve-volume-id',
475 description => 'Script that will be exectued during various steps in the containers lifetime.',
476 },
477 tags => {
478 type => 'string', format => 'pve-tag-list',
479 description => 'Tags of the Container. This is only meta information.',
480 optional => 1,
481 },
482 };
483
484 my $valid_lxc_conf_keys = {
485 'lxc.apparmor.profile' => 1,
486 'lxc.apparmor.allow_incomplete' => 1,
487 'lxc.apparmor.allow_nesting' => 1,
488 'lxc.apparmor.raw' => 1,
489 'lxc.selinux.context' => 1,
490 'lxc.include' => 1,
491 'lxc.arch' => 1,
492 'lxc.uts.name' => 1,
493 'lxc.signal.halt' => 1,
494 'lxc.signal.reboot' => 1,
495 'lxc.signal.stop' => 1,
496 'lxc.init.cmd' => 1,
497 'lxc.pty.max' => 1,
498 'lxc.console.logfile' => 1,
499 'lxc.console.path' => 1,
500 'lxc.tty.max' => 1,
501 'lxc.devtty.dir' => 1,
502 'lxc.hook.autodev' => 1,
503 'lxc.autodev' => 1,
504 'lxc.kmsg' => 1,
505 'lxc.mount.fstab' => 1,
506 'lxc.mount.entry' => 1,
507 'lxc.mount.auto' => 1,
508 'lxc.rootfs.path' => 'lxc.rootfs.path is auto generated from rootfs',
509 'lxc.rootfs.mount' => 1,
510 'lxc.rootfs.options' => 'lxc.rootfs.options is not supported' .
511 ', please use mount point options in the "rootfs" key',
512 # lxc.cgroup.*
513 # lxc.prlimit.*
514 # lxc.net.*
515 'lxc.cap.drop' => 1,
516 'lxc.cap.keep' => 1,
517 'lxc.seccomp.profile' => 1,
518 'lxc.seccomp.notify.proxy' => 1,
519 'lxc.seccomp.notify.cookie' => 1,
520 'lxc.idmap' => 1,
521 'lxc.hook.pre-start' => 1,
522 'lxc.hook.pre-mount' => 1,
523 'lxc.hook.mount' => 1,
524 'lxc.hook.start' => 1,
525 'lxc.hook.stop' => 1,
526 'lxc.hook.post-stop' => 1,
527 'lxc.hook.clone' => 1,
528 'lxc.hook.destroy' => 1,
529 'lxc.hook.version' => 1,
530 'lxc.log.level' => 1,
531 'lxc.log.file' => 1,
532 'lxc.start.auto' => 1,
533 'lxc.start.delay' => 1,
534 'lxc.start.order' => 1,
535 'lxc.group' => 1,
536 'lxc.environment' => 1,
537
538 # All these are namespaced via CLONE_NEWIPC (see namespaces(7)).
539 'lxc.sysctl.fs.mqueue' => 1,
540 'lxc.sysctl.kernel.msgmax' => 1,
541 'lxc.sysctl.kernel.msgmnb' => 1,
542 'lxc.sysctl.kernel.msgmni' => 1,
543 'lxc.sysctl.kernel.sem' => 1,
544 'lxc.sysctl.kernel.shmall' => 1,
545 'lxc.sysctl.kernel.shmmax' => 1,
546 'lxc.sysctl.kernel.shmmni' => 1,
547 'lxc.sysctl.kernel.shm_rmid_forced' => 1,
548 };
549
550 my $deprecated_lxc_conf_keys = {
551 # Deprecated (removed with lxc 3.0):
552 'lxc.aa_profile' => 'lxc.apparmor.profile',
553 'lxc.aa_allow_incomplete' => 'lxc.apparmor.allow_incomplete',
554 'lxc.console' => 'lxc.console.path',
555 'lxc.devttydir' => 'lxc.tty.dir',
556 'lxc.haltsignal' => 'lxc.signal.halt',
557 'lxc.rebootsignal' => 'lxc.signal.reboot',
558 'lxc.stopsignal' => 'lxc.signal.stop',
559 'lxc.id_map' => 'lxc.idmap',
560 'lxc.init_cmd' => 'lxc.init.cmd',
561 'lxc.loglevel' => 'lxc.log.level',
562 'lxc.logfile' => 'lxc.log.file',
563 'lxc.mount' => 'lxc.mount.fstab',
564 'lxc.network.type' => 'lxc.net.INDEX.type',
565 'lxc.network.flags' => 'lxc.net.INDEX.flags',
566 'lxc.network.link' => 'lxc.net.INDEX.link',
567 'lxc.network.mtu' => 'lxc.net.INDEX.mtu',
568 'lxc.network.name' => 'lxc.net.INDEX.name',
569 'lxc.network.hwaddr' => 'lxc.net.INDEX.hwaddr',
570 'lxc.network.ipv4' => 'lxc.net.INDEX.ipv4.address',
571 'lxc.network.ipv4.gateway' => 'lxc.net.INDEX.ipv4.gateway',
572 'lxc.network.ipv6' => 'lxc.net.INDEX.ipv6.address',
573 'lxc.network.ipv6.gateway' => 'lxc.net.INDEX.ipv6.gateway',
574 'lxc.network.script.up' => 'lxc.net.INDEX.script.up',
575 'lxc.network.script.down' => 'lxc.net.INDEX.script.down',
576 'lxc.pts' => 'lxc.pty.max',
577 'lxc.se_context' => 'lxc.selinux.context',
578 'lxc.seccomp' => 'lxc.seccomp.profile',
579 'lxc.tty' => 'lxc.tty.max',
580 'lxc.utsname' => 'lxc.uts.name',
581 };
582
583 sub is_valid_lxc_conf_key {
584 my ($vmid, $key) = @_;
585 if ($key =~ /^lxc\.limit\./) {
586 warn "vm $vmid - $key: lxc.limit.* was renamed to lxc.prlimit.*\n";
587 return 1;
588 }
589 if (defined(my $new_name = $deprecated_lxc_conf_keys->{$key})) {
590 warn "vm $vmid - $key is deprecated and was renamed to $new_name\n";
591 return 1;
592 }
593 my $validity = $valid_lxc_conf_keys->{$key};
594 return $validity if defined($validity);
595 return 1 if $key =~ /^lxc\.cgroup\./ # allow all cgroup values
596 || $key =~ /^lxc\.prlimit\./ # allow all prlimits
597 || $key =~ /^lxc\.net\./; # allow custom network definitions
598 return 0;
599 }
600
601 our $netconf_desc = {
602 type => {
603 type => 'string',
604 optional => 1,
605 description => "Network interface type.",
606 enum => [qw(veth)],
607 },
608 name => {
609 type => 'string',
610 format_description => 'string',
611 description => 'Name of the network device as seen from inside the container. (lxc.network.name)',
612 pattern => '[-_.\w\d]+',
613 },
614 bridge => {
615 type => 'string',
616 format_description => 'bridge',
617 description => 'Bridge to attach the network device to.',
618 pattern => '[-_.\w\d]+',
619 optional => 1,
620 },
621 hwaddr => get_standard_option('mac-addr', {
622 description => 'The interface MAC address. This is dynamically allocated by default, but you can set that statically if needed, for example to always have the same link-local IPv6 address. (lxc.network.hwaddr)',
623 }),
624 mtu => {
625 type => 'integer',
626 description => 'Maximum transfer unit of the interface. (lxc.network.mtu)',
627 minimum => 64, # minimum ethernet frame is 64 bytes
628 optional => 1,
629 },
630 ip => {
631 type => 'string',
632 format => 'pve-ipv4-config',
633 format_description => '(IPv4/CIDR|dhcp|manual)',
634 description => 'IPv4 address in CIDR format.',
635 optional => 1,
636 },
637 gw => {
638 type => 'string',
639 format => 'ipv4',
640 format_description => 'GatewayIPv4',
641 description => 'Default gateway for IPv4 traffic.',
642 optional => 1,
643 },
644 ip6 => {
645 type => 'string',
646 format => 'pve-ipv6-config',
647 format_description => '(IPv6/CIDR|auto|dhcp|manual)',
648 description => 'IPv6 address in CIDR format.',
649 optional => 1,
650 },
651 gw6 => {
652 type => 'string',
653 format => 'ipv6',
654 format_description => 'GatewayIPv6',
655 description => 'Default gateway for IPv6 traffic.',
656 optional => 1,
657 },
658 firewall => {
659 type => 'boolean',
660 description => "Controls whether this interface's firewall rules should be used.",
661 optional => 1,
662 },
663 tag => {
664 type => 'integer',
665 minimum => 1,
666 maximum => 4094,
667 description => "VLAN tag for this interface.",
668 optional => 1,
669 },
670 trunks => {
671 type => 'string',
672 pattern => qr/\d+(?:;\d+)*/,
673 format_description => 'vlanid[;vlanid...]',
674 description => "VLAN ids to pass through the interface",
675 optional => 1,
676 },
677 rate => {
678 type => 'number',
679 format_description => 'mbps',
680 description => "Apply rate limiting to the interface",
681 optional => 1,
682 },
683 };
684 PVE::JSONSchema::register_format('pve-lxc-network', $netconf_desc);
685
686 my $MAX_LXC_NETWORKS = 10;
687 for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) {
688 $confdesc->{"net$i"} = {
689 optional => 1,
690 type => 'string', format => $netconf_desc,
691 description => "Specifies network interfaces for the container.",
692 };
693 }
694
695 PVE::JSONSchema::register_format('pve-lxc-mp-string', \&verify_lxc_mp_string);
696 sub verify_lxc_mp_string {
697 my ($mp, $noerr) = @_;
698
699 # do not allow:
700 # /./ or /../
701 # /. or /.. at the end
702 # ../ at the beginning
703
704 if($mp =~ m@/\.\.?/@ ||
705 $mp =~ m@/\.\.?$@ ||
706 $mp =~ m@^\.\./@) {
707 return undef if $noerr;
708 die "$mp contains illegal character sequences\n";
709 }
710 return $mp;
711 }
712
713 my $mp_desc = {
714 %$rootfs_desc,
715 backup => {
716 type => 'boolean',
717 description => 'Whether to include the mount point in backups.',
718 verbose_description => 'Whether to include the mount point in backups '.
719 '(only used for volume mount points).',
720 optional => 1,
721 },
722 mp => {
723 type => 'string',
724 format => 'pve-lxc-mp-string',
725 format_description => 'Path',
726 description => 'Path to the mount point as seen from inside the container '.
727 '(must not contain symlinks).',
728 verbose_description => "Path to the mount point as seen from inside the container.\n\n".
729 "NOTE: Must not contain any symlinks for security reasons."
730 },
731 };
732 PVE::JSONSchema::register_format('pve-ct-mountpoint', $mp_desc);
733
734 my $unuseddesc = {
735 optional => 1,
736 type => 'string', format => 'pve-volume-id',
737 description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
738 };
739
740 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
741 $confdesc->{"mp$i"} = {
742 optional => 1,
743 type => 'string', format => $mp_desc,
744 description => "Use volume as container mount point.",
745 optional => 1,
746 };
747 }
748
749 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
750 $confdesc->{"unused$i"} = $unuseddesc;
751 }
752
753 sub parse_pct_config {
754 my ($filename, $raw) = @_;
755
756 return undef if !defined($raw);
757
758 my $res = {
759 digest => Digest::SHA::sha1_hex($raw),
760 snapshots => {},
761 pending => {},
762 };
763
764 $filename =~ m|/lxc/(\d+).conf$|
765 || die "got strange filename '$filename'";
766
767 my $vmid = $1;
768
769 my $conf = $res;
770 my $descr = '';
771 my $section = '';
772
773 my @lines = split(/\n/, $raw);
774 foreach my $line (@lines) {
775 next if $line =~ m/^\s*$/;
776
777 if ($line =~ m/^\[pve:pending\]\s*$/i) {
778 $section = 'pending';
779 $conf->{description} = $descr if $descr;
780 $descr = '';
781 $conf = $res->{$section} = {};
782 next;
783 } elsif ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
784 $section = $1;
785 $conf->{description} = $descr if $descr;
786 $descr = '';
787 $conf = $res->{snapshots}->{$section} = {};
788 next;
789 }
790
791 if ($line =~ m/^\#(.*)\s*$/) {
792 $descr .= PVE::Tools::decode_text($1) . "\n";
793 next;
794 }
795
796 if ($line =~ m/^(lxc\.[a-z0-9_\-\.]+)(:|\s*=)\s*(.*?)\s*$/) {
797 my $key = $1;
798 my $value = $3;
799 my $validity = is_valid_lxc_conf_key($vmid, $key);
800 if ($validity eq 1) {
801 push @{$conf->{lxc}}, [$key, $value];
802 } elsif (my $errmsg = $validity) {
803 warn "vm $vmid - $key: $errmsg\n";
804 } else {
805 warn "vm $vmid - unable to parse config: $line\n";
806 }
807 } elsif ($line =~ m/^(description):\s*(.*\S)\s*$/) {
808 $descr .= PVE::Tools::decode_text($2);
809 } elsif ($line =~ m/snapstate:\s*(prepare|delete)\s*$/) {
810 $conf->{snapstate} = $1;
811 } elsif ($line =~ m/^delete:\s*(.*\S)\s*$/) {
812 my $value = $1;
813 if ($section eq 'pending') {
814 $conf->{delete} = $value;
815 } else {
816 warn "vm $vmid - property 'delete' is only allowed in [pve:pending]\n";
817 }
818 } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(\S.*)\s*$/) {
819 my $key = $1;
820 my $value = $2;
821 eval { $value = PVE::LXC::Config->check_type($key, $value); };
822 warn "vm $vmid - unable to parse value of '$key' - $@" if $@;
823 $conf->{$key} = $value;
824 } else {
825 warn "vm $vmid - unable to parse config: $line\n";
826 }
827 }
828
829 $conf->{description} = $descr if $descr;
830
831 delete $res->{snapstate}; # just to be sure
832
833 return $res;
834 }
835
836 sub write_pct_config {
837 my ($filename, $conf) = @_;
838
839 delete $conf->{snapstate}; # just to be sure
840
841 my $volidlist = PVE::LXC::Config->get_vm_volumes($conf);
842 my $used_volids = {};
843 foreach my $vid (@$volidlist) {
844 $used_volids->{$vid} = 1;
845 }
846
847 # remove 'unusedX' settings if the volume is still used
848 foreach my $key (keys %$conf) {
849 my $value = $conf->{$key};
850 if ($key =~ m/^unused/ && $used_volids->{$value}) {
851 delete $conf->{$key};
852 }
853 }
854
855 my $generate_raw_config = sub {
856 my ($conf) = @_;
857
858 my $raw = '';
859
860 # add description as comment to top of file
861 my $descr = $conf->{description} || '';
862 foreach my $cl (split(/\n/, $descr)) {
863 $raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
864 }
865
866 foreach my $key (sort keys %$conf) {
867 next if $key eq 'digest' || $key eq 'description' ||
868 $key eq 'pending' || $key eq 'snapshots' ||
869 $key eq 'snapname' || $key eq 'lxc';
870 my $value = $conf->{$key};
871 die "detected invalid newline inside property '$key'\n"
872 if $value =~ m/\n/;
873 $raw .= "$key: $value\n";
874 }
875
876 if (my $lxcconf = $conf->{lxc}) {
877 foreach my $entry (@$lxcconf) {
878 my ($k, $v) = @$entry;
879 $raw .= "$k: $v\n";
880 }
881 }
882
883 return $raw;
884 };
885
886 my $raw = &$generate_raw_config($conf);
887
888 if (scalar(keys %{$conf->{pending}})){
889 $raw .= "\n[pve:pending]\n";
890 $raw .= &$generate_raw_config($conf->{pending});
891 }
892
893 foreach my $snapname (sort keys %{$conf->{snapshots}}) {
894 $raw .= "\n[$snapname]\n";
895 $raw .= &$generate_raw_config($conf->{snapshots}->{$snapname});
896 }
897
898 return $raw;
899 }
900
901 sub update_pct_config {
902 my ($class, $vmid, $conf, $running, $param, $delete, $revert) = @_;
903
904 my $storage_cfg = PVE::Storage::config();
905
906 foreach my $opt (@$revert) {
907 delete $conf->{pending}->{$opt};
908 $class->remove_from_pending_delete($conf, $opt); # also remove from deletion queue
909 }
910
911 # write updates to pending section
912 my $modified = {}; # record modified options
913
914 foreach my $opt (@$delete) {
915 if (!defined($conf->{$opt}) && !defined($conf->{pending}->{$opt})) {
916 warn "cannot delete '$opt' - not set in current configuration!\n";
917 next;
918 }
919 $modified->{$opt} = 1;
920 if ($opt eq 'memory' || $opt eq 'rootfs' || $opt eq 'ostype') {
921 die "unable to delete required option '$opt'\n";
922 } elsif ($opt =~ m/^unused(\d+)$/) {
923 $class->check_protection($conf, "can't remove CT $vmid drive '$opt'");
924 } elsif ($opt =~ m/^mp(\d+)$/) {
925 $class->check_protection($conf, "can't remove CT $vmid drive '$opt'");
926 } elsif ($opt eq 'unprivileged') {
927 die "unable to delete read-only option: '$opt'\n";
928 }
929 $class->add_to_pending_delete($conf, $opt);
930 }
931
932 my $check_content_type = sub {
933 my ($mp) = @_;
934 my $sid = PVE::Storage::parse_volume_id($mp->{volume});
935 my $storage_config = PVE::Storage::storage_config($storage_cfg, $sid);
936 die "storage '$sid' does not allow content type 'rootdir' (Container)\n"
937 if !$storage_config->{content}->{rootdir};
938 };
939
940 foreach my $opt (sort keys %$param) { # add/change
941 $modified->{$opt} = 1;
942 my $value = $param->{$opt};
943 if ($opt =~ m/^mp(\d+)$/ || $opt eq 'rootfs') {
944 $class->check_protection($conf, "can't update CT $vmid drive '$opt'");
945 my $mp = $opt eq 'rootfs' ? $class->parse_ct_rootfs($value) : $class->parse_ct_mountpoint($value);
946 $check_content_type->($mp) if ($mp->{type} eq 'volume');
947 } elsif ($opt eq 'hookscript') {
948 PVE::GuestHelpers::check_hookscript($value);
949 } elsif ($opt eq 'nameserver') {
950 $value = PVE::LXC::verify_nameserver_list($value);
951 } elsif ($opt eq 'searchdomain') {
952 $value = PVE::LXC::verify_searchdomain_list($value);
953 } elsif ($opt eq 'unprivileged') {
954 die "unable to modify read-only option: '$opt'\n";
955 }
956 $conf->{pending}->{$opt} = $value;
957 $class->remove_from_pending_delete($conf, $opt);
958 }
959
960 my $changes = $class->cleanup_pending($conf);
961
962 my $errors = {};
963 if ($running) {
964 $class->vmconfig_hotplug_pending($vmid, $conf, $storage_cfg, $modified, $errors);
965 } else {
966 $class->vmconfig_apply_pending($vmid, $conf, $storage_cfg, $modified, $errors);
967 }
968
969 return $errors;
970 }
971
972 sub check_type {
973 my ($class, $key, $value) = @_;
974
975 die "unknown setting '$key'\n" if !$confdesc->{$key};
976
977 my $type = $confdesc->{$key}->{type};
978
979 if (!defined($value)) {
980 die "got undefined value\n";
981 }
982
983 if ($value =~ m/[\n\r]/) {
984 die "property contains a line feed\n";
985 }
986
987 if ($type eq 'boolean') {
988 return 1 if ($value eq '1') || ($value =~ m/^(on|yes|true)$/i);
989 return 0 if ($value eq '0') || ($value =~ m/^(off|no|false)$/i);
990 die "type check ('boolean') failed - got '$value'\n";
991 } elsif ($type eq 'integer') {
992 return int($1) if $value =~ m/^(\d+)$/;
993 die "type check ('integer') failed - got '$value'\n";
994 } elsif ($type eq 'number') {
995 return $value if $value =~ m/^(\d+)(\.\d+)?$/;
996 die "type check ('number') failed - got '$value'\n";
997 } elsif ($type eq 'string') {
998 if (my $fmt = $confdesc->{$key}->{format}) {
999 PVE::JSONSchema::check_format($fmt, $value);
1000 return $value;
1001 }
1002 return $value;
1003 } else {
1004 die "internal error"
1005 }
1006 }
1007
1008
1009 # add JSON properties for create and set function
1010 sub json_config_properties {
1011 my ($class, $prop) = @_;
1012
1013 foreach my $opt (keys %$confdesc) {
1014 next if $opt eq 'parent' || $opt eq 'snaptime';
1015 next if $prop->{$opt};
1016 $prop->{$opt} = $confdesc->{$opt};
1017 }
1018
1019 return $prop;
1020 }
1021
1022 sub __parse_ct_mountpoint_full {
1023 my ($class, $desc, $data, $noerr) = @_;
1024
1025 $data //= '';
1026
1027 my $res;
1028 eval { $res = PVE::JSONSchema::parse_property_string($desc, $data) };
1029 if ($@) {
1030 return undef if $noerr;
1031 die $@;
1032 }
1033
1034 if (defined(my $size = $res->{size})) {
1035 $size = PVE::JSONSchema::parse_size($size);
1036 if (!defined($size)) {
1037 return undef if $noerr;
1038 die "invalid size: $size\n";
1039 }
1040 $res->{size} = $size;
1041 }
1042
1043 $res->{type} = $class->classify_mountpoint($res->{volume});
1044
1045 return $res;
1046 };
1047
1048 sub parse_ct_rootfs {
1049 my ($class, $data, $noerr) = @_;
1050
1051 my $res = $class->__parse_ct_mountpoint_full($rootfs_desc, $data, $noerr);
1052
1053 $res->{mp} = '/' if defined($res);
1054
1055 return $res;
1056 }
1057
1058 sub parse_ct_mountpoint {
1059 my ($class, $data, $noerr) = @_;
1060
1061 return $class->__parse_ct_mountpoint_full($mp_desc, $data, $noerr);
1062 }
1063
1064 sub print_ct_mountpoint {
1065 my ($class, $info, $nomp) = @_;
1066 my $skip = [ 'type' ];
1067 push @$skip, 'mp' if $nomp;
1068 return PVE::JSONSchema::print_property_string($info, $mp_desc, $skip);
1069 }
1070
1071 sub print_lxc_network {
1072 my ($class, $net) = @_;
1073 return PVE::JSONSchema::print_property_string($net, $netconf_desc);
1074 }
1075
1076 sub parse_lxc_network {
1077 my ($class, $data) = @_;
1078
1079 my $res = {};
1080
1081 return $res if !$data;
1082
1083 $res = PVE::JSONSchema::parse_property_string($netconf_desc, $data);
1084
1085 $res->{type} = 'veth';
1086 if (!$res->{hwaddr}) {
1087 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
1088 $res->{hwaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
1089 }
1090
1091 return $res;
1092 }
1093
1094 sub parse_features {
1095 my ($class, $data) = @_;
1096 return {} if !$data;
1097 return PVE::JSONSchema::parse_property_string($features_desc, $data);
1098 }
1099
1100 sub option_exists {
1101 my ($class, $name) = @_;
1102
1103 return defined($confdesc->{$name});
1104 }
1105 # END JSON config code
1106
1107 my $LXC_FASTPLUG_OPTIONS= {
1108 'description' => 1,
1109 'onboot' => 1,
1110 'startup' => 1,
1111 'protection' => 1,
1112 'hostname' => 1,
1113 'hookscript' => 1,
1114 'cores' => 1,
1115 'tags' => 1,
1116 'lock' => 1,
1117 };
1118
1119 sub vmconfig_hotplug_pending {
1120 my ($class, $vmid, $conf, $storecfg, $selection, $errors) = @_;
1121
1122 my $pid = PVE::LXC::find_lxc_pid($vmid);
1123 my $rootdir = "/proc/$pid/root";
1124
1125 my $add_hotplug_error = sub {
1126 my ($opt, $msg) = @_;
1127 $errors->{$opt} = "unable to hotplug $opt: $msg";
1128 };
1129
1130 my $changes;
1131 foreach my $opt (sort keys %{$conf->{pending}}) { # add/change
1132 next if $selection && !$selection->{$opt};
1133 if ($LXC_FASTPLUG_OPTIONS->{$opt}) {
1134 $conf->{$opt} = delete $conf->{pending}->{$opt};
1135 $changes = 1;
1136 }
1137 }
1138
1139 if ($changes) {
1140 $class->write_config($vmid, $conf);
1141 }
1142
1143 # There's no separate swap size to configure, there's memory and "total"
1144 # memory (iow. memory+swap). This means we have to change them together.
1145 my $hotplug_memory_done;
1146 my $hotplug_memory = sub {
1147 my ($wanted_memory, $wanted_swap) = @_;
1148 my $old_memory = ($conf->{memory} || $confdesc->{memory}->{default});
1149 my $old_swap = ($conf->{swap} || $confdesc->{swap}->{default});
1150
1151 $wanted_memory //= $old_memory;
1152 $wanted_swap //= $old_swap;
1153
1154 my $total = $wanted_memory + $wanted_swap;
1155 my $old_total = $old_memory + $old_swap;
1156
1157 if ($total > $old_total) {
1158 PVE::LXC::write_cgroup_value("memory", $vmid,
1159 "memory.memsw.limit_in_bytes",
1160 int($total*1024*1024));
1161 PVE::LXC::write_cgroup_value("memory", $vmid,
1162 "memory.limit_in_bytes",
1163 int($wanted_memory*1024*1024));
1164 } else {
1165 PVE::LXC::write_cgroup_value("memory", $vmid,
1166 "memory.limit_in_bytes",
1167 int($wanted_memory*1024*1024));
1168 PVE::LXC::write_cgroup_value("memory", $vmid,
1169 "memory.memsw.limit_in_bytes",
1170 int($total*1024*1024));
1171 }
1172 $hotplug_memory_done = 1;
1173 };
1174
1175 my $pending_delete_hash = $class->parse_pending_delete($conf->{pending}->{delete});
1176 # FIXME: $force deletion is not implemented for CTs
1177 foreach my $opt (sort keys %$pending_delete_hash) {
1178 next if $selection && !$selection->{$opt};
1179 eval {
1180 if ($LXC_FASTPLUG_OPTIONS->{$opt}) {
1181 # pass
1182 } elsif ($opt =~ m/^unused(\d+)$/) {
1183 PVE::LXC::delete_mountpoint_volume($storecfg, $vmid, $conf->{$opt})
1184 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1185 } elsif ($opt eq 'swap') {
1186 $hotplug_memory->(undef, 0);
1187 } elsif ($opt eq 'cpulimit') {
1188 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_period_us", -1);
1189 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", -1);
1190 } elsif ($opt eq 'cpuunits') {
1191 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $confdesc->{cpuunits}->{default});
1192 } elsif ($opt =~ m/^net(\d)$/) {
1193 my $netid = $1;
1194 PVE::Network::veth_delete("veth${vmid}i$netid");
1195 } else {
1196 die "skip\n"; # skip non-hotpluggable opts
1197 }
1198 };
1199 if (my $err = $@) {
1200 $add_hotplug_error->($opt, $err) if $err ne "skip\n";
1201 } else {
1202 delete $conf->{$opt};
1203 $class->remove_from_pending_delete($conf, $opt);
1204 }
1205 }
1206
1207 foreach my $opt (sort keys %{$conf->{pending}}) {
1208 next if $opt eq 'delete'; # just to be sure
1209 next if $selection && !$selection->{$opt};
1210 my $value = $conf->{pending}->{$opt};
1211 eval {
1212 if ($opt eq 'cpulimit') {
1213 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_period_us", 100000);
1214 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", int(100000*$value));
1215 } elsif ($opt eq 'cpuunits') {
1216 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $value);
1217 } elsif ($opt =~ m/^net(\d+)$/) {
1218 my $netid = $1;
1219 my $net = $class->parse_lxc_network($value);
1220 $value = $class->print_lxc_network($net);
1221 PVE::LXC::update_net($vmid, $conf, $opt, $net, $netid, $rootdir);
1222 } elsif ($opt eq 'memory' || $opt eq 'swap') {
1223 if (!$hotplug_memory_done) { # don't call twice if both opts are passed
1224 $hotplug_memory->($conf->{pending}->{memory}, $conf->{pending}->{swap});
1225 }
1226 } elsif ($opt =~ m/^mp(\d+)$/) {
1227 if (!PVE::LXC::Tools::can_use_new_mount_api()) {
1228 die "skip\n";
1229 }
1230
1231 $class->apply_pending_mountpoint($vmid, $conf, $opt, $storecfg, 1);
1232 # apply_pending_mountpoint modifies the value if it creates a new disk
1233 $value = $conf->{pending}->{$opt};
1234 } else {
1235 die "skip\n"; # skip non-hotpluggable
1236 }
1237 };
1238 if (my $err = $@) {
1239 $add_hotplug_error->($opt, $err) if $err ne "skip\n";
1240 } else {
1241 $conf->{$opt} = $value;
1242 delete $conf->{pending}->{$opt};
1243 }
1244 }
1245
1246 $class->write_config($vmid, $conf);
1247 }
1248
1249 sub vmconfig_apply_pending {
1250 my ($class, $vmid, $conf, $storecfg, $selection, $errors) = @_;
1251
1252 my $add_apply_error = sub {
1253 my ($opt, $msg) = @_;
1254 my $err_msg = "unable to apply pending change $opt : $msg";
1255 $errors->{$opt} = $err_msg;
1256 warn $err_msg;
1257 };
1258
1259 my $pending_delete_hash = $class->parse_pending_delete($conf->{pending}->{delete});
1260 # FIXME: $force deletion is not implemented for CTs
1261 foreach my $opt (sort keys %$pending_delete_hash) {
1262 next if $selection && !$selection->{$opt};
1263 $class->cleanup_pending($conf);
1264 eval {
1265 if ($opt =~ m/^mp(\d+)$/) {
1266 my $mp = $class->parse_ct_mountpoint($conf->{$opt});
1267 if ($mp->{type} eq 'volume') {
1268 $class->add_unused_volume($conf, $mp->{volume})
1269 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1270 }
1271 } elsif ($opt =~ m/^unused(\d+)$/) {
1272 PVE::LXC::delete_mountpoint_volume($storecfg, $vmid, $conf->{$opt})
1273 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1274 }
1275 };
1276 if (my $err = $@) {
1277 $add_apply_error->($opt, $err);
1278 } else {
1279 delete $conf->{$opt};
1280 $class->remove_from_pending_delete($conf, $opt);
1281 }
1282 }
1283
1284 foreach my $opt (sort keys %{$conf->{pending}}) { # add/change
1285 next if $opt eq 'delete'; # just to be sure
1286 next if $selection && !$selection->{$opt};
1287 eval {
1288 if ($opt =~ m/^mp(\d+)$/) {
1289 $class->apply_pending_mountpoint($vmid, $conf, $opt, $storecfg, 0);
1290 } elsif ($opt =~ m/^net(\d+)$/) {
1291 my $netid = $1;
1292 my $net = $class->parse_lxc_network($conf->{pending}->{$opt});
1293 $conf->{pending}->{$opt} = $class->print_lxc_network($net);
1294 }
1295 };
1296 if (my $err = $@) {
1297 $add_apply_error->($opt, $err);
1298 } else {
1299 $class->cleanup_pending($conf);
1300 $conf->{$opt} = delete $conf->{pending}->{$opt};
1301 }
1302 }
1303
1304 $class->write_config($vmid, $conf);
1305 }
1306
1307 my $rescan_volume = sub {
1308 my ($storecfg, $mp) = @_;
1309 eval {
1310 $mp->{size} = PVE::Storage::volume_size_info($storecfg, $mp->{volume}, 5);
1311 };
1312 warn "Could not rescan volume size - $@\n" if $@;
1313 };
1314
1315 sub apply_pending_mountpoint {
1316 my ($class, $vmid, $conf, $opt, $storecfg, $running) = @_;
1317
1318 my $mp = $class->parse_ct_mountpoint($conf->{pending}->{$opt});
1319 my $old = $conf->{$opt};
1320 if ($mp->{type} eq 'volume') {
1321 if ($mp->{volume} =~ $PVE::LXC::NEW_DISK_RE) {
1322 my $original_value = $conf->{pending}->{$opt};
1323 my $vollist = PVE::LXC::create_disks(
1324 $storecfg,
1325 $vmid,
1326 { $opt => $original_value },
1327 $conf,
1328 1,
1329 );
1330 if ($running) {
1331 # Re-parse mount point:
1332 my $mp = $class->parse_ct_mountpoint($conf->{pending}->{$opt});
1333 eval {
1334 PVE::LXC::mountpoint_hotplug($vmid, $conf, $opt, $mp, $storecfg);
1335 };
1336 my $err = $@;
1337 if ($err) {
1338 PVE::LXC::destroy_disks($storecfg, $vollist);
1339 # The pending-changes code collects errors but keeps on looping through further
1340 # pending changes, so unroll the change in $conf as well if destroy_disks()
1341 # didn't die().
1342 $conf->{pending}->{$opt} = $original_value;
1343 die $err;
1344 }
1345 }
1346 } else {
1347 die "skip\n" if $running && defined($old); # TODO: "changing" mount points?
1348 $rescan_volume->($storecfg, $mp);
1349 if ($running) {
1350 PVE::LXC::mountpoint_hotplug($vmid, $conf, $opt, $mp, $storecfg);
1351 }
1352 $conf->{pending}->{$opt} = $class->print_ct_mountpoint($mp);
1353 }
1354 }
1355
1356 if (defined($old)) {
1357 my $mp = $class->parse_ct_mountpoint($old);
1358 if ($mp->{type} eq 'volume') {
1359 $class->add_unused_volume($conf, $mp->{volume})
1360 if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
1361 }
1362 }
1363 }
1364
1365 sub classify_mountpoint {
1366 my ($class, $vol) = @_;
1367 if ($vol =~ m!^/!) {
1368 return 'device' if $vol =~ m!^/dev/!;
1369 return 'bind';
1370 }
1371 return 'volume';
1372 }
1373
1374 my $__is_volume_in_use = sub {
1375 my ($class, $config, $volid) = @_;
1376 my $used = 0;
1377
1378 $class->foreach_mountpoint($config, sub {
1379 my ($ms, $mountpoint) = @_;
1380 return if $used;
1381 $used = $mountpoint->{type} eq 'volume' && $mountpoint->{volume} eq $volid;
1382 });
1383
1384 return $used;
1385 };
1386
1387 sub is_volume_in_use_by_snapshots {
1388 my ($class, $config, $volid) = @_;
1389
1390 if (my $snapshots = $config->{snapshots}) {
1391 foreach my $snap (keys %$snapshots) {
1392 return 1 if $__is_volume_in_use->($class, $snapshots->{$snap}, $volid);
1393 }
1394 }
1395
1396 return 0;
1397 }
1398
1399 sub is_volume_in_use {
1400 my ($class, $config, $volid, $include_snapshots, $include_pending) = @_;
1401 return 1 if $__is_volume_in_use->($class, $config, $volid);
1402 return 1 if $include_snapshots && $class->is_volume_in_use_by_snapshots($config, $volid);
1403 return 1 if $include_pending && $__is_volume_in_use->($class, $config->{pending}, $volid);
1404 return 0;
1405 }
1406
1407 sub has_dev_console {
1408 my ($class, $conf) = @_;
1409
1410 return !(defined($conf->{console}) && !$conf->{console});
1411 }
1412
1413 sub has_lxc_entry {
1414 my ($class, $conf, $keyname) = @_;
1415
1416 if (my $lxcconf = $conf->{lxc}) {
1417 foreach my $entry (@$lxcconf) {
1418 my ($key, undef) = @$entry;
1419 return 1 if $key eq $keyname;
1420 }
1421 }
1422
1423 return 0;
1424 }
1425
1426 sub get_tty_count {
1427 my ($class, $conf) = @_;
1428
1429 return $conf->{tty} // $confdesc->{tty}->{default};
1430 }
1431
1432 sub get_cmode {
1433 my ($class, $conf) = @_;
1434
1435 return $conf->{cmode} // $confdesc->{cmode}->{default};
1436 }
1437
1438 sub mountpoint_names {
1439 my ($class, $reverse) = @_;
1440
1441 my @names = ('rootfs');
1442
1443 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
1444 push @names, "mp$i";
1445 }
1446
1447 return $reverse ? reverse @names : @names;
1448 }
1449
1450 sub foreach_mountpoint_full {
1451 my ($class, $conf, $reverse, $func, @param) = @_;
1452
1453 my $mps = [ grep { defined($conf->{$_}) } $class->mountpoint_names($reverse) ];
1454 foreach my $key (@$mps) {
1455 my $value = $conf->{$key};
1456 my $mountpoint = $key eq 'rootfs' ? $class->parse_ct_rootfs($value, 1) : $class->parse_ct_mountpoint($value, 1);
1457 next if !defined($mountpoint);
1458
1459 &$func($key, $mountpoint, @param);
1460 }
1461 }
1462
1463 sub foreach_mountpoint {
1464 my ($class, $conf, $func, @param) = @_;
1465
1466 $class->foreach_mountpoint_full($conf, 0, $func, @param);
1467 }
1468
1469 sub foreach_mountpoint_reverse {
1470 my ($class, $conf, $func, @param) = @_;
1471
1472 $class->foreach_mountpoint_full($conf, 1, $func, @param);
1473 }
1474
1475 sub get_vm_volumes {
1476 my ($class, $conf, $excludes) = @_;
1477
1478 my $vollist = [];
1479
1480 $class->foreach_mountpoint($conf, sub {
1481 my ($ms, $mountpoint) = @_;
1482
1483 return if $excludes && $ms eq $excludes;
1484
1485 my $volid = $mountpoint->{volume};
1486 return if !$volid || $mountpoint->{type} ne 'volume';
1487
1488 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
1489 return if !$sid;
1490
1491 push @$vollist, $volid;
1492 });
1493
1494 return $vollist;
1495 }
1496
1497 sub get_replicatable_volumes {
1498 my ($class, $storecfg, $vmid, $conf, $cleanup, $noerr) = @_;
1499
1500 my $volhash = {};
1501
1502 my $test_volid = sub {
1503 my ($volid, $mountpoint) = @_;
1504
1505 return if !$volid;
1506
1507 my $mptype = $mountpoint->{type};
1508 my $replicate = $mountpoint->{replicate} // 1;
1509
1510 if ($mptype ne 'volume') {
1511 # skip bindmounts if replicate = 0 even for cleanup,
1512 # since bind mounts could not have been replicated ever
1513 return if !$replicate;
1514 die "unable to replicate mountpoint type '$mptype'\n";
1515 }
1516
1517 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, $noerr);
1518 return if !$storeid;
1519
1520 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
1521 return if $scfg->{shared};
1522
1523 my ($path, $owner, $vtype) = PVE::Storage::path($storecfg, $volid);
1524 return if !$owner || ($owner != $vmid);
1525
1526 die "unable to replicate volume '$volid', type '$vtype'\n" if $vtype ne 'images';
1527
1528 return if !$cleanup && !$replicate;
1529
1530 if (!PVE::Storage::volume_has_feature($storecfg, 'replicate', $volid)) {
1531 return if $cleanup || $noerr;
1532 die "missing replicate feature on volume '$volid'\n";
1533 }
1534
1535 $volhash->{$volid} = 1;
1536 };
1537
1538 $class->foreach_mountpoint($conf, sub {
1539 my ($ms, $mountpoint) = @_;
1540 $test_volid->($mountpoint->{volume}, $mountpoint);
1541 });
1542
1543 foreach my $snapname (keys %{$conf->{snapshots}}) {
1544 my $snap = $conf->{snapshots}->{$snapname};
1545 $class->foreach_mountpoint($snap, sub {
1546 my ($ms, $mountpoint) = @_;
1547 $test_volid->($mountpoint->{volume}, $mountpoint);
1548 });
1549 }
1550
1551 # add 'unusedX' volumes to volhash
1552 foreach my $key (keys %$conf) {
1553 if ($key =~ m/^unused/) {
1554 $test_volid->($conf->{$key}, { type => 'volume', replicate => 1 });
1555 }
1556 }
1557
1558 return $volhash;
1559 }
1560
1561 1;