]> git.proxmox.com Git - pve-container.git/blob - src/PVE/LXC/Config.pm
add @param to foreach_mountpoint(_xx)
[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 = 10;
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::Tools::run_command(['/usr/bin/lxc-stop', '-n', $vmid, '--kill'])
168 if $class->__snapshot_check_running($vmid);
169 }
170
171 sub __snapshot_rollback_vm_start {
172 my ($class, $vmid, $vmstate, $forcemachine);
173
174 die "implement me - save vmstate\n";
175 }
176
177 sub __snapshot_foreach_volume {
178 my ($class, $conf, $func) = @_;
179
180 $class->foreach_mountpoint($conf, $func);
181 }
182
183 # END implemented abstract methods from PVE::AbstractConfig
184
185 # BEGIN JSON config code
186
187 cfs_register_file('/lxc/', \&parse_pct_config, \&write_pct_config);
188
189 my $rootfs_desc = {
190 volume => {
191 type => 'string',
192 default_key => 1,
193 format => 'pve-lxc-mp-string',
194 format_description => 'volume',
195 description => 'Volume, device or directory to mount into the container.',
196 },
197 size => {
198 type => 'string',
199 format => 'disk-size',
200 format_description => 'DiskSize',
201 description => 'Volume size (read only value).',
202 optional => 1,
203 },
204 acl => {
205 type => 'boolean',
206 description => 'Explicitly enable or disable ACL support.',
207 optional => 1,
208 },
209 ro => {
210 type => 'boolean',
211 description => 'Read-only mountpoint',
212 optional => 1,
213 },
214 quota => {
215 type => 'boolean',
216 description => 'Enable user quotas inside the container (not supported with zfs subvolumes)',
217 optional => 1,
218 },
219 };
220
221 PVE::JSONSchema::register_standard_option('pve-ct-rootfs', {
222 type => 'string', format => $rootfs_desc,
223 description => "Use volume as container root.",
224 optional => 1,
225 });
226
227 PVE::JSONSchema::register_standard_option('pve-lxc-snapshot-name', {
228 description => "The name of the snapshot.",
229 type => 'string', format => 'pve-configid',
230 maxLength => 40,
231 });
232
233 my $confdesc = {
234 lock => {
235 optional => 1,
236 type => 'string',
237 description => "Lock/unlock the VM.",
238 enum => [qw(migrate backup snapshot rollback)],
239 },
240 onboot => {
241 optional => 1,
242 type => 'boolean',
243 description => "Specifies whether a VM will be started during system bootup.",
244 default => 0,
245 },
246 startup => get_standard_option('pve-startup-order'),
247 template => {
248 optional => 1,
249 type => 'boolean',
250 description => "Enable/disable Template.",
251 default => 0,
252 },
253 arch => {
254 optional => 1,
255 type => 'string',
256 enum => ['amd64', 'i386'],
257 description => "OS architecture type.",
258 default => 'amd64',
259 },
260 ostype => {
261 optional => 1,
262 type => 'string',
263 enum => [qw(debian ubuntu centos fedora opensuse archlinux alpine gentoo unmanaged)],
264 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.",
265 },
266 console => {
267 optional => 1,
268 type => 'boolean',
269 description => "Attach a console device (/dev/console) to the container.",
270 default => 1,
271 },
272 tty => {
273 optional => 1,
274 type => 'integer',
275 description => "Specify the number of tty available to the container",
276 minimum => 0,
277 maximum => 6,
278 default => 2,
279 },
280 cpulimit => {
281 optional => 1,
282 type => 'number',
283 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.",
284 minimum => 0,
285 maximum => 128,
286 default => 0,
287 },
288 cpuunits => {
289 optional => 1,
290 type => 'integer',
291 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.",
292 minimum => 0,
293 maximum => 500000,
294 default => 1024,
295 },
296 memory => {
297 optional => 1,
298 type => 'integer',
299 description => "Amount of RAM for the VM in MB.",
300 minimum => 16,
301 default => 512,
302 },
303 swap => {
304 optional => 1,
305 type => 'integer',
306 description => "Amount of SWAP for the VM in MB.",
307 minimum => 0,
308 default => 512,
309 },
310 hostname => {
311 optional => 1,
312 description => "Set a host name for the container.",
313 type => 'string', format => 'dns-name',
314 maxLength => 255,
315 },
316 description => {
317 optional => 1,
318 type => 'string',
319 description => "Container description. Only used on the configuration web interface.",
320 },
321 searchdomain => {
322 optional => 1,
323 type => 'string', format => 'dns-name-list',
324 description => "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.",
325 },
326 nameserver => {
327 optional => 1,
328 type => 'string', format => 'address-list',
329 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.",
330 },
331 rootfs => get_standard_option('pve-ct-rootfs'),
332 parent => {
333 optional => 1,
334 type => 'string', format => 'pve-configid',
335 maxLength => 40,
336 description => "Parent snapshot name. This is used internally, and should not be modified.",
337 },
338 snaptime => {
339 optional => 1,
340 description => "Timestamp for snapshots.",
341 type => 'integer',
342 minimum => 0,
343 },
344 cmode => {
345 optional => 1,
346 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).",
347 type => 'string',
348 enum => ['shell', 'console', 'tty'],
349 default => 'tty',
350 },
351 protection => {
352 optional => 1,
353 type => 'boolean',
354 description => "Sets the protection flag of the container. This will prevent the CT or CT's disk remove/update operation.",
355 default => 0,
356 },
357 unprivileged => {
358 optional => 1,
359 type => 'boolean',
360 description => "Makes the container run as unprivileged user. (Should not be modified manually.)",
361 default => 0,
362 },
363 };
364
365 my $valid_lxc_conf_keys = {
366 'lxc.include' => 1,
367 'lxc.arch' => 1,
368 'lxc.utsname' => 1,
369 'lxc.haltsignal' => 1,
370 'lxc.rebootsignal' => 1,
371 'lxc.stopsignal' => 1,
372 'lxc.init_cmd' => 1,
373 'lxc.network.type' => 1,
374 'lxc.network.flags' => 1,
375 'lxc.network.link' => 1,
376 'lxc.network.mtu' => 1,
377 'lxc.network.name' => 1,
378 'lxc.network.hwaddr' => 1,
379 'lxc.network.ipv4' => 1,
380 'lxc.network.ipv4.gateway' => 1,
381 'lxc.network.ipv6' => 1,
382 'lxc.network.ipv6.gateway' => 1,
383 'lxc.network.script.up' => 1,
384 'lxc.network.script.down' => 1,
385 'lxc.pts' => 1,
386 'lxc.console.logfile' => 1,
387 'lxc.console' => 1,
388 'lxc.tty' => 1,
389 'lxc.devttydir' => 1,
390 'lxc.hook.autodev' => 1,
391 'lxc.autodev' => 1,
392 'lxc.kmsg' => 1,
393 'lxc.mount' => 1,
394 'lxc.mount.entry' => 1,
395 'lxc.mount.auto' => 1,
396 'lxc.rootfs' => 'lxc.rootfs is auto generated from rootfs',
397 'lxc.rootfs.mount' => 1,
398 'lxc.rootfs.options' => 'lxc.rootfs.options is not supported' .
399 ', please use mountpoint options in the "rootfs" key',
400 # lxc.cgroup.*
401 'lxc.cap.drop' => 1,
402 'lxc.cap.keep' => 1,
403 'lxc.aa_profile' => 1,
404 'lxc.aa_allow_incomplete' => 1,
405 'lxc.se_context' => 1,
406 'lxc.seccomp' => 1,
407 'lxc.id_map' => 1,
408 'lxc.hook.pre-start' => 1,
409 'lxc.hook.pre-mount' => 1,
410 'lxc.hook.mount' => 1,
411 'lxc.hook.start' => 1,
412 'lxc.hook.stop' => 1,
413 'lxc.hook.post-stop' => 1,
414 'lxc.hook.clone' => 1,
415 'lxc.hook.destroy' => 1,
416 'lxc.loglevel' => 1,
417 'lxc.logfile' => 1,
418 'lxc.start.auto' => 1,
419 'lxc.start.delay' => 1,
420 'lxc.start.order' => 1,
421 'lxc.group' => 1,
422 'lxc.environment' => 1,
423 };
424
425 our $netconf_desc = {
426 type => {
427 type => 'string',
428 optional => 1,
429 description => "Network interface type.",
430 enum => [qw(veth)],
431 },
432 name => {
433 type => 'string',
434 format_description => 'string',
435 description => 'Name of the network device as seen from inside the container. (lxc.network.name)',
436 pattern => '[-_.\w\d]+',
437 },
438 bridge => {
439 type => 'string',
440 format_description => 'bridge',
441 description => 'Bridge to attach the network device to.',
442 pattern => '[-_.\w\d]+',
443 optional => 1,
444 },
445 hwaddr => {
446 type => 'string',
447 format_description => "XX:XX:XX:XX:XX:XX",
448 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)',
449 pattern => qr/(?:[a-f0-9]{2}:){5}[a-f0-9]{2}/i,
450 optional => 1,
451 },
452 mtu => {
453 type => 'integer',
454 description => 'Maximum transfer unit of the interface. (lxc.network.mtu)',
455 minimum => 64, # minimum ethernet frame is 64 bytes
456 optional => 1,
457 },
458 ip => {
459 type => 'string',
460 format => 'pve-ipv4-config',
461 format_description => 'IPv4Format/CIDR',
462 description => 'IPv4 address in CIDR format.',
463 optional => 1,
464 },
465 gw => {
466 type => 'string',
467 format => 'ipv4',
468 format_description => 'GatewayIPv4',
469 description => 'Default gateway for IPv4 traffic.',
470 optional => 1,
471 },
472 ip6 => {
473 type => 'string',
474 format => 'pve-ipv6-config',
475 format_description => 'IPv6Format/CIDR',
476 description => 'IPv6 address in CIDR format.',
477 optional => 1,
478 },
479 gw6 => {
480 type => 'string',
481 format => 'ipv6',
482 format_description => 'GatewayIPv6',
483 description => 'Default gateway for IPv6 traffic.',
484 optional => 1,
485 },
486 firewall => {
487 type => 'boolean',
488 description => "Controls whether this interface's firewall rules should be used.",
489 optional => 1,
490 },
491 tag => {
492 type => 'integer',
493 minimum => '1',
494 maximum => '4094',
495 description => "VLAN tag for this interface.",
496 optional => 1,
497 },
498 trunks => {
499 type => 'string',
500 pattern => qr/\d+(?:;\d+)*/,
501 format_description => 'vlanid[;vlanid...]',
502 description => "VLAN ids to pass through the interface",
503 optional => 1,
504 },
505 rate => {
506 type => 'number',
507 format_description => 'mbps',
508 description => "Apply rate limiting to the interface",
509 optional => 1,
510 },
511 };
512 PVE::JSONSchema::register_format('pve-lxc-network', $netconf_desc);
513
514 my $MAX_LXC_NETWORKS = 10;
515 for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) {
516 $confdesc->{"net$i"} = {
517 optional => 1,
518 type => 'string', format => $netconf_desc,
519 description => "Specifies network interfaces for the container.",
520 };
521 }
522
523 PVE::JSONSchema::register_format('pve-lxc-mp-string', \&verify_lxc_mp_string);
524 sub verify_lxc_mp_string {
525 my ($mp, $noerr) = @_;
526
527 # do not allow:
528 # /./ or /../
529 # /. or /.. at the end
530 # ../ at the beginning
531
532 if($mp =~ m@/\.\.?/@ ||
533 $mp =~ m@/\.\.?$@ ||
534 $mp =~ m@^\.\./@) {
535 return undef if $noerr;
536 die "$mp contains illegal character sequences\n";
537 }
538 return $mp;
539 }
540
541 my $mp_desc = {
542 %$rootfs_desc,
543 backup => {
544 type => 'boolean',
545 description => 'Whether to include the mountpoint in backups.',
546 verbose_description => 'Whether to include the mountpoint in backups '.
547 '(only used for volume mountpoints).',
548 optional => 1,
549 },
550 mp => {
551 type => 'string',
552 format => 'pve-lxc-mp-string',
553 format_description => 'Path',
554 description => 'Path to the mountpoint as seen from inside the container '.
555 '(must not contain symlinks).',
556 verbose_description => "Path to the mountpoint as seen from inside the container.\n\n".
557 "NOTE: Must not contain any symlinks for security reasons."
558 },
559 };
560 PVE::JSONSchema::register_format('pve-ct-mountpoint', $mp_desc);
561
562 my $unuseddesc = {
563 optional => 1,
564 type => 'string', format => 'pve-volume-id',
565 description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
566 };
567
568 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
569 $confdesc->{"mp$i"} = {
570 optional => 1,
571 type => 'string', format => $mp_desc,
572 description => "Use volume as container mount point.",
573 optional => 1,
574 };
575 }
576
577 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
578 $confdesc->{"unused$i"} = $unuseddesc;
579 }
580
581 sub parse_pct_config {
582 my ($filename, $raw) = @_;
583
584 return undef if !defined($raw);
585
586 my $res = {
587 digest => Digest::SHA::sha1_hex($raw),
588 snapshots => {},
589 };
590
591 $filename =~ m|/lxc/(\d+).conf$|
592 || die "got strange filename '$filename'";
593
594 my $vmid = $1;
595
596 my $conf = $res;
597 my $descr = '';
598 my $section = '';
599
600 my @lines = split(/\n/, $raw);
601 foreach my $line (@lines) {
602 next if $line =~ m/^\s*$/;
603
604 if ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
605 $section = $1;
606 $conf->{description} = $descr if $descr;
607 $descr = '';
608 $conf = $res->{snapshots}->{$section} = {};
609 next;
610 }
611
612 if ($line =~ m/^\#(.*)\s*$/) {
613 $descr .= PVE::Tools::decode_text($1) . "\n";
614 next;
615 }
616
617 if ($line =~ m/^(lxc\.[a-z0-9_\-\.]+)(:|\s*=)\s*(.*?)\s*$/) {
618 my $key = $1;
619 my $value = $3;
620 my $validity = $valid_lxc_conf_keys->{$key} || 0;
621 if ($validity eq 1 || $key =~ m/^lxc\.cgroup\./) {
622 push @{$conf->{lxc}}, [$key, $value];
623 } elsif (my $errmsg = $validity) {
624 warn "vm $vmid - $key: $errmsg\n";
625 } else {
626 warn "vm $vmid - unable to parse config: $line\n";
627 }
628 } elsif ($line =~ m/^(description):\s*(.*\S)\s*$/) {
629 $descr .= PVE::Tools::decode_text($2);
630 } elsif ($line =~ m/snapstate:\s*(prepare|delete)\s*$/) {
631 $conf->{snapstate} = $1;
632 } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(\S.*)\s*$/) {
633 my $key = $1;
634 my $value = $2;
635 eval { $value = PVE::LXC::Config->check_type($key, $value); };
636 warn "vm $vmid - unable to parse value of '$key' - $@" if $@;
637 $conf->{$key} = $value;
638 } else {
639 warn "vm $vmid - unable to parse config: $line\n";
640 }
641 }
642
643 $conf->{description} = $descr if $descr;
644
645 delete $res->{snapstate}; # just to be sure
646
647 return $res;
648 }
649
650 sub write_pct_config {
651 my ($filename, $conf) = @_;
652
653 delete $conf->{snapstate}; # just to be sure
654
655 my $volidlist = PVE::LXC::Config->get_vm_volumes($conf);
656 my $used_volids = {};
657 foreach my $vid (@$volidlist) {
658 $used_volids->{$vid} = 1;
659 }
660
661 # remove 'unusedX' settings if the volume is still used
662 foreach my $key (keys %$conf) {
663 my $value = $conf->{$key};
664 if ($key =~ m/^unused/ && $used_volids->{$value}) {
665 delete $conf->{$key};
666 }
667 }
668
669 my $generate_raw_config = sub {
670 my ($conf) = @_;
671
672 my $raw = '';
673
674 # add description as comment to top of file
675 my $descr = $conf->{description} || '';
676 foreach my $cl (split(/\n/, $descr)) {
677 $raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
678 }
679
680 foreach my $key (sort keys %$conf) {
681 next if $key eq 'digest' || $key eq 'description' ||
682 $key eq 'pending' || $key eq 'snapshots' ||
683 $key eq 'snapname' || $key eq 'lxc';
684 my $value = $conf->{$key};
685 die "detected invalid newline inside property '$key'\n"
686 if $value =~ m/\n/;
687 $raw .= "$key: $value\n";
688 }
689
690 if (my $lxcconf = $conf->{lxc}) {
691 foreach my $entry (@$lxcconf) {
692 my ($k, $v) = @$entry;
693 $raw .= "$k: $v\n";
694 }
695 }
696
697 return $raw;
698 };
699
700 my $raw = &$generate_raw_config($conf);
701
702 foreach my $snapname (sort keys %{$conf->{snapshots}}) {
703 $raw .= "\n[$snapname]\n";
704 $raw .= &$generate_raw_config($conf->{snapshots}->{$snapname});
705 }
706
707 return $raw;
708 }
709
710 sub update_pct_config {
711 my ($class, $vmid, $conf, $running, $param, $delete) = @_;
712
713 my @nohotplug;
714
715 my $new_disks = 0;
716 my @deleted_volumes;
717
718 my $rootdir;
719 if ($running) {
720 my $pid = PVE::LXC::find_lxc_pid($vmid);
721 $rootdir = "/proc/$pid/root";
722 }
723
724 my $hotplug_error = sub {
725 if ($running) {
726 push @nohotplug, @_;
727 return 1;
728 } else {
729 return 0;
730 }
731 };
732
733 if (defined($delete)) {
734 foreach my $opt (@$delete) {
735 if (!exists($conf->{$opt})) {
736 warn "no such option: $opt\n";
737 next;
738 }
739
740 if ($opt eq 'hostname' || $opt eq 'memory' || $opt eq 'rootfs') {
741 die "unable to delete required option '$opt'\n";
742 } elsif ($opt eq 'swap') {
743 delete $conf->{$opt};
744 PVE::LXC::write_cgroup_value("memory", $vmid,
745 "memory.memsw.limit_in_bytes", -1);
746 } elsif ($opt eq 'description' || $opt eq 'onboot' || $opt eq 'startup') {
747 delete $conf->{$opt};
748 } elsif ($opt eq 'nameserver' || $opt eq 'searchdomain' ||
749 $opt eq 'tty' || $opt eq 'console' || $opt eq 'cmode') {
750 next if $hotplug_error->($opt);
751 delete $conf->{$opt};
752 } elsif ($opt eq 'cpulimit') {
753 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", -1);
754 delete $conf->{$opt};
755 } elsif ($opt eq 'cpuunits') {
756 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $confdesc->{cpuunits}->{default});
757 delete $conf->{$opt};
758 } elsif ($opt =~ m/^net(\d)$/) {
759 delete $conf->{$opt};
760 next if !$running;
761 my $netid = $1;
762 PVE::Network::veth_delete("veth${vmid}i$netid");
763 } elsif ($opt eq 'protection') {
764 delete $conf->{$opt};
765 } elsif ($opt =~ m/^unused(\d+)$/) {
766 next if $hotplug_error->($opt);
767 PVE::LXC::Config->check_protection($conf, "can't remove CT $vmid drive '$opt'");
768 push @deleted_volumes, $conf->{$opt};
769 delete $conf->{$opt};
770 } elsif ($opt =~ m/^mp(\d+)$/) {
771 next if $hotplug_error->($opt);
772 PVE::LXC::Config->check_protection($conf, "can't remove CT $vmid drive '$opt'");
773 my $mp = PVE::LXC::Config->parse_ct_mountpoint($conf->{$opt});
774 delete $conf->{$opt};
775 if ($mp->{type} eq 'volume') {
776 PVE::LXC::Config->add_unused_volume($conf, $mp->{volume});
777 }
778 } elsif ($opt eq 'unprivileged') {
779 die "unable to delete read-only option: '$opt'\n";
780 } else {
781 die "implement me (delete: $opt)"
782 }
783 PVE::LXC::Config->write_config($vmid, $conf) if $running;
784 }
785 }
786
787 # There's no separate swap size to configure, there's memory and "total"
788 # memory (iow. memory+swap). This means we have to change them together.
789 my $wanted_memory = PVE::Tools::extract_param($param, 'memory');
790 my $wanted_swap = PVE::Tools::extract_param($param, 'swap');
791 if (defined($wanted_memory) || defined($wanted_swap)) {
792
793 my $old_memory = ($conf->{memory} || 512);
794 my $old_swap = ($conf->{swap} || 0);
795
796 $wanted_memory //= $old_memory;
797 $wanted_swap //= $old_swap;
798
799 my $total = $wanted_memory + $wanted_swap;
800 if ($running) {
801 my $old_total = $old_memory + $old_swap;
802 if ($total > $old_total) {
803 PVE::LXC::write_cgroup_value("memory", $vmid,
804 "memory.memsw.limit_in_bytes",
805 int($total*1024*1024));
806 PVE::LXC::write_cgroup_value("memory", $vmid,
807 "memory.limit_in_bytes",
808 int($wanted_memory*1024*1024));
809 } else {
810 PVE::LXC::write_cgroup_value("memory", $vmid,
811 "memory.limit_in_bytes",
812 int($wanted_memory*1024*1024));
813 PVE::LXC::write_cgroup_value("memory", $vmid,
814 "memory.memsw.limit_in_bytes",
815 int($total*1024*1024));
816 }
817 }
818 $conf->{memory} = $wanted_memory;
819 $conf->{swap} = $wanted_swap;
820
821 PVE::LXC::Config->write_config($vmid, $conf) if $running;
822 }
823
824 my $used_volids = {};
825 my $check_content_type = sub {
826 my ($mp) = @_;
827 my $sid = PVE::Storage::parse_volume_id($mp->{volume});
828 my $scfg = PVE::Storage::config();
829 my $storage_config = PVE::Storage::storage_config($scfg, $sid);
830 die "storage '$sid' does not allow content type 'rootdir' (Container)\n"
831 if !$storage_config->{content}->{rootdir};
832 };
833
834 foreach my $opt (keys %$param) {
835 my $value = $param->{$opt};
836 my $check_protection_msg = "can't update CT $vmid drive '$opt'";
837 if ($opt eq 'hostname') {
838 $conf->{$opt} = $value;
839 } elsif ($opt eq 'onboot') {
840 $conf->{$opt} = $value ? 1 : 0;
841 } elsif ($opt eq 'startup') {
842 $conf->{$opt} = $value;
843 } elsif ($opt eq 'tty' || $opt eq 'console' || $opt eq 'cmode') {
844 next if $hotplug_error->($opt);
845 $conf->{$opt} = $value;
846 } elsif ($opt eq 'nameserver') {
847 next if $hotplug_error->($opt);
848 my $list = PVE::LXC::verify_nameserver_list($value);
849 $conf->{$opt} = $list;
850 } elsif ($opt eq 'searchdomain') {
851 next if $hotplug_error->($opt);
852 my $list = PVE::LXC::verify_searchdomain_list($value);
853 $conf->{$opt} = $list;
854 } elsif ($opt eq 'cpulimit') {
855 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", int(100000*$value));
856 $conf->{$opt} = $value;
857 } elsif ($opt eq 'cpuunits') {
858 $conf->{$opt} = $value;
859 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $value);
860 } elsif ($opt eq 'description') {
861 $conf->{$opt} = PVE::Tools::encode_text($value);
862 } elsif ($opt =~ m/^net(\d+)$/) {
863 my $netid = $1;
864 my $net = PVE::LXC::Config->parse_lxc_network($value);
865 if (!$running) {
866 $conf->{$opt} = PVE::LXC::Config->print_lxc_network($net);
867 } else {
868 PVE::LXC::update_net($vmid, $conf, $opt, $net, $netid, $rootdir);
869 }
870 } elsif ($opt eq 'protection') {
871 $conf->{$opt} = $value ? 1 : 0;
872 } elsif ($opt =~ m/^mp(\d+)$/) {
873 next if $hotplug_error->($opt);
874 PVE::LXC::Config->check_protection($conf, $check_protection_msg);
875 my $old = $conf->{$opt};
876 my $mp = PVE::LXC::Config->parse_ct_mountpoint($value);
877 if ($mp->{type} eq 'volume') {
878 &$check_content_type($mp);
879 $used_volids->{$mp->{volume}} = 1;
880 }
881 $conf->{$opt} = $value;
882 if (defined($old)) {
883 my $mp = PVE::LXC::Config->parse_ct_mountpoint($old);
884 if ($mp->{type} eq 'volume') {
885 PVE::LXC::Config->add_unused_volume($conf, $mp->{volume});
886 }
887 }
888 $new_disks = 1;
889 } elsif ($opt eq 'rootfs') {
890 next if $hotplug_error->($opt);
891 PVE::LXC::Config->check_protection($conf, $check_protection_msg);
892 my $old = $conf->{$opt};
893 $conf->{$opt} = $value;
894 my $mp = PVE::LXC::Config->parse_ct_rootfs($value);
895 if ($mp->{type} eq 'volume') {
896 &$check_content_type($mp);
897 $used_volids->{$mp->{volume}} = 1;
898 }
899 if (defined($old)) {
900 my $mp = PVE::LXC::Config->parse_ct_rootfs($old);
901 if ($mp->{type} eq 'volume') {
902 PVE::LXC::Config->add_unused_volume($conf, $mp->{volume});
903 }
904 }
905 $new_disks = 1;
906 } elsif ($opt eq 'unprivileged') {
907 die "unable to modify read-only option: '$opt'\n";
908 } elsif ($opt eq 'ostype') {
909 next if $hotplug_error->($opt);
910 $conf->{$opt} = $value;
911 } else {
912 die "implement me: $opt";
913 }
914 PVE::LXC::Config->write_config($vmid, $conf) if $running;
915 }
916
917 # Apply deletions and creations of new volumes
918 if (@deleted_volumes) {
919 my $storage_cfg = PVE::Storage::config();
920 foreach my $volume (@deleted_volumes) {
921 next if $used_volids->{$volume}; # could have been re-added, too
922 # also check for references in snapshots
923 next if $class->is_volume_in_use($conf, $volume, 1);
924 PVE::LXC::delete_mountpoint_volume($storage_cfg, $vmid, $volume);
925 }
926 }
927
928 if ($new_disks) {
929 my $storage_cfg = PVE::Storage::config();
930 PVE::LXC::create_disks($storage_cfg, $vmid, $conf, $conf);
931 }
932
933 # This should be the last thing we do here
934 if ($running && scalar(@nohotplug)) {
935 die "unable to modify " . join(',', @nohotplug) . " while container is running\n";
936 }
937 }
938
939 sub check_type {
940 my ($class, $key, $value) = @_;
941
942 die "unknown setting '$key'\n" if !$confdesc->{$key};
943
944 my $type = $confdesc->{$key}->{type};
945
946 if (!defined($value)) {
947 die "got undefined value\n";
948 }
949
950 if ($value =~ m/[\n\r]/) {
951 die "property contains a line feed\n";
952 }
953
954 if ($type eq 'boolean') {
955 return 1 if ($value eq '1') || ($value =~ m/^(on|yes|true)$/i);
956 return 0 if ($value eq '0') || ($value =~ m/^(off|no|false)$/i);
957 die "type check ('boolean') failed - got '$value'\n";
958 } elsif ($type eq 'integer') {
959 return int($1) if $value =~ m/^(\d+)$/;
960 die "type check ('integer') failed - got '$value'\n";
961 } elsif ($type eq 'number') {
962 return $value if $value =~ m/^(\d+)(\.\d+)?$/;
963 die "type check ('number') failed - got '$value'\n";
964 } elsif ($type eq 'string') {
965 if (my $fmt = $confdesc->{$key}->{format}) {
966 PVE::JSONSchema::check_format($fmt, $value);
967 return $value;
968 }
969 return $value;
970 } else {
971 die "internal error"
972 }
973 }
974
975
976 # add JSON properties for create and set function
977 sub json_config_properties {
978 my ($class, $prop) = @_;
979
980 foreach my $opt (keys %$confdesc) {
981 next if $opt eq 'parent' || $opt eq 'snaptime';
982 next if $prop->{$opt};
983 $prop->{$opt} = $confdesc->{$opt};
984 }
985
986 return $prop;
987 }
988
989 sub __parse_ct_mountpoint_full {
990 my ($class, $desc, $data, $noerr) = @_;
991
992 $data //= '';
993
994 my $res;
995 eval { $res = PVE::JSONSchema::parse_property_string($desc, $data) };
996 if ($@) {
997 return undef if $noerr;
998 die $@;
999 }
1000
1001 if (defined(my $size = $res->{size})) {
1002 $size = PVE::JSONSchema::parse_size($size);
1003 if (!defined($size)) {
1004 return undef if $noerr;
1005 die "invalid size: $size\n";
1006 }
1007 $res->{size} = $size;
1008 }
1009
1010 $res->{type} = $class->classify_mountpoint($res->{volume});
1011
1012 return $res;
1013 };
1014
1015 sub parse_ct_rootfs {
1016 my ($class, $data, $noerr) = @_;
1017
1018 my $res = $class->__parse_ct_mountpoint_full($rootfs_desc, $data, $noerr);
1019
1020 $res->{mp} = '/' if defined($res);
1021
1022 return $res;
1023 }
1024
1025 sub parse_ct_mountpoint {
1026 my ($class, $data, $noerr) = @_;
1027
1028 return $class->__parse_ct_mountpoint_full($mp_desc, $data, $noerr);
1029 }
1030
1031 sub print_ct_mountpoint {
1032 my ($class, $info, $nomp) = @_;
1033 my $skip = [ 'type' ];
1034 push @$skip, 'mp' if $nomp;
1035 return PVE::JSONSchema::print_property_string($info, $mp_desc, $skip);
1036 }
1037
1038 sub print_lxc_network {
1039 my ($class, $net) = @_;
1040 return PVE::JSONSchema::print_property_string($net, $netconf_desc);
1041 }
1042
1043 sub parse_lxc_network {
1044 my ($class, $data) = @_;
1045
1046 my $res = {};
1047
1048 return $res if !$data;
1049
1050 $res = PVE::JSONSchema::parse_property_string($netconf_desc, $data);
1051
1052 $res->{type} = 'veth';
1053 $res->{hwaddr} = PVE::Tools::random_ether_addr() if !$res->{hwaddr};
1054
1055 return $res;
1056 }
1057
1058 sub option_exists {
1059 my ($class, $name) = @_;
1060
1061 return defined($confdesc->{$name});
1062 }
1063 # END JSON config code
1064
1065 sub classify_mountpoint {
1066 my ($class, $vol) = @_;
1067 if ($vol =~ m!^/!) {
1068 return 'device' if $vol =~ m!^/dev/!;
1069 return 'bind';
1070 }
1071 return 'volume';
1072 }
1073
1074 sub is_volume_in_use {
1075 my ($class, $config, $volid, $include_snapshots) = @_;
1076 my $used = 0;
1077
1078 $class->foreach_mountpoint($config, sub {
1079 my ($ms, $mountpoint) = @_;
1080 return if $used;
1081 $used = $mountpoint->{type} eq 'volume' && $mountpoint->{volume} eq $volid;
1082 });
1083
1084 my $snapshots = $config->{snapshots};
1085 if ($include_snapshots && $snapshots) {
1086 foreach my $snap (keys %$snapshots) {
1087 $used ||= $class->is_volume_in_use($snapshots->{$snap}, $volid);
1088 }
1089 }
1090
1091 return $used;
1092 }
1093
1094 sub has_dev_console {
1095 my ($class, $conf) = @_;
1096
1097 return !(defined($conf->{console}) && !$conf->{console});
1098 }
1099
1100 sub get_tty_count {
1101 my ($class, $conf) = @_;
1102
1103 return $conf->{tty} // $confdesc->{tty}->{default};
1104 }
1105
1106 sub get_cmode {
1107 my ($class, $conf) = @_;
1108
1109 return $conf->{cmode} // $confdesc->{cmode}->{default};
1110 }
1111
1112 sub mountpoint_names {
1113 my ($class, $reverse) = @_;
1114
1115 my @names = ('rootfs');
1116
1117 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
1118 push @names, "mp$i";
1119 }
1120
1121 return $reverse ? reverse @names : @names;
1122 }
1123
1124 sub foreach_mountpoint_full {
1125 my ($class, $conf, $reverse, $func, @param) = @_;
1126
1127 foreach my $key ($class->mountpoint_names($reverse)) {
1128 my $value = $conf->{$key};
1129 next if !defined($value);
1130 my $mountpoint = $key eq 'rootfs' ? $class->parse_ct_rootfs($value, 1) : $class->parse_ct_mountpoint($value, 1);
1131 next if !defined($mountpoint);
1132
1133 &$func($key, $mountpoint, @param);
1134 }
1135 }
1136
1137 sub foreach_mountpoint {
1138 my ($class, $conf, $func, @param) = @_;
1139
1140 $class->foreach_mountpoint_full($conf, 0, $func, @param);
1141 }
1142
1143 sub foreach_mountpoint_reverse {
1144 my ($class, $conf, $func, @param) = @_;
1145
1146 $class->foreach_mountpoint_full($conf, 1, $func, @param);
1147 }
1148
1149 sub get_vm_volumes {
1150 my ($class, $conf, $excludes) = @_;
1151
1152 my $vollist = [];
1153
1154 $class->foreach_mountpoint($conf, sub {
1155 my ($ms, $mountpoint) = @_;
1156
1157 return if $excludes && $ms eq $excludes;
1158
1159 my $volid = $mountpoint->{volume};
1160 return if !$volid || $mountpoint->{type} ne 'volume';
1161
1162 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
1163 return if !$sid;
1164
1165 push @$vollist, $volid;
1166 });
1167
1168 return $vollist;
1169 }
1170
1171 return 1;