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