]> git.proxmox.com Git - pve-container.git/blame - src/PVE/LXC.pm
add regression tests for inittab
[pve-container.git] / src / PVE / LXC.pm
CommitLineData
f76a2828
DM
1package PVE::LXC;
2
3use strict;
4use warnings;
5
6use File::Path;
7use Fcntl ':flock';
8
9use PVE::Cluster qw(cfs_register_file cfs_read_file);
10use PVE::SafeSyslog;
11use PVE::INotify;
55fa4e09 12use PVE::Tools qw($IPV6RE $IPV4RE);
f76a2828
DM
13
14use Data::Dumper;
15
16cfs_register_file('/lxc/', \&parse_lxc_config, \&write_lxc_config);
17
7dfc49cc
DM
18PVE::JSONSchema::register_format('pve-lxc-network', \&verify_lxc_network);
19sub verify_lxc_network {
20 my ($value, $noerr) = @_;
21
22 return $value if parse_lxc_network($value);
23
24 return undef if $noerr;
25
26 die "unable to parse network setting\n";
27}
28
f76a2828
DM
29my $nodename = PVE::INotify::nodename();
30
822de0c3
DM
31sub parse_lxc_size {
32 my ($name, $value) = @_;
33
34 if ($value =~ m/^(\d+)(b|k|m|g)?$/i) {
35 my ($res, $unit) = ($1, lc($2 || 'b'));
36
37 return $res if $unit eq 'b';
38 return $res*1024 if $unit eq 'k';
39 return $res*1024*1024 if $unit eq 'm';
40 return $res*1024*1024*1024 if $unit eq 'g';
41 }
42
43 return undef;
44}
45
f76a2828 46my $valid_lxc_keys = {
822de0c3 47 'lxc.arch' => 'i386|x86|i686|x86_64|amd64',
f76a2828
DM
48 'lxc.include' => 1,
49 'lxc.rootfs' => 1,
50 'lxc.mount' => 1,
51 'lxc.utsname' => 1,
52
54664cd3 53 'lxc.id_map' => 1,
822de0c3
DM
54
55 'lxc.cgroup.memory.limit_in_bytes' => \&parse_lxc_size,
56 'lxc.cgroup.memory.memsw.usage_in_bytes' => \&parse_lxc_size,
57
54664cd3
DM
58 # mount related
59 'lxc.mount' => 1,
60 'lxc.mount.entry' => 1,
61 'lxc.mount.auto' => 1,
62
63 # not used by pve
64 'lxc.tty' => 1,
65 'lxc.pts' => 1,
66 'lxc.haltsignal' => 1,
67 'lxc.rebootsignal' => 1,
68 'lxc.stopsignal' => 1,
69 'lxc.init_cmd' => 1,
70 'lxc.console' => 1,
71 'lxc.console.logfile' => 1,
72 'lxc.devttydir' => 1,
73 'lxc.autodev' => 1,
74 'lxc.kmsg' => 1,
75 'lxc.cap.drop' => 1,
76 'lxc.cap.keep' => 1,
77 'lxc.aa_profile' => 1,
78 'lxc.aa_allow_incomplete' => 1,
79 'lxc.se_context' => 1,
80 'lxc.loglevel' => 1,
81 'lxc.logfile' => 1,
82 'lxc.environment' => 1,
83
84
85 # autostart
86 'lxc.start.auto' => 1,
87 'lxc.start.delay' => 1,
88 'lxc.start.order' => 1,
89 'lxc.group' => 1,
90
91 # hooks
92 'lxc.hook.pre-start' => 1,
93 'lxc.hook.pre-mount' => 1,
94 'lxc.hook.mount' => 1,
95 'lxc.hook.autodev' => 1,
96 'lxc.hook.start' => 1,
97 'lxc.hook.post-stop' => 1,
98 'lxc.hook.clone' => 1,
99
f76a2828
DM
100 # pve related keys
101 'pve.comment' => 1,
102};
103
93285df8 104my $valid_lxc_network_keys = {
f76a2828 105 type => 1,
f76a2828
DM
106 link => 1,
107 mtu => 1,
108 name => 1, # ifname inside container
109 'veth.pair' => 1, # ifname at host (eth${vmid}.X)
110 hwaddr => 1,
93285df8
DM
111};
112
113my $valid_pve_network_keys = {
114 ip => 1,
115 gw => 1,
116 ip6 => 1,
117 gw6 => 1,
f76a2828
DM
118};
119
120my $lxc_array_configs = {
121 'lxc.network' => 1,
122 'lxc.mount' => 1,
123 'lxc.include' => 1,
124};
125
126sub write_lxc_config {
127 my ($filename, $data) = @_;
128
129 my $raw = "";
130
131 return $raw if !$data;
132
133 my $done_hash = { digest => 1};
7dfc49cc 134
f76a2828
DM
135 foreach my $k (sort keys %$data) {
136 next if $k !~ m/^lxc\./;
137 $done_hash->{$k} = 1;
138 $raw .= "$k = $data->{$k}\n";
139 }
7dfc49cc 140
f76a2828 141 foreach my $k (sort keys %$data) {
fff3a342
DM
142 next if $k !~ m/^pve\./;
143 $done_hash->{$k} = 1;
144 $raw .= "$k = $data->{$k}\n";
145 }
146
147 foreach my $k (sort keys %$data) {
f76a2828
DM
148 next if $k !~ m/^net\d+$/;
149 $done_hash->{$k} = 1;
150 my $net = $data->{$k};
151 $raw .= "lxc.network.type = $net->{type}\n";
152 foreach my $subkey (sort keys %$net) {
153 next if $subkey eq 'type';
93285df8
DM
154 if ($valid_lxc_network_keys->{$subkey}) {
155 $raw .= "lxc.network.$subkey = $net->{$subkey}\n";
156 } elsif ($valid_pve_network_keys->{$subkey}) {
157 $raw .= "pve.network.$subkey = $net->{$subkey}\n";
158 } else {
159 die "found invalid network key '$subkey'";
160 }
f76a2828
DM
161 }
162 }
163
164 foreach my $k (sort keys %$data) {
165 next if $done_hash->{$k};
166 die "found un-written value in config - implement this!";
167 }
168
f76a2828
DM
169 return $raw;
170}
171
822de0c3
DM
172sub parse_lxc_option {
173 my ($name, $value) = @_;
174
175 my $parser = $valid_lxc_keys->{$name};
176
177 die "inavlid key '$name'\n" if !defined($parser);
178
179 if ($parser eq '1') {
180 return $value;
181 } elsif (ref($parser)) {
182 my $res = &$parser($name, $value);
183 return $res if defined($res);
184 } else {
185 # assume regex
186 return $value if $value =~ m/^$parser$/;
187 }
188
189 die "unable to parse value '$value' for option '$name'\n";
190}
191
f76a2828
DM
192sub parse_lxc_config {
193 my ($filename, $raw) = @_;
194
195 return undef if !defined($raw);
196
197 my $data = {
198 digest => Digest::SHA::sha1_hex($raw),
199 };
200
201 $filename =~ m|/lxc/(\d+)/config$|
202 || die "got strange filename '$filename'";
203
204 my $vmid = $1;
205
206 my $network_counter = 0;
207 my $network_list = [];
208 my $host_ifnames = {};
209
210 my $find_next_hostif_name = sub {
211 for (my $i = 0; $i < 10; $i++) {
212 my $name = "veth${vmid}.$i";
213 if (!$host_ifnames->{$name}) {
214 $host_ifnames->{$name} = 1;
215 return $name;
216 }
217 }
218
219 die "unable to find free host_ifname"; # should not happen
220 };
7dfc49cc 221
f76a2828
DM
222 my $push_network = sub {
223 my ($netconf) = @_;
224 return if !$netconf;
225 push @{$network_list}, $netconf;
226 $network_counter++;
227 if (my $netname = $netconf->{'veth.pair'}) {
228 if ($netname =~ m/^veth(\d+).(\d)$/) {
229 die "wrong vmid for network interface pair\n" if $1 != $vmid;
230 my $host_ifnames->{$netname} = 1;
231 } else {
232 die "wrong network interface pair\n";
233 }
234 }
235 };
236
237 my $network;
7dfc49cc 238
f76a2828
DM
239 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
240 my $line = $1;
241
242 next if $line =~ m/^\#/;
243 next if $line =~ m/^\s*$/;
244
245 if ($line =~ m/^lxc\.network\.(\S+)\s*=\s*(\S+)\s*$/) {
246 my ($subkey, $value) = ($1, $2);
247 if ($subkey eq 'type') {
248 &$push_network($network);
249 $network = { type => $value };
93285df8
DM
250 } elsif ($valid_lxc_network_keys->{$subkey}) {
251 $network->{$subkey} = $value;
252 } else {
253 die "unable to parse config line: $line\n";
254 }
255 next;
256 }
257 if ($line =~ m/^pve\.network\.(\S+)\s*=\s*(\S+)\s*$/) {
258 my ($subkey, $value) = ($1, $2);
259 if ($valid_pve_network_keys->{$subkey}) {
f76a2828
DM
260 $network->{$subkey} = $value;
261 } else {
262 die "unable to parse config line: $line\n";
263 }
f76a2828
DM
264 next;
265 }
266 if ($line =~ m/^(pve.comment)\s*=\s*(\S.*)\s*$/) {
267 my ($name, $value) = ($1, $2);
268 $data->{$name} = $value;
269 next;
270 }
54664cd3 271 if ($line =~ m/^((?:pve|lxc)\.\S+)\s*=\s*(\S.*)\s*$/) {
f76a2828
DM
272 my ($name, $value) = ($1, $2);
273
f76a2828
DM
274 die "multiple definitions for $name\n" if defined($data->{$name});
275
822de0c3 276 $data->{$name} = parse_lxc_option($name, $value);
f76a2828
DM
277 next;
278 }
279
280 die "unable to parse config line: $line\n";
281 }
282
283 &$push_network($network);
284
285 foreach my $net (@{$network_list}) {
286 $net->{'veth.pair'} = &$find_next_hostif_name() if !$net->{'veth.pair'};
287 $net->{hwaddr} = PVE::Tools::random_ether_addr() if !$net->{hwaddr};
288 die "unsupported network type '$net->{type}'\n" if $net->{type} ne 'veth';
289
290 if ($net->{'veth.pair'} =~ m/^veth\d+.(\d+)$/) {
291 $data->{"net$1"} = $net;
292 }
7dfc49cc
DM
293 }
294
295 return $data;
f76a2828
DM
296}
297
298sub config_list {
299 my $vmlist = PVE::Cluster::get_vmlist();
300 my $res = {};
301 return $res if !$vmlist || !$vmlist->{ids};
302 my $ids = $vmlist->{ids};
303
304 foreach my $vmid (keys %$ids) {
305 next if !$vmid; # skip CT0
306 my $d = $ids->{$vmid};
307 next if !$d->{node} || $d->{node} ne $nodename;
308 next if !$d->{type} || $d->{type} ne 'lxc';
309 $res->{$vmid}->{type} = 'lxc';
310 }
311 return $res;
312}
313
314sub cfs_config_path {
315 my ($vmid, $node) = @_;
316
317 $node = $nodename if !$node;
318 return "nodes/$node/lxc/$vmid/config";
319}
320
9c2d4ce9
DM
321sub config_file {
322 my ($vmid, $node) = @_;
323
324 my $cfspath = cfs_config_path($vmid, $node);
325 return "/etc/pve/$cfspath";
326}
327
f76a2828
DM
328sub load_config {
329 my ($vmid) = @_;
330
331 my $cfspath = cfs_config_path($vmid);
332
333 my $conf = PVE::Cluster::cfs_read_file($cfspath);
334 die "container $vmid does not exists\n" if !defined($conf);
335
336 return $conf;
337}
338
339sub write_config {
340 my ($vmid, $conf) = @_;
341
342 my $cfspath = cfs_config_path($vmid);
343
344 PVE::Cluster::cfs_write_file($cfspath, $conf);
345}
346
9c2d4ce9
DM
347my $tempcounter = 0;
348sub write_temp_config {
349 my ($vmid, $conf) = @_;
7dfc49cc 350
9c2d4ce9
DM
351 $tempcounter++;
352 my $filename = "/tmp/temp-lxc-conf-$vmid-$$-$tempcounter.conf";
353
354 my $raw = write_lxc_config($filename, $conf);
355
356 PVE::Tools::file_set_contents($filename, $raw);
7dfc49cc 357
9c2d4ce9
DM
358 return $filename;
359}
360
f76a2828
DM
361sub lock_container {
362 my ($vmid, $timeout, $code, @param) = @_;
363
364 my $lockdir = "/run/lock/lxc";
365 my $lockfile = "$lockdir/pve-config-{$vmid}.lock";
366
367 File::Path::make_path($lockdir);
368
369 my $res = PVE::Tools::lock_file($lockfile, $timeout, $code, @param);
370
371 die $@ if $@;
372
373 return $res;
374}
375
376my $confdesc = {
377 onboot => {
378 optional => 1,
379 type => 'boolean',
380 description => "Specifies whether a VM will be started during system bootup.",
381 default => 0,
382 },
383 cpus => {
384 optional => 1,
385 type => 'integer',
386 description => "The number of CPUs for this container.",
387 minimum => 1,
388 default => 1,
389 },
390 cpuunits => {
391 optional => 1,
392 type => 'integer',
393 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 weights of all the other running VMs.\n\nNOTE: You can disable fair-scheduler configuration by setting this to 0.",
394 minimum => 0,
395 maximum => 500000,
396 default => 1000,
397 },
398 memory => {
399 optional => 1,
400 type => 'integer',
401 description => "Amount of RAM for the VM in MB.",
402 minimum => 16,
403 default => 512,
404 },
405 swap => {
406 optional => 1,
407 type => 'integer',
408 description => "Amount of SWAP for the VM in MB.",
409 minimum => 0,
410 default => 512,
411 },
412 disk => {
413 optional => 1,
414 type => 'number',
415 description => "Amount of disk space for the VM in GB. A zero indicates no limits.",
416 minimum => 0,
417 default => 2,
418 },
419 hostname => {
420 optional => 1,
421 description => "Set a host name for the container.",
422 type => 'string',
423 maxLength => 255,
424 },
425 description => {
426 optional => 1,
427 type => 'string',
428 description => "Container description. Only used on the configuration web interface.",
429 },
430 searchdomain => {
431 optional => 1,
432 type => 'string',
433 description => "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain or nameserver.",
434 },
435 nameserver => {
436 optional => 1,
437 type => 'string',
438 description => "Sets DNS server IP address for a container. Create will automatically use the setting from the host if you neither set searchdomain or nameserver.",
439 },
ec52ac21
DM
440};
441
442my $MAX_LXC_NETWORKS = 10;
443for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) {
444 $confdesc->{"net$i"} = {
f76a2828
DM
445 optional => 1,
446 type => 'string', format => 'pve-lxc-network',
447 description => "Specifies network interfaces for the container.",
7dfc49cc 448 };
ec52ac21
DM
449}
450
451sub option_exists {
452 my ($name) = @_;
453
454 return defined($confdesc->{$name});
455}
f76a2828
DM
456
457# add JSON properties for create and set function
458sub json_config_properties {
459 my $prop = shift;
460
461 foreach my $opt (keys %$confdesc) {
462 $prop->{$opt} = $confdesc->{$opt};
463 }
464
465 return $prop;
466}
467
822de0c3
DM
468# container status helpers
469
470sub list_active_containers {
471
472 my $filename = "/proc/net/unix";
473
474 # similar test is used by lcxcontainers.c: list_active_containers
475 my $res = {};
476
477 my $fh = IO::File->new ($filename, "r");
478 return $res if !$fh;
479
480 while (defined(my $line = <$fh>)) {
481 if ($line =~ m/^[a-f0-9]+:\s\S+\s\S+\s\S+\s\S+\s\S+\s\d+\s(\S+)$/) {
482 my $path = $1;
483 if ($path =~ m!^@/etc/pve/lxc/(\d+)/command$!) {
484 $res->{$1} = 1;
485 }
486 }
487 }
488
489 close($fh);
490
491 return $res;
492}
f76a2828 493
5c752bbf
DM
494# warning: this is slow
495sub check_running {
496 my ($vmid) = @_;
497
498 my $active_hash = list_active_containers();
499
500 return 1 if defined($active_hash->{$vmid});
501
502 return undef;
503}
504
f76a2828
DM
505sub vmstatus {
506 my ($opt_vmid) = @_;
507
508 my $list = $opt_vmid ? { $opt_vmid => { type => 'lxc' }} : config_list();
509
822de0c3
DM
510 my $active_hash = list_active_containers();
511
f76a2828 512 foreach my $vmid (keys %$list) {
f76a2828 513 my $d = $list->{$vmid};
822de0c3 514 $d->{status} = $active_hash->{$vmid} ? 'running' : 'stopped';
f76a2828
DM
515
516 my $cfspath = cfs_config_path($vmid);
238a56cb
DM
517 my $conf = PVE::Cluster::cfs_read_file($cfspath) || {};
518
519 $d->{name} = $conf->{'lxc.utsname'} || "CT$vmid";
520 $d->{name} =~ s/[\s]//g;
e901d418 521
238a56cb
DM
522 $d->{cpus} = 1; # fixme:
523
524 $d->{disk} = 0;
525 $d->{maxdisk} = 1;
526 if (my $private = $conf->{'lxc.rootfs'}) {
527 my $res = PVE::Tools::df($private, 2);
528 $d->{disk} = $res->{used};
529 $d->{maxdisk} = $res->{total};
530 }
531
532 $d->{mem} = 0;
533 $d->{swap} = 0;
534 $d->{maxmem} = ($conf->{'lxc.cgroup.memory.limit_in_bytes'}||0) +
535 ($conf->{'lxc.cgroup.memory.memsw.usage_in_bytes'}||0);
e901d418 536
238a56cb
DM
537 $d->{uptime} = 0;
538 $d->{cpu} = 0;
e901d418 539
238a56cb
DM
540 $d->{netout} = 0;
541 $d->{netin} = 0;
f76a2828 542
238a56cb
DM
543 $d->{diskread} = 0;
544 $d->{diskwrite} = 0;
f76a2828 545 }
238a56cb
DM
546
547 foreach my $vmid (keys %$list) {
548 my $d = $list->{$vmid};
549 next if $d->{status} ne 'running';
f76a2828 550
22a77285
DM
551 $d->{uptime} = 100; # fixme:
552
238a56cb
DM
553 $d->{mem} = read_cgroup_value('memory', $vmid, 'memory.usage_in_bytes');
554 $d->{swap} = read_cgroup_value('memory', $vmid, 'memory.memsw.usage_in_bytes') - $d->{mem};
555 }
556
f76a2828
DM
557 return $list;
558}
559
7dfc49cc
DM
560
561sub print_lxc_network {
f76a2828
DM
562 my $net = shift;
563
7dfc49cc 564 die "no network link defined\n" if !$net->{link};
f76a2828 565
7dfc49cc
DM
566 my $res = "link=$net->{link}";
567
93285df8 568 foreach my $k (qw(hwaddr mtu name ip gw ip6 gw6)) {
f76a2828
DM
569 next if !defined($net->{$k});
570 $res .= ",$k=$net->{$k}";
571 }
7dfc49cc 572
f76a2828
DM
573 return $res;
574}
575
7dfc49cc
DM
576sub parse_lxc_network {
577 my ($data) = @_;
578
579 my $res = {};
580
581 return $res if !$data;
582
583 foreach my $pv (split (/,/, $data)) {
93285df8 584 if ($pv =~ m/^(link|hwaddr|mtu|name|ip|ip6|gw|gw6)=(\S+)$/) {
7dfc49cc
DM
585 $res->{$1} = $2;
586 } else {
587 return undef;
588 }
589 }
590
591 $res->{type} = 'veth';
592 $res->{hwaddr} = PVE::Tools::random_ether_addr() if !$res->{mac};
593
594 return $res;
595}
f76a2828 596
238a56cb
DM
597sub read_cgroup_value {
598 my ($group, $vmid, $name, $full) = @_;
599
600 my $path = "/sys/fs/cgroup/$group/lxc/$vmid/$name";
601
602 return PVE::Tools::file_get_contents($path) if $full;
603
604 return PVE::Tools::file_read_firstline($path);
605}
606
52f1d76b
DM
607sub find_lxc_console_pids {
608
609 my $res = {};
610
611 PVE::Tools::dir_glob_foreach('/proc', '\d+', sub {
612 my ($pid) = @_;
613
614 my $cmdline = PVE::Tools::file_read_firstline("/proc/$pid/cmdline");
615 return if !$cmdline;
616
617 my @args = split(/\0/, $cmdline);
618
619 # serach for lxc-console -n <vmid>
620 return if scalar(@args) != 3;
621 return if $args[1] ne '-n';
622 return if $args[2] !~ m/^\d+$/;
623 return if $args[0] !~ m|^(/usr/bin/)?lxc-console$|;
624
625 my $vmid = $args[2];
626
627 push @{$res->{$vmid}}, $pid;
628 });
629
630 return $res;
631}
632
55fa4e09
DM
633my $ipv4_reverse_mask = [
634 '0.0.0.0',
635 '128.0.0.0',
636 '192.0.0.0',
637 '224.0.0.0',
638 '240.0.0.0',
639 '248.0.0.0',
640 '252.0.0.0',
641 '254.0.0.0',
642 '255.0.0.0',
643 '255.128.0.0',
644 '255.192.0.0',
645 '255.224.0.0',
646 '255.240.0.0',
647 '255.248.0.0',
648 '255.252.0.0',
649 '255.254.0.0',
650 '255.255.0.0',
651 '255.255.128.0',
652 '255.255.192.0',
653 '255.255.224.0',
654 '255.255.240.0',
655 '255.255.248.0',
656 '255.255.252.0',
657 '255.255.254.0',
658 '255.255.255.0',
659 '255.255.255.128',
660 '255.255.255.192',
661 '255.255.255.224',
662 '255.255.255.240',
663 '255.255.255.248',
664 '255.255.255.252',
665 '255.255.255.254',
666 '255.255.255.255',
667];
668
669# Note: we cannot use Net:IP, because that only allows strict
670# CIDR networks
671sub parse_ipv4_cidr {
672 my ($cidr, $noerr) = @_;
673
674 if ($cidr =~ m!^($IPV4RE)(?:/(\d+))$! && ($2 > 7) && ($2 < 32)) {
675 return { address => $1, netmask => $ipv4_reverse_mask->[$2] };
676 }
677
678 return undef if $noerr;
679
680 die "unable to parse ipv4 address/mask\n";
681}
93285df8
DM
682
683sub update_lxc_config {
684 my ($vmid, $conf, $running, $param, $delete) = @_;
685
686 # fixme: hotplug
687 die "unable to modify config while container is running\n" if $running;
688
689 if (defined($delete)) {
690 foreach my $opt (@$delete) {
691 if ($opt eq 'hostname' || $opt eq 'memory') {
692 die "unable to delete required option '$opt'\n";
693 } elsif ($opt eq 'swap') {
694 delete $conf->{'lxc.cgroup.memory.memsw.usage_in_bytes'};
695 } elsif ($opt eq 'description') {
696 delete $conf->{'pve.comment'};
697 } elsif ($opt =~ m/^net\d$/) {
698 delete $conf->{$opt};
699 } else {
700 die "implement me"
701 }
702 }
703 }
704
705 foreach my $opt (keys %$param) {
706 my $value = $param->{$opt};
707 if ($opt eq 'hostname') {
708 $conf->{'lxc.utsname'} = $value;
709 } elsif ($opt eq 'memory') {
710 $conf->{'lxc.cgroup.memory.limit_in_bytes'} = $value*1024*1024;
711 } elsif ($opt eq 'swap') {
712 $conf->{'lxc.cgroup.memory.memsw.usage_in_bytes'} = $value*1024*1024;
713 } elsif ($opt eq 'description') {
714 $conf->{'pve.comment'} = PVE::Tools::encode_text($value);
715 } elsif ($opt =~ m/^net(\d+)$/) {
716 my $netid = $1;
717 my $net = PVE::LXC::parse_lxc_network($value);
718 $net->{'veth.pair'} = "veth${vmid}.$netid";
719 $conf->{$opt} = $net;
720 } else {
721 die "implement me"
722 }
723 }
724}
238a56cb 725
f76a2828 7261;