]> git.proxmox.com Git - pve-container.git/blob - src/PVE/LXC/Config.pm
fix deletion of backup snapshots
[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 (not supported with bind mounts)',
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 optional => 1,
547 },
548 mp => {
549 type => 'string',
550 format => 'pve-lxc-mp-string',
551 format_description => 'Path',
552 description => 'Path to the mountpoint as seen from inside the container.',
553 },
554 };
555 PVE::JSONSchema::register_format('pve-ct-mountpoint', $mp_desc);
556
557 my $unuseddesc = {
558 optional => 1,
559 type => 'string', format => 'pve-volume-id',
560 description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
561 };
562
563 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
564 $confdesc->{"mp$i"} = {
565 optional => 1,
566 type => 'string', format => $mp_desc,
567 description => "Use volume as container mount point.",
568 optional => 1,
569 };
570 }
571
572 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
573 $confdesc->{"unused$i"} = $unuseddesc;
574 }
575
576 sub parse_pct_config {
577 my ($filename, $raw) = @_;
578
579 return undef if !defined($raw);
580
581 my $res = {
582 digest => Digest::SHA::sha1_hex($raw),
583 snapshots => {},
584 };
585
586 $filename =~ m|/lxc/(\d+).conf$|
587 || die "got strange filename '$filename'";
588
589 my $vmid = $1;
590
591 my $conf = $res;
592 my $descr = '';
593 my $section = '';
594
595 my @lines = split(/\n/, $raw);
596 foreach my $line (@lines) {
597 next if $line =~ m/^\s*$/;
598
599 if ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
600 $section = $1;
601 $conf->{description} = $descr if $descr;
602 $descr = '';
603 $conf = $res->{snapshots}->{$section} = {};
604 next;
605 }
606
607 if ($line =~ m/^\#(.*)\s*$/) {
608 $descr .= PVE::Tools::decode_text($1) . "\n";
609 next;
610 }
611
612 if ($line =~ m/^(lxc\.[a-z0-9_\-\.]+)(:|\s*=)\s*(.*?)\s*$/) {
613 my $key = $1;
614 my $value = $3;
615 my $validity = $valid_lxc_conf_keys->{$key} || 0;
616 if ($validity eq 1 || $key =~ m/^lxc\.cgroup\./) {
617 push @{$conf->{lxc}}, [$key, $value];
618 } elsif (my $errmsg = $validity) {
619 warn "vm $vmid - $key: $errmsg\n";
620 } else {
621 warn "vm $vmid - unable to parse config: $line\n";
622 }
623 } elsif ($line =~ m/^(description):\s*(.*\S)\s*$/) {
624 $descr .= PVE::Tools::decode_text($2);
625 } elsif ($line =~ m/snapstate:\s*(prepare|delete)\s*$/) {
626 $conf->{snapstate} = $1;
627 } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(\S.*)\s*$/) {
628 my $key = $1;
629 my $value = $2;
630 eval { $value = PVE::LXC::Config->check_type($key, $value); };
631 warn "vm $vmid - unable to parse value of '$key' - $@" if $@;
632 $conf->{$key} = $value;
633 } else {
634 warn "vm $vmid - unable to parse config: $line\n";
635 }
636 }
637
638 $conf->{description} = $descr if $descr;
639
640 delete $res->{snapstate}; # just to be sure
641
642 return $res;
643 }
644
645 sub write_pct_config {
646 my ($filename, $conf) = @_;
647
648 delete $conf->{snapstate}; # just to be sure
649
650 my $volidlist = PVE::LXC::Config->get_vm_volumes($conf);
651 my $used_volids = {};
652 foreach my $vid (@$volidlist) {
653 $used_volids->{$vid} = 1;
654 }
655
656 # remove 'unusedX' settings if the volume is still used
657 foreach my $key (keys %$conf) {
658 my $value = $conf->{$key};
659 if ($key =~ m/^unused/ && $used_volids->{$value}) {
660 delete $conf->{$key};
661 }
662 }
663
664 my $generate_raw_config = sub {
665 my ($conf) = @_;
666
667 my $raw = '';
668
669 # add description as comment to top of file
670 my $descr = $conf->{description} || '';
671 foreach my $cl (split(/\n/, $descr)) {
672 $raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
673 }
674
675 foreach my $key (sort keys %$conf) {
676 next if $key eq 'digest' || $key eq 'description' ||
677 $key eq 'pending' || $key eq 'snapshots' ||
678 $key eq 'snapname' || $key eq 'lxc';
679 my $value = $conf->{$key};
680 die "detected invalid newline inside property '$key'\n"
681 if $value =~ m/\n/;
682 $raw .= "$key: $value\n";
683 }
684
685 if (my $lxcconf = $conf->{lxc}) {
686 foreach my $entry (@$lxcconf) {
687 my ($k, $v) = @$entry;
688 $raw .= "$k: $v\n";
689 }
690 }
691
692 return $raw;
693 };
694
695 my $raw = &$generate_raw_config($conf);
696
697 foreach my $snapname (sort keys %{$conf->{snapshots}}) {
698 $raw .= "\n[$snapname]\n";
699 $raw .= &$generate_raw_config($conf->{snapshots}->{$snapname});
700 }
701
702 return $raw;
703 }
704
705 sub update_pct_config {
706 my ($class, $vmid, $conf, $running, $param, $delete) = @_;
707
708 my @nohotplug;
709
710 my $new_disks = 0;
711 my @deleted_volumes;
712
713 my $rootdir;
714 if ($running) {
715 my $pid = PVE::LXC::find_lxc_pid($vmid);
716 $rootdir = "/proc/$pid/root";
717 }
718
719 my $hotplug_error = sub {
720 if ($running) {
721 push @nohotplug, @_;
722 return 1;
723 } else {
724 return 0;
725 }
726 };
727
728 if (defined($delete)) {
729 foreach my $opt (@$delete) {
730 if (!exists($conf->{$opt})) {
731 warn "no such option: $opt\n";
732 next;
733 }
734
735 if ($opt eq 'hostname' || $opt eq 'memory' || $opt eq 'rootfs') {
736 die "unable to delete required option '$opt'\n";
737 } elsif ($opt eq 'swap') {
738 delete $conf->{$opt};
739 PVE::LXC::write_cgroup_value("memory", $vmid,
740 "memory.memsw.limit_in_bytes", -1);
741 } elsif ($opt eq 'description' || $opt eq 'onboot' || $opt eq 'startup') {
742 delete $conf->{$opt};
743 } elsif ($opt eq 'nameserver' || $opt eq 'searchdomain' ||
744 $opt eq 'tty' || $opt eq 'console' || $opt eq 'cmode') {
745 next if $hotplug_error->($opt);
746 delete $conf->{$opt};
747 } elsif ($opt eq 'cpulimit') {
748 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", -1);
749 delete $conf->{$opt};
750 } elsif ($opt eq 'cpuunits') {
751 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $confdesc->{cpuunits}->{default});
752 delete $conf->{$opt};
753 } elsif ($opt =~ m/^net(\d)$/) {
754 delete $conf->{$opt};
755 next if !$running;
756 my $netid = $1;
757 PVE::Network::veth_delete("veth${vmid}i$netid");
758 } elsif ($opt eq 'protection') {
759 delete $conf->{$opt};
760 } elsif ($opt =~ m/^unused(\d+)$/) {
761 next if $hotplug_error->($opt);
762 PVE::LXC::Config->check_protection($conf, "can't remove CT $vmid drive '$opt'");
763 push @deleted_volumes, $conf->{$opt};
764 delete $conf->{$opt};
765 } elsif ($opt =~ m/^mp(\d+)$/) {
766 next if $hotplug_error->($opt);
767 PVE::LXC::Config->check_protection($conf, "can't remove CT $vmid drive '$opt'");
768 my $mp = PVE::LXC::Config->parse_ct_mountpoint($conf->{$opt});
769 delete $conf->{$opt};
770 if ($mp->{type} eq 'volume') {
771 PVE::LXC::Config->add_unused_volume($conf, $mp->{volume});
772 }
773 } elsif ($opt eq 'unprivileged') {
774 die "unable to delete read-only option: '$opt'\n";
775 } else {
776 die "implement me (delete: $opt)"
777 }
778 PVE::LXC::Config->write_config($vmid, $conf) if $running;
779 }
780 }
781
782 # There's no separate swap size to configure, there's memory and "total"
783 # memory (iow. memory+swap). This means we have to change them together.
784 my $wanted_memory = PVE::Tools::extract_param($param, 'memory');
785 my $wanted_swap = PVE::Tools::extract_param($param, 'swap');
786 if (defined($wanted_memory) || defined($wanted_swap)) {
787
788 my $old_memory = ($conf->{memory} || 512);
789 my $old_swap = ($conf->{swap} || 0);
790
791 $wanted_memory //= $old_memory;
792 $wanted_swap //= $old_swap;
793
794 my $total = $wanted_memory + $wanted_swap;
795 if ($running) {
796 my $old_total = $old_memory + $old_swap;
797 if ($total > $old_total) {
798 PVE::LXC::write_cgroup_value("memory", $vmid,
799 "memory.memsw.limit_in_bytes",
800 int($total*1024*1024));
801 PVE::LXC::write_cgroup_value("memory", $vmid,
802 "memory.limit_in_bytes",
803 int($wanted_memory*1024*1024));
804 } else {
805 PVE::LXC::write_cgroup_value("memory", $vmid,
806 "memory.limit_in_bytes",
807 int($wanted_memory*1024*1024));
808 PVE::LXC::write_cgroup_value("memory", $vmid,
809 "memory.memsw.limit_in_bytes",
810 int($total*1024*1024));
811 }
812 }
813 $conf->{memory} = $wanted_memory;
814 $conf->{swap} = $wanted_swap;
815
816 PVE::LXC::Config->write_config($vmid, $conf) if $running;
817 }
818
819 my $used_volids = {};
820 my $check_content_type = sub {
821 my ($mp) = @_;
822 my $sid = PVE::Storage::parse_volume_id($mp->{volume});
823 my $scfg = PVE::Storage::config();
824 my $storage_config = PVE::Storage::storage_config($scfg, $sid);
825 die "storage '$sid' does not allow content type 'rootdir' (Container)\n"
826 if !$storage_config->{content}->{rootdir};
827 };
828
829 foreach my $opt (keys %$param) {
830 my $value = $param->{$opt};
831 my $check_protection_msg = "can't update CT $vmid drive '$opt'";
832 if ($opt eq 'hostname') {
833 $conf->{$opt} = $value;
834 } elsif ($opt eq 'onboot') {
835 $conf->{$opt} = $value ? 1 : 0;
836 } elsif ($opt eq 'startup') {
837 $conf->{$opt} = $value;
838 } elsif ($opt eq 'tty' || $opt eq 'console' || $opt eq 'cmode') {
839 next if $hotplug_error->($opt);
840 $conf->{$opt} = $value;
841 } elsif ($opt eq 'nameserver') {
842 next if $hotplug_error->($opt);
843 my $list = PVE::LXC::verify_nameserver_list($value);
844 $conf->{$opt} = $list;
845 } elsif ($opt eq 'searchdomain') {
846 next if $hotplug_error->($opt);
847 my $list = PVE::LXC::verify_searchdomain_list($value);
848 $conf->{$opt} = $list;
849 } elsif ($opt eq 'cpulimit') {
850 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", int(100000*$value));
851 $conf->{$opt} = $value;
852 } elsif ($opt eq 'cpuunits') {
853 $conf->{$opt} = $value;
854 PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $value);
855 } elsif ($opt eq 'description') {
856 $conf->{$opt} = PVE::Tools::encode_text($value);
857 } elsif ($opt =~ m/^net(\d+)$/) {
858 my $netid = $1;
859 my $net = PVE::LXC::Config->parse_lxc_network($value);
860 if (!$running) {
861 $conf->{$opt} = PVE::LXC::Config->print_lxc_network($net);
862 } else {
863 PVE::LXC::update_net($vmid, $conf, $opt, $net, $netid, $rootdir);
864 }
865 } elsif ($opt eq 'protection') {
866 $conf->{$opt} = $value ? 1 : 0;
867 } elsif ($opt =~ m/^mp(\d+)$/) {
868 next if $hotplug_error->($opt);
869 PVE::LXC::Config->check_protection($conf, $check_protection_msg);
870 my $old = $conf->{$opt};
871 my $mp = PVE::LXC::Config->parse_ct_mountpoint($value);
872 if ($mp->{type} eq 'volume') {
873 &$check_content_type($mp);
874 $used_volids->{$mp->{volume}} = 1;
875 }
876 $conf->{$opt} = $value;
877 if (defined($old)) {
878 my $mp = PVE::LXC::Config->parse_ct_mountpoint($old);
879 if ($mp->{type} eq 'volume') {
880 PVE::LXC::Config->add_unused_volume($conf, $mp->{volume});
881 }
882 }
883 $new_disks = 1;
884 } elsif ($opt eq 'rootfs') {
885 next if $hotplug_error->($opt);
886 PVE::LXC::Config->check_protection($conf, $check_protection_msg);
887 my $old = $conf->{$opt};
888 $conf->{$opt} = $value;
889 my $mp = PVE::LXC::Config->parse_ct_rootfs($value);
890 if ($mp->{type} eq 'volume') {
891 &$check_content_type($mp);
892 $used_volids->{$mp->{volume}} = 1;
893 }
894 if (defined($old)) {
895 my $mp = PVE::LXC::Config->parse_ct_rootfs($old);
896 if ($mp->{type} eq 'volume') {
897 PVE::LXC::Config->add_unused_volume($conf, $mp->{volume});
898 }
899 }
900 $new_disks = 1;
901 } elsif ($opt eq 'unprivileged') {
902 die "unable to modify read-only option: '$opt'\n";
903 } elsif ($opt eq 'ostype') {
904 next if $hotplug_error->($opt);
905 $conf->{$opt} = $value;
906 } else {
907 die "implement me: $opt";
908 }
909 PVE::LXC::Config->write_config($vmid, $conf) if $running;
910 }
911
912 # Apply deletions and creations of new volumes
913 if (@deleted_volumes) {
914 my $storage_cfg = PVE::Storage::config();
915 foreach my $volume (@deleted_volumes) {
916 next if $used_volids->{$volume}; # could have been re-added, too
917 # also check for references in snapshots
918 next if $class->is_volume_in_use($conf, $volume, 1);
919 PVE::LXC::delete_mountpoint_volume($storage_cfg, $vmid, $volume);
920 }
921 }
922
923 if ($new_disks) {
924 my $storage_cfg = PVE::Storage::config();
925 PVE::LXC::create_disks($storage_cfg, $vmid, $conf, $conf);
926 }
927
928 # This should be the last thing we do here
929 if ($running && scalar(@nohotplug)) {
930 die "unable to modify " . join(',', @nohotplug) . " while container is running\n";
931 }
932 }
933
934 sub check_type {
935 my ($class, $key, $value) = @_;
936
937 die "unknown setting '$key'\n" if !$confdesc->{$key};
938
939 my $type = $confdesc->{$key}->{type};
940
941 if (!defined($value)) {
942 die "got undefined value\n";
943 }
944
945 if ($value =~ m/[\n\r]/) {
946 die "property contains a line feed\n";
947 }
948
949 if ($type eq 'boolean') {
950 return 1 if ($value eq '1') || ($value =~ m/^(on|yes|true)$/i);
951 return 0 if ($value eq '0') || ($value =~ m/^(off|no|false)$/i);
952 die "type check ('boolean') failed - got '$value'\n";
953 } elsif ($type eq 'integer') {
954 return int($1) if $value =~ m/^(\d+)$/;
955 die "type check ('integer') failed - got '$value'\n";
956 } elsif ($type eq 'number') {
957 return $value if $value =~ m/^(\d+)(\.\d+)?$/;
958 die "type check ('number') failed - got '$value'\n";
959 } elsif ($type eq 'string') {
960 if (my $fmt = $confdesc->{$key}->{format}) {
961 PVE::JSONSchema::check_format($fmt, $value);
962 return $value;
963 }
964 return $value;
965 } else {
966 die "internal error"
967 }
968 }
969
970
971 # add JSON properties for create and set function
972 sub json_config_properties {
973 my ($class, $prop) = @_;
974
975 foreach my $opt (keys %$confdesc) {
976 next if $opt eq 'parent' || $opt eq 'snaptime';
977 next if $prop->{$opt};
978 $prop->{$opt} = $confdesc->{$opt};
979 }
980
981 return $prop;
982 }
983
984 sub __parse_ct_mountpoint_full {
985 my ($class, $desc, $data, $noerr) = @_;
986
987 $data //= '';
988
989 my $res;
990 eval { $res = PVE::JSONSchema::parse_property_string($desc, $data) };
991 if ($@) {
992 return undef if $noerr;
993 die $@;
994 }
995
996 if (defined(my $size = $res->{size})) {
997 $size = PVE::JSONSchema::parse_size($size);
998 if (!defined($size)) {
999 return undef if $noerr;
1000 die "invalid size: $size\n";
1001 }
1002 $res->{size} = $size;
1003 }
1004
1005 $res->{type} = $class->classify_mountpoint($res->{volume});
1006
1007 return $res;
1008 };
1009
1010 sub parse_ct_rootfs {
1011 my ($class, $data, $noerr) = @_;
1012
1013 my $res = $class->__parse_ct_mountpoint_full($rootfs_desc, $data, $noerr);
1014
1015 $res->{mp} = '/' if defined($res);
1016
1017 return $res;
1018 }
1019
1020 sub parse_ct_mountpoint {
1021 my ($class, $data, $noerr) = @_;
1022
1023 return $class->__parse_ct_mountpoint_full($mp_desc, $data, $noerr);
1024 }
1025
1026 sub print_ct_mountpoint {
1027 my ($class, $info, $nomp) = @_;
1028 my $skip = [ 'type' ];
1029 push @$skip, 'mp' if $nomp;
1030 return PVE::JSONSchema::print_property_string($info, $mp_desc, $skip);
1031 }
1032
1033 sub print_lxc_network {
1034 my ($class, $net) = @_;
1035 return PVE::JSONSchema::print_property_string($net, $netconf_desc);
1036 }
1037
1038 sub parse_lxc_network {
1039 my ($class, $data) = @_;
1040
1041 my $res = {};
1042
1043 return $res if !$data;
1044
1045 $res = PVE::JSONSchema::parse_property_string($netconf_desc, $data);
1046
1047 $res->{type} = 'veth';
1048 $res->{hwaddr} = PVE::Tools::random_ether_addr() if !$res->{hwaddr};
1049
1050 return $res;
1051 }
1052
1053 sub option_exists {
1054 my ($class, $name) = @_;
1055
1056 return defined($confdesc->{$name});
1057 }
1058 # END JSON config code
1059
1060 sub classify_mountpoint {
1061 my ($class, $vol) = @_;
1062 if ($vol =~ m!^/!) {
1063 return 'device' if $vol =~ m!^/dev/!;
1064 return 'bind';
1065 }
1066 return 'volume';
1067 }
1068
1069 sub is_volume_in_use {
1070 my ($class, $config, $volid, $include_snapshots) = @_;
1071 my $used = 0;
1072
1073 $class->foreach_mountpoint($config, sub {
1074 my ($ms, $mountpoint) = @_;
1075 return if $used;
1076 $used = $mountpoint->{type} eq 'volume' && $mountpoint->{volume} eq $volid;
1077 });
1078
1079 my $snapshots = $config->{snapshots};
1080 if ($include_snapshots && $snapshots) {
1081 foreach my $snap (keys %$snapshots) {
1082 $used ||= $class->is_volume_in_use($snapshots->{$snap}, $volid);
1083 }
1084 }
1085
1086 return $used;
1087 }
1088
1089 sub has_dev_console {
1090 my ($class, $conf) = @_;
1091
1092 return !(defined($conf->{console}) && !$conf->{console});
1093 }
1094
1095 sub get_tty_count {
1096 my ($class, $conf) = @_;
1097
1098 return $conf->{tty} // $confdesc->{tty}->{default};
1099 }
1100
1101 sub get_cmode {
1102 my ($class, $conf) = @_;
1103
1104 return $conf->{cmode} // $confdesc->{cmode}->{default};
1105 }
1106
1107 sub mountpoint_names {
1108 my ($class, $reverse) = @_;
1109
1110 my @names = ('rootfs');
1111
1112 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
1113 push @names, "mp$i";
1114 }
1115
1116 return $reverse ? reverse @names : @names;
1117 }
1118
1119 sub foreach_mountpoint_full {
1120 my ($class, $conf, $reverse, $func) = @_;
1121
1122 foreach my $key ($class->mountpoint_names($reverse)) {
1123 my $value = $conf->{$key};
1124 next if !defined($value);
1125 my $mountpoint = $key eq 'rootfs' ? $class->parse_ct_rootfs($value, 1) : $class->parse_ct_mountpoint($value, 1);
1126 next if !defined($mountpoint);
1127
1128 &$func($key, $mountpoint);
1129 }
1130 }
1131
1132 sub foreach_mountpoint {
1133 my ($class, $conf, $func) = @_;
1134
1135 $class->foreach_mountpoint_full($conf, 0, $func);
1136 }
1137
1138 sub foreach_mountpoint_reverse {
1139 my ($class, $conf, $func) = @_;
1140
1141 $class->foreach_mountpoint_full($conf, 1, $func);
1142 }
1143
1144 sub get_vm_volumes {
1145 my ($class, $conf, $excludes) = @_;
1146
1147 my $vollist = [];
1148
1149 $class->foreach_mountpoint($conf, sub {
1150 my ($ms, $mountpoint) = @_;
1151
1152 return if $excludes && $ms eq $excludes;
1153
1154 my $volid = $mountpoint->{volume};
1155 return if !$volid || $mountpoint->{type} ne 'volume';
1156
1157 my ($sid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
1158 return if !$sid;
1159
1160 push @$vollist, $volid;
1161 });
1162
1163 return $vollist;
1164 }
1165
1166 return 1;