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