]> git.proxmox.com Git - pve-container.git/blob - src/PVE/LXC/Config.pm
detect_architecture: use ELF machine header to detect ISA
[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 $confdesc = {
276 lock => {
277 optional => 1,
278 type => 'string',
279 description => "Lock/unlock the VM.",
280 enum => [qw(backup disk migrate mounted rollback snapshot snapshot-delete)],
281 },
282 onboot => {
283 optional => 1,
284 type => 'boolean',
285 description => "Specifies whether a VM will be started during system bootup.",
286 default => 0,
287 },
288 startup => get_standard_option('pve-startup-order'),
289 template => {
290 optional => 1,
291 type => 'boolean',
292 description => "Enable/disable Template.",
293 default => 0,
294 },
295 arch => {
296 optional => 1,
297 type => 'string',
298 enum => ['amd64', 'i386', 'arm64', 'armhf'],
299 description => "OS architecture type.",
300 default => 'amd64',
301 },
302 ostype => {
303 optional => 1,
304 type => 'string',
305 enum => [qw(debian ubuntu centos fedora opensuse archlinux alpine gentoo unmanaged)],
306 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.",
307 },
308 console => {
309 optional => 1,
310 type => 'boolean',
311 description => "Attach a console device (/dev/console) to the container.",
312 default => 1,
313 },
314 tty => {
315 optional => 1,
316 type => 'integer',
317 description => "Specify the number of tty available to the container",
318 minimum => 0,
319 maximum => 6,
320 default => 2,
321 },
322 cores => {
323 optional => 1,
324 type => 'integer',
325 description => "The number of cores assigned to the container. A container can use all available cores by default.",
326 minimum => 1,
327 maximum => 128,
328 },
329 cpulimit => {
330 optional => 1,
331 type => 'number',
332 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.",
333 minimum => 0,
334 maximum => 128,
335 default => 0,
336 },
337 cpuunits => {
338 optional => 1,
339 type => 'integer',
340 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.",
341 minimum => 0,
342 maximum => 500000,
343 default => 1024,
344 },
345 memory => {
346 optional => 1,
347 type => 'integer',
348 description => "Amount of RAM for the VM in MB.",
349 minimum => 16,
350 default => 512,
351 },
352 swap => {
353 optional => 1,
354 type => 'integer',
355 description => "Amount of SWAP for the VM in MB.",
356 minimum => 0,
357 default => 512,
358 },
359 hostname => {
360 optional => 1,
361 description => "Set a host name for the container.",
362 type => 'string', format => 'dns-name',
363 maxLength => 255,
364 },
365 description => {
366 optional => 1,
367 type => 'string',
368 description => "Container description. Only used on the configuration web interface.",
369 },
370 searchdomain => {
371 optional => 1,
372 type => 'string', format => 'dns-name-list',
373 description => "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.",
374 },
375 nameserver => {
376 optional => 1,
377 type => 'string', format => 'address-list',
378 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.",
379 },
380 rootfs => get_standard_option('pve-ct-rootfs'),
381 parent => {
382 optional => 1,
383 type => 'string', format => 'pve-configid',
384 maxLength => 40,
385 description => "Parent snapshot name. This is used internally, and should not be modified.",
386 },
387 snaptime => {
388 optional => 1,
389 description => "Timestamp for snapshots.",
390 type => 'integer',
391 minimum => 0,
392 },
393 cmode => {
394 optional => 1,
395 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).",
396 type => 'string',
397 enum => ['shell', 'console', 'tty'],
398 default => 'tty',
399 },
400 protection => {
401 optional => 1,
402 type => 'boolean',
403 description => "Sets the protection flag of the container. This will prevent the CT or CT's disk remove/update operation.",
404 default => 0,
405 },
406 unprivileged => {
407 optional => 1,
408 type => 'boolean',
409 description => "Makes the container run as unprivileged user. (Should not be modified manually.)",
410 default => 0,
411 },
412 };
413
414 my $valid_lxc_conf_keys = {
415 'lxc.apparmor.profile' => 1,
416 'lxc.apparmor.allow_incomplete' => 1,
417 'lxc.selinux.context' => 1,
418 'lxc.include' => 1,
419 'lxc.arch' => 1,
420 'lxc.uts.name' => 1,
421 'lxc.signal.halt' => 1,
422 'lxc.signal.reboot' => 1,
423 'lxc.signal.stop' => 1,
424 'lxc.init.cmd' => 1,
425 'lxc.pty.max' => 1,
426 'lxc.console.logfile' => 1,
427 'lxc.console.path' => 1,
428 'lxc.tty.max' => 1,
429 'lxc.devtty.dir' => 1,
430 'lxc.hook.autodev' => 1,
431 'lxc.autodev' => 1,
432 'lxc.kmsg' => 1,
433 'lxc.mount.fstab' => 1,
434 'lxc.mount.entry' => 1,
435 'lxc.mount.auto' => 1,
436 'lxc.rootfs.path' => 'lxc.rootfs.path is auto generated from rootfs',
437 'lxc.rootfs.mount' => 1,
438 'lxc.rootfs.options' => 'lxc.rootfs.options is not supported' .
439 ', please use mount point options in the "rootfs" key',
440 # lxc.cgroup.*
441 # lxc.prlimit.*
442 'lxc.cap.drop' => 1,
443 'lxc.cap.keep' => 1,
444 'lxc.seccomp.profile' => 1,
445 'lxc.idmap' => 1,
446 'lxc.hook.pre-start' => 1,
447 'lxc.hook.pre-mount' => 1,
448 'lxc.hook.mount' => 1,
449 'lxc.hook.start' => 1,
450 'lxc.hook.stop' => 1,
451 'lxc.hook.post-stop' => 1,
452 'lxc.hook.clone' => 1,
453 'lxc.hook.destroy' => 1,
454 'lxc.log.level' => 1,
455 'lxc.log.file' => 1,
456 'lxc.start.auto' => 1,
457 'lxc.start.delay' => 1,
458 'lxc.start.order' => 1,
459 'lxc.group' => 1,
460 'lxc.environment' => 1,
461 };
462
463 my $deprecated_lxc_conf_keys = {
464 # Deprecated (removed with lxc 3.0):
465 'lxc.aa_profile' => 'lxc.apparmor.profile',
466 'lxc.aa_allow_incomplete' => 'lxc.apparmor.allow_incomplete',
467 'lxc.console' => 'lxc.console.path',
468 'lxc.devttydir' => 'lxc.tty.dir',
469 'lxc.haltsignal' => 'lxc.signal.halt',
470 'lxc.rebootsignal' => 'lxc.signal.reboot',
471 'lxc.stopsignal' => 'lxc.signal.stop',
472 'lxc.id_map' => 'lxc.idmap',
473 'lxc.init_cmd' => 'lxc.init.cmd',
474 'lxc.loglevel' => 'lxc.log.level',
475 'lxc.logfile' => 'lxc.log.file',
476 'lxc.mount' => 'lxc.mount.fstab',
477 'lxc.network.type' => 'lxc.net.INDEX.type',
478 'lxc.network.flags' => 'lxc.net.INDEX.flags',
479 'lxc.network.link' => 'lxc.net.INDEX.link',
480 'lxc.network.mtu' => 'lxc.net.INDEX.mtu',
481 'lxc.network.name' => 'lxc.net.INDEX.name',
482 'lxc.network.hwaddr' => 'lxc.net.INDEX.hwaddr',
483 'lxc.network.ipv4' => 'lxc.net.INDEX.ipv4.address',
484 'lxc.network.ipv4.gateway' => 'lxc.net.INDEX.ipv4.gateway',
485 'lxc.network.ipv6' => 'lxc.net.INDEX.ipv6.address',
486 'lxc.network.ipv6.gateway' => 'lxc.net.INDEX.ipv6.gateway',
487 'lxc.network.script.up' => 'lxc.net.INDEX.script.up',
488 'lxc.network.script.down' => 'lxc.net.INDEX.script.down',
489 'lxc.pts' => 'lxc.pty.max',
490 'lxc.se_context' => 'lxc.selinux.context',
491 'lxc.seccomp' => 'lxc.seccomp.profile',
492 'lxc.tty' => 'lxc.tty.max',
493 'lxc.utsname' => 'lxc.uts.name',
494 };
495
496 sub is_valid_lxc_conf_key {
497 my ($vmid, $key) = @_;
498 if ($key =~ /^lxc\.limit\./) {
499 warn "vm $vmid - $key: lxc.limit.* was renamed to lxc.prlimit.*\n";
500 return 1;
501 }
502 if (defined(my $new_name = $deprecated_lxc_conf_keys->{$key})) {
503 warn "vm $vmid - $key is deprecated and was renamed to $new_name\n";
504 return 1;
505 }
506 my $validity = $valid_lxc_conf_keys->{$key};
507 return $validity if defined($validity);
508 return 1 if $key =~ /^lxc\.cgroup\./ # allow all cgroup values
509 || $key =~ /^lxc\.prlimit\./ # allow all prlimits
510 || $key =~ /^lxc\.net\./; # allow custom network definitions
511 return 0;
512 }
513
514 our $netconf_desc = {
515 type => {
516 type => 'string',
517 optional => 1,
518 description => "Network interface type.",
519 enum => [qw(veth)],
520 },
521 name => {
522 type => 'string',
523 format_description => 'string',
524 description => 'Name of the network device as seen from inside the container. (lxc.network.name)',
525 pattern => '[-_.\w\d]+',
526 },
527 bridge => {
528 type => 'string',
529 format_description => 'bridge',
530 description => 'Bridge to attach the network device to.',
531 pattern => '[-_.\w\d]+',
532 optional => 1,
533 },
534 hwaddr => {
535 type => 'string',
536 format_description => "XX:XX:XX:XX:XX:XX",
537 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)',
538 pattern => qr/(?:[a-f0-9]{2}:){5}[a-f0-9]{2}/i,
539 optional => 1,
540 },
541 mtu => {
542 type => 'integer',
543 description => 'Maximum transfer unit of the interface. (lxc.network.mtu)',
544 minimum => 64, # minimum ethernet frame is 64 bytes
545 optional => 1,
546 },
547 ip => {
548 type => 'string',
549 format => 'pve-ipv4-config',
550 format_description => '(IPv4/CIDR|dhcp|manual)',
551 description => 'IPv4 address in CIDR format.',
552 optional => 1,
553 },
554 gw => {
555 type => 'string',
556 format => 'ipv4',
557 format_description => 'GatewayIPv4',
558 description => 'Default gateway for IPv4 traffic.',
559 optional => 1,
560 },
561 ip6 => {
562 type => 'string',
563 format => 'pve-ipv6-config',
564 format_description => '(IPv6/CIDR|auto|dhcp|manual)',
565 description => 'IPv6 address in CIDR format.',
566 optional => 1,
567 },
568 gw6 => {
569 type => 'string',
570 format => 'ipv6',
571 format_description => 'GatewayIPv6',
572 description => 'Default gateway for IPv6 traffic.',
573 optional => 1,
574 },
575 firewall => {
576 type => 'boolean',
577 description => "Controls whether this interface's firewall rules should be used.",
578 optional => 1,
579 },
580 tag => {
581 type => 'integer',
582 minimum => 1,
583 maximum => 4094,
584 description => "VLAN tag for this interface.",
585 optional => 1,
586 },
587 trunks => {
588 type => 'string',
589 pattern => qr/\d+(?:;\d+)*/,
590 format_description => 'vlanid[;vlanid...]',
591 description => "VLAN ids to pass through the interface",
592 optional => 1,
593 },
594 rate => {
595 type => 'number',
596 format_description => 'mbps',
597 description => "Apply rate limiting to the interface",
598 optional => 1,
599 },
600 };
601 PVE::JSONSchema::register_format('pve-lxc-network', $netconf_desc);
602
603 my $MAX_LXC_NETWORKS = 10;
604 for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) {
605 $confdesc->{"net$i"} = {
606 optional => 1,
607 type => 'string', format => $netconf_desc,
608 description => "Specifies network interfaces for the container.",
609 };
610 }
611
612 PVE::JSONSchema::register_format('pve-lxc-mp-string', \&verify_lxc_mp_string);
613 sub verify_lxc_mp_string {
614 my ($mp, $noerr) = @_;
615
616 # do not allow:
617 # /./ or /../
618 # /. or /.. at the end
619 # ../ at the beginning
620
621 if($mp =~ m@/\.\.?/@ ||
622 $mp =~ m@/\.\.?$@ ||
623 $mp =~ m@^\.\./@) {
624 return undef if $noerr;
625 die "$mp contains illegal character sequences\n";
626 }
627 return $mp;
628 }
629
630 my $mp_desc = {
631 %$rootfs_desc,
632 backup => {
633 type => 'boolean',
634 description => 'Whether to include the mount point in backups.',
635 verbose_description => 'Whether to include the mount point in backups '.
636 '(only used for volume mount points).',
637 optional => 1,
638 },
639 mp => {
640 type => 'string',
641 format => 'pve-lxc-mp-string',
642 format_description => 'Path',
643 description => 'Path to the mount point as seen from inside the container '.
644 '(must not contain symlinks).',
645 verbose_description => "Path to the mount point as seen from inside the container.\n\n".
646 "NOTE: Must not contain any symlinks for security reasons."
647 },
648 };
649 PVE::JSONSchema::register_format('pve-ct-mountpoint', $mp_desc);
650
651 my $unuseddesc = {
652 optional => 1,
653 type => 'string', format => 'pve-volume-id',
654 description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
655 };
656
657 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
658 $confdesc->{"mp$i"} = {
659 optional => 1,
660 type => 'string', format => $mp_desc,
661 description => "Use volume as container mount point.",
662 optional => 1,
663 };
664 }
665
666 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
667 $confdesc->{"unused$i"} = $unuseddesc;
668 }
669
670 sub parse_pct_config {
671 my ($filename, $raw) = @_;
672
673 return undef if !defined($raw);
674
675 my $res = {
676 digest => Digest::SHA::sha1_hex($raw),
677 snapshots => {},
678 };
679
680 $filename =~ m|/lxc/(\d+).conf$|
681 || die "got strange filename '$filename'";
682
683 my $vmid = $1;
684
685 my $conf = $res;
686 my $descr = '';
687 my $section = '';
688
689 my @lines = split(/\n/, $raw);
690 foreach my $line (@lines) {
691 next if $line =~ m/^\s*$/;
692
693 if ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
694 $section = $1;
695 $conf->{description} = $descr if $descr;
696 $descr = '';
697 $conf = $res->{snapshots}->{$section} = {};
698 next;
699 }
700
701 if ($line =~ m/^\#(.*)\s*$/) {
702 $descr .= PVE::Tools::decode_text($1) . "\n";
703 next;
704 }
705
706 if ($line =~ m/^(lxc\.[a-z0-9_\-\.]+)(:|\s*=)\s*(.*?)\s*$/) {
707 my $key = $1;
708 my $value = $3;
709 my $validity = is_valid_lxc_conf_key($vmid, $key);
710 if ($validity eq 1) {
711 push @{$conf->{lxc}}, [$key, $value];
712 } elsif (my $errmsg = $validity) {
713 warn "vm $vmid - $key: $errmsg\n";
714 } else {
715 warn "vm $vmid - unable to parse config: $line\n";
716 }
717 } elsif ($line =~ m/^(description):\s*(.*\S)\s*$/) {
718 $descr .= PVE::Tools::decode_text($2);
719 } elsif ($line =~ m/snapstate:\s*(prepare|delete)\s*$/) {
720 $conf->{snapstate} = $1;
721 } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(\S.*)\s*$/) {
722 my $key = $1;
723 my $value = $2;
724 eval { $value = PVE::LXC::Config->check_type($key, $value); };
725 warn "vm $vmid - unable to parse value of '$key' - $@" if $@;
726 $conf->{$key} = $value;
727 } else {
728 warn "vm $vmid - unable to parse config: $line\n";
729 }
730 }
731
732 $conf->{description} = $descr if $descr;
733
734 delete $res->{snapstate}; # just to be sure
735
736 return $res;
737 }
738
739 sub write_pct_config {
740 my ($filename, $conf) = @_;
741
742 delete $conf->{snapstate}; # just to be sure
743
744 my $volidlist = PVE::LXC::Config->get_vm_volumes($conf);
745 my $used_volids = {};
746 foreach my $vid (@$volidlist) {
747 $used_volids->{$vid} = 1;
748 }
749
750 # remove 'unusedX' settings if the volume is still used
751 foreach my $key (keys %$conf) {
752 my $value = $conf->{$key};
753 if ($key =~ m/^unused/ && $used_volids->{$value}) {
754 delete $conf->{$key};
755 }
756 }
757
758 my $generate_raw_config = sub {
759 my ($conf) = @_;
760
761 my $raw = '';
762
763 # add description as comment to top of file
764 my $descr = $conf->{description} || '';
765 foreach my $cl (split(/\n/, $descr)) {
766 $raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
767 }
768
769 foreach my $key (sort keys %$conf) {
770 next if $key eq 'digest' || $key eq 'description' ||
771 $key eq 'pending' || $key eq 'snapshots' ||
772 $key eq 'snapname' || $key eq 'lxc';
773 my $value = $conf->{$key};
774 die "detected invalid newline inside property '$key'\n"
775 if $value =~ m/\n/;
776 $raw .= "$key: $value\n";
777 }
778
779 if (my $lxcconf = $conf->{lxc}) {
780 foreach my $entry (@$lxcconf) {
781 my ($k, $v) = @$entry;
782 $raw .= "$k: $v\n";
783 }
784 }
785
786 return $raw;
787 };
788
789 my $raw = &$generate_raw_config($conf);
790
791 foreach my $snapname (sort keys %{$conf->{snapshots}}) {
792 $raw .= "\n[$snapname]\n";
793 $raw .= &$generate_raw_config($conf->{snapshots}->{$snapname});
794 }
795
796 return $raw;
797 }
798
799 sub update_pct_config {
800 my ($class, $vmid, $conf, $running, $param, $delete) = @_;
801
802 my @nohotplug;
803
804 my $new_disks = 0;
805 my @deleted_volumes;
806
807 my $rootdir;
808 if ($running) {
809 my $pid = PVE::LXC::find_lxc_pid($vmid);
810 $rootdir = "/proc/$pid/root";
811 }
812
813 my $hotplug_error = sub {
814 if ($running) {
815 push @nohotplug, @_;
816 return 1;
817 } else {
818 return 0;
819 }
820 };
821
822 if (defined($delete)) {
823 foreach my $opt (@$delete) {
824 if (!exists($conf->{$opt})) {
825 # silently ignore
826 next;
827 }
828
829 if ($opt eq 'memory' || $opt eq 'rootfs') {
830 die "unable to delete required option '$opt'\n";
831 } elsif ($opt eq 'hostname') {
832 delete $conf->{$opt};
833 } elsif ($opt eq 'swap') {
834 delete $conf->{$opt};
835 PVE::LXC::write_cgroup_value("memory", $vmid,
836 "memory.memsw.limit_in_bytes", -1);
837 } elsif ($opt eq 'description' || $opt eq 'onboot' || $opt eq 'startup') {
838 delete $conf->{$opt};
839 } elsif ($opt eq 'nameserver' || $opt eq 'searchdomain' ||
840 $opt eq 'tty' || $opt eq 'console' || $opt eq 'cmode') {
841 next if $hotplug_error->($opt);
842 delete $conf->{$opt};
843 } elsif ($opt eq 'cores') {
844 delete $conf->{$opt}; # rest is handled by pvestatd
845 } elsif ($opt eq 'cpulimit') {
846 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", -1);
847 delete $conf->{$opt};
848 } elsif ($opt eq 'cpuunits') {
849 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $confdesc->{cpuunits}->{default});
850 delete $conf->{$opt};
851 } elsif ($opt =~ m/^net(\d)$/) {
852 delete $conf->{$opt};
853 next if !$running;
854 my $netid = $1;
855 PVE::Network::veth_delete("veth${vmid}i$netid");
856 } elsif ($opt eq 'protection') {
857 delete $conf->{$opt};
858 } elsif ($opt =~ m/^unused(\d+)$/) {
859 next if $hotplug_error->($opt);
860 PVE::LXC::Config->check_protection($conf, "can't remove CT $vmid drive '$opt'");
861 push @deleted_volumes, $conf->{$opt};
862 delete $conf->{$opt};
863 } elsif ($opt =~ m/^mp(\d+)$/) {
864 next if $hotplug_error->($opt);
865 PVE::LXC::Config->check_protection($conf, "can't remove CT $vmid drive '$opt'");
866 my $mp = PVE::LXC::Config->parse_ct_mountpoint($conf->{$opt});
867 delete $conf->{$opt};
868 if ($mp->{type} eq 'volume') {
869 PVE::LXC::Config->add_unused_volume($conf, $mp->{volume});
870 }
871 } elsif ($opt eq 'unprivileged') {
872 die "unable to delete read-only option: '$opt'\n";
873 } else {
874 die "implement me (delete: $opt)"
875 }
876 PVE::LXC::Config->write_config($vmid, $conf) if $running;
877 }
878 }
879
880 # There's no separate swap size to configure, there's memory and "total"
881 # memory (iow. memory+swap). This means we have to change them together.
882 my $wanted_memory = PVE::Tools::extract_param($param, 'memory');
883 my $wanted_swap = PVE::Tools::extract_param($param, 'swap');
884 if (defined($wanted_memory) || defined($wanted_swap)) {
885
886 my $old_memory = ($conf->{memory} || 512);
887 my $old_swap = ($conf->{swap} || 0);
888
889 $wanted_memory //= $old_memory;
890 $wanted_swap //= $old_swap;
891
892 my $total = $wanted_memory + $wanted_swap;
893 if ($running) {
894 my $old_total = $old_memory + $old_swap;
895 if ($total > $old_total) {
896 PVE::LXC::write_cgroup_value("memory", $vmid,
897 "memory.memsw.limit_in_bytes",
898 int($total*1024*1024));
899 PVE::LXC::write_cgroup_value("memory", $vmid,
900 "memory.limit_in_bytes",
901 int($wanted_memory*1024*1024));
902 } else {
903 PVE::LXC::write_cgroup_value("memory", $vmid,
904 "memory.limit_in_bytes",
905 int($wanted_memory*1024*1024));
906 PVE::LXC::write_cgroup_value("memory", $vmid,
907 "memory.memsw.limit_in_bytes",
908 int($total*1024*1024));
909 }
910 }
911 $conf->{memory} = $wanted_memory;
912 $conf->{swap} = $wanted_swap;
913
914 PVE::LXC::Config->write_config($vmid, $conf) if $running;
915 }
916
917 my $storecfg = PVE::Storage::config();
918
919 my $used_volids = {};
920 my $check_content_type = sub {
921 my ($mp) = @_;
922 my $sid = PVE::Storage::parse_volume_id($mp->{volume});
923 my $storage_config = PVE::Storage::storage_config($storecfg, $sid);
924 die "storage '$sid' does not allow content type 'rootdir' (Container)\n"
925 if !$storage_config->{content}->{rootdir};
926 };
927
928 my $rescan_volume = sub {
929 my ($mp) = @_;
930 eval {
931 $mp->{size} = PVE::Storage::volume_size_info($storecfg, $mp->{volume}, 5)
932 if !defined($mp->{size});
933 };
934 warn "Could not rescan volume size - $@\n" if $@;
935 };
936
937 foreach my $opt (keys %$param) {
938 my $value = $param->{$opt};
939 my $check_protection_msg = "can't update CT $vmid drive '$opt'";
940 if ($opt eq 'hostname' || $opt eq 'arch') {
941 $conf->{$opt} = $value;
942 } elsif ($opt eq 'onboot') {
943 $conf->{$opt} = $value ? 1 : 0;
944 } elsif ($opt eq 'startup') {
945 $conf->{$opt} = $value;
946 } elsif ($opt eq 'tty' || $opt eq 'console' || $opt eq 'cmode') {
947 next if $hotplug_error->($opt);
948 $conf->{$opt} = $value;
949 } elsif ($opt eq 'nameserver') {
950 next if $hotplug_error->($opt);
951 my $list = PVE::LXC::verify_nameserver_list($value);
952 $conf->{$opt} = $list;
953 } elsif ($opt eq 'searchdomain') {
954 next if $hotplug_error->($opt);
955 my $list = PVE::LXC::verify_searchdomain_list($value);
956 $conf->{$opt} = $list;
957 } elsif ($opt eq 'cores') {
958 $conf->{$opt} = $value;# rest is handled by pvestatd
959 } elsif ($opt eq 'cpulimit') {
960 if ($value == 0) {
961 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", -1);
962 } else {
963 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", int(100000*$value));
964 }
965 $conf->{$opt} = $value;
966 } elsif ($opt eq 'cpuunits') {
967 $conf->{$opt} = $value;
968 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $value);
969 } elsif ($opt eq 'description') {
970 $conf->{$opt} = $value;
971 } elsif ($opt =~ m/^net(\d+)$/) {
972 my $netid = $1;
973 my $net = PVE::LXC::Config->parse_lxc_network($value);
974 if (!$running) {
975 $conf->{$opt} = PVE::LXC::Config->print_lxc_network($net);
976 } else {
977 PVE::LXC::update_net($vmid, $conf, $opt, $net, $netid, $rootdir);
978 }
979 } elsif ($opt eq 'protection') {
980 $conf->{$opt} = $value ? 1 : 0;
981 } elsif ($opt =~ m/^mp(\d+)$/) {
982 next if $hotplug_error->($opt);
983 PVE::LXC::Config->check_protection($conf, $check_protection_msg);
984 my $old = $conf->{$opt};
985 my $mp = PVE::LXC::Config->parse_ct_mountpoint($value);
986 if ($mp->{type} eq 'volume') {
987 &$check_content_type($mp);
988 $used_volids->{$mp->{volume}} = 1;
989 &$rescan_volume($mp);
990 $conf->{$opt} = PVE::LXC::Config->print_ct_mountpoint($mp);
991 } else {
992 $conf->{$opt} = $value;
993 }
994 if (defined($old)) {
995 my $mp = PVE::LXC::Config->parse_ct_mountpoint($old);
996 if ($mp->{type} eq 'volume') {
997 PVE::LXC::Config->add_unused_volume($conf, $mp->{volume});
998 }
999 }
1000 $new_disks = 1;
1001 } elsif ($opt eq 'rootfs') {
1002 next if $hotplug_error->($opt);
1003 PVE::LXC::Config->check_protection($conf, $check_protection_msg);
1004 my $old = $conf->{$opt};
1005 my $mp = PVE::LXC::Config->parse_ct_rootfs($value);
1006 if ($mp->{type} eq 'volume') {
1007 &$check_content_type($mp);
1008 $used_volids->{$mp->{volume}} = 1;
1009 &$rescan_volume($mp);
1010 $conf->{$opt} = PVE::LXC::Config->print_ct_mountpoint($mp, 1);
1011 } else {
1012 $conf->{$opt} = $value;
1013 }
1014 if (defined($old)) {
1015 my $mp = PVE::LXC::Config->parse_ct_rootfs($old);
1016 if ($mp->{type} eq 'volume') {
1017 PVE::LXC::Config->add_unused_volume($conf, $mp->{volume});
1018 }
1019 }
1020 $new_disks = 1;
1021 } elsif ($opt eq 'unprivileged') {
1022 die "unable to modify read-only option: '$opt'\n";
1023 } elsif ($opt eq 'ostype') {
1024 next if $hotplug_error->($opt);
1025 $conf->{$opt} = $value;
1026 } else {
1027 die "implement me: $opt";
1028 }
1029
1030 PVE::LXC::Config->write_config($vmid, $conf) if $running;
1031 }
1032
1033 # Apply deletions and creations of new volumes
1034 if (@deleted_volumes) {
1035 my $storage_cfg = PVE::Storage::config();
1036 foreach my $volume (@deleted_volumes) {
1037 next if $used_volids->{$volume}; # could have been re-added, too
1038 # also check for references in snapshots
1039 next if $class->is_volume_in_use($conf, $volume, 1);
1040 PVE::LXC::delete_mountpoint_volume($storage_cfg, $vmid, $volume);
1041 }
1042 }
1043
1044 if ($new_disks) {
1045 my $storage_cfg = PVE::Storage::config();
1046 PVE::LXC::create_disks($storage_cfg, $vmid, $conf, $conf);
1047 }
1048
1049 # This should be the last thing we do here
1050 if ($running && scalar(@nohotplug)) {
1051 die "unable to modify " . join(',', @nohotplug) . " while container is running\n";
1052 }
1053 }
1054
1055 sub check_type {
1056 my ($class, $key, $value) = @_;
1057
1058 die "unknown setting '$key'\n" if !$confdesc->{$key};
1059
1060 my $type = $confdesc->{$key}->{type};
1061
1062 if (!defined($value)) {
1063 die "got undefined value\n";
1064 }
1065
1066 if ($value =~ m/[\n\r]/) {
1067 die "property contains a line feed\n";
1068 }
1069
1070 if ($type eq 'boolean') {
1071 return 1 if ($value eq '1') || ($value =~ m/^(on|yes|true)$/i);
1072 return 0 if ($value eq '0') || ($value =~ m/^(off|no|false)$/i);
1073 die "type check ('boolean') failed - got '$value'\n";
1074 } elsif ($type eq 'integer') {
1075 return int($1) if $value =~ m/^(\d+)$/;
1076 die "type check ('integer') failed - got '$value'\n";
1077 } elsif ($type eq 'number') {
1078 return $value if $value =~ m/^(\d+)(\.\d+)?$/;
1079 die "type check ('number') failed - got '$value'\n";
1080 } elsif ($type eq 'string') {
1081 if (my $fmt = $confdesc->{$key}->{format}) {
1082 PVE::JSONSchema::check_format($fmt, $value);
1083 return $value;
1084 }
1085 return $value;
1086 } else {
1087 die "internal error"
1088 }
1089 }
1090
1091
1092 # add JSON properties for create and set function
1093 sub json_config_properties {
1094 my ($class, $prop) = @_;
1095
1096 foreach my $opt (keys %$confdesc) {
1097 next if $opt eq 'parent' || $opt eq 'snaptime';
1098 next if $prop->{$opt};
1099 $prop->{$opt} = $confdesc->{$opt};
1100 }
1101
1102 return $prop;
1103 }
1104
1105 sub __parse_ct_mountpoint_full {
1106 my ($class, $desc, $data, $noerr) = @_;
1107
1108 $data //= '';
1109
1110 my $res;
1111 eval { $res = PVE::JSONSchema::parse_property_string($desc, $data) };
1112 if ($@) {
1113 return undef if $noerr;
1114 die $@;
1115 }
1116
1117 if (defined(my $size = $res->{size})) {
1118 $size = PVE::JSONSchema::parse_size($size);
1119 if (!defined($size)) {
1120 return undef if $noerr;
1121 die "invalid size: $size\n";
1122 }
1123 $res->{size} = $size;
1124 }
1125
1126 $res->{type} = $class->classify_mountpoint($res->{volume});
1127
1128 return $res;
1129 };
1130
1131 sub parse_ct_rootfs {
1132 my ($class, $data, $noerr) = @_;
1133
1134 my $res = $class->__parse_ct_mountpoint_full($rootfs_desc, $data, $noerr);
1135
1136 $res->{mp} = '/' if defined($res);
1137
1138 return $res;
1139 }
1140
1141 sub parse_ct_mountpoint {
1142 my ($class, $data, $noerr) = @_;
1143
1144 return $class->__parse_ct_mountpoint_full($mp_desc, $data, $noerr);
1145 }
1146
1147 sub print_ct_mountpoint {
1148 my ($class, $info, $nomp) = @_;
1149 my $skip = [ 'type' ];
1150 push @$skip, 'mp' if $nomp;
1151 return PVE::JSONSchema::print_property_string($info, $mp_desc, $skip);
1152 }
1153
1154 sub print_lxc_network {
1155 my ($class, $net) = @_;
1156 return PVE::JSONSchema::print_property_string($net, $netconf_desc);
1157 }
1158
1159 sub parse_lxc_network {
1160 my ($class, $data) = @_;
1161
1162 my $res = {};
1163
1164 return $res if !$data;
1165
1166 $res = PVE::JSONSchema::parse_property_string($netconf_desc, $data);
1167
1168 $res->{type} = 'veth';
1169 if (!$res->{hwaddr}) {
1170 my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
1171 $res->{hwaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
1172 }
1173
1174 return $res;
1175 }
1176
1177 sub option_exists {
1178 my ($class, $name) = @_;
1179
1180 return defined($confdesc->{$name});
1181 }
1182 # END JSON config code
1183
1184 sub classify_mountpoint {
1185 my ($class, $vol) = @_;
1186 if ($vol =~ m!^/!) {
1187 return 'device' if $vol =~ m!^/dev/!;
1188 return 'bind';
1189 }
1190 return 'volume';
1191 }
1192
1193 my $is_volume_in_use = sub {
1194 my ($class, $config, $volid) = @_;
1195 my $used = 0;
1196
1197 $class->foreach_mountpoint($config, sub {
1198 my ($ms, $mountpoint) = @_;
1199 return if $used;
1200 $used = $mountpoint->{type} eq 'volume' && $mountpoint->{volume} eq $volid;
1201 });
1202
1203 return $used;
1204 };
1205
1206 sub is_volume_in_use_by_snapshots {
1207 my ($class, $config, $volid) = @_;
1208
1209 if (my $snapshots = $config->{snapshots}) {
1210 foreach my $snap (keys %$snapshots) {
1211 return 1 if $is_volume_in_use->($class, $snapshots->{$snap}, $volid);
1212 }
1213 }
1214
1215 return 0;
1216 };
1217
1218 sub is_volume_in_use {
1219 my ($class, $config, $volid, $include_snapshots) = @_;
1220 return 1 if $is_volume_in_use->($class, $config, $volid);
1221 return 1 if $include_snapshots && $class->is_volume_in_use_by_snapshots($config, $volid);
1222 return 0;
1223 }
1224
1225 sub has_dev_console {
1226 my ($class, $conf) = @_;
1227
1228 return !(defined($conf->{console}) && !$conf->{console});
1229 }
1230
1231 sub has_lxc_entry {
1232 my ($class, $conf, $keyname) = @_;
1233
1234 if (my $lxcconf = $conf->{lxc}) {
1235 foreach my $entry (@$lxcconf) {
1236 my ($key, undef) = @$entry;
1237 return 1 if $key eq $keyname;
1238 }
1239 }
1240
1241 return 0;
1242 }
1243
1244 sub get_tty_count {
1245 my ($class, $conf) = @_;
1246
1247 return $conf->{tty} // $confdesc->{tty}->{default};
1248 }
1249
1250 sub get_cmode {
1251 my ($class, $conf) = @_;
1252
1253 return $conf->{cmode} // $confdesc->{cmode}->{default};
1254 }
1255
1256 sub mountpoint_names {
1257 my ($class, $reverse) = @_;
1258
1259 my @names = ('rootfs');
1260
1261 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
1262 push @names, "mp$i";
1263 }
1264
1265 return $reverse ? reverse @names : @names;
1266 }
1267
1268 sub foreach_mountpoint_full {
1269 my ($class, $conf, $reverse, $func, @param) = @_;
1270
1271 my $mps = [ grep { defined($conf->{$_}) } $class->mountpoint_names($reverse) ];
1272 foreach my $key (@$mps) {
1273 my $value = $conf->{$key};
1274 my $mountpoint = $key eq 'rootfs' ? $class->parse_ct_rootfs($value, 1) : $class->parse_ct_mountpoint($value, 1);
1275 next if !defined($mountpoint);
1276
1277 &$func($key, $mountpoint, @param);
1278 }
1279 }
1280
1281 sub foreach_mountpoint {
1282 my ($class, $conf, $func, @param) = @_;
1283
1284 $class->foreach_mountpoint_full($conf, 0, $func, @param);
1285 }
1286
1287 sub foreach_mountpoint_reverse {
1288 my ($class, $conf, $func, @param) = @_;
1289
1290 $class->foreach_mountpoint_full($conf, 1, $func, @param);
1291 }
1292
1293 sub get_vm_volumes {
1294 my ($class, $conf, $excludes) = @_;
1295
1296 my $vollist = [];
1297
1298 $class->foreach_mountpoint($conf, sub {
1299 my ($ms, $mountpoint) = @_;
1300
1301 return if $excludes && $ms eq $excludes;
1302
1303 my $volid = $mountpoint->{volume};
1304 return if !$volid || $mountpoint->{type} ne 'volume';
1305
1306 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
1307 return if !$sid;
1308
1309 push @$vollist, $volid;
1310 });
1311
1312 return $vollist;
1313 }
1314
1315 sub get_replicatable_volumes {
1316 my ($class, $storecfg, $vmid, $conf, $cleanup, $noerr) = @_;
1317
1318 my $volhash = {};
1319
1320 my $test_volid = sub {
1321 my ($volid, $mountpoint) = @_;
1322
1323 return if !$volid;
1324
1325 my $mptype = $mountpoint->{type};
1326 my $replicate = $mountpoint->{replicate} // 1;
1327
1328 if ($mptype ne 'volume') {
1329 # skip bindmounts if replicate = 0 even for cleanup,
1330 # since bind mounts could not have been replicated ever
1331 return if !$replicate;
1332 die "unable to replicate mountpoint type '$mptype'\n";
1333 }
1334
1335 my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, $noerr);
1336 return if !$storeid;
1337
1338 my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
1339 return if $scfg->{shared};
1340
1341 my ($path, $owner, $vtype) = PVE::Storage::path($storecfg, $volid);
1342 return if !$owner || ($owner != $vmid);
1343
1344 die "unable to replicate volume '$volid', type '$vtype'\n" if $vtype ne 'images';
1345
1346 return if !$cleanup && !$replicate;
1347
1348 if (!PVE::Storage::volume_has_feature($storecfg, 'replicate', $volid)) {
1349 return if $cleanup || $noerr;
1350 die "missing replicate feature on volume '$volid'\n";
1351 }
1352
1353 $volhash->{$volid} = 1;
1354 };
1355
1356 $class->foreach_mountpoint($conf, sub {
1357 my ($ms, $mountpoint) = @_;
1358 $test_volid->($mountpoint->{volume}, $mountpoint);
1359 });
1360
1361 foreach my $snapname (keys %{$conf->{snapshots}}) {
1362 my $snap = $conf->{snapshots}->{$snapname};
1363 $class->foreach_mountpoint($snap, sub {
1364 my ($ms, $mountpoint) = @_;
1365 $test_volid->($mountpoint->{volume}, $mountpoint);
1366 });
1367 }
1368
1369 # add 'unusedX' volumes to volhash
1370 foreach my $key (keys %$conf) {
1371 if ($key =~ m/^unused/) {
1372 $test_volid->($conf->{$key}, { type => 'volume', replicate => 1 });
1373 }
1374 }
1375
1376 return $volhash;
1377 }
1378
1379 1;