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