]> git.proxmox.com Git - pve-container.git/blob - src/PVE/LXC.pm
add mount hook, chnage network config for ip setup
[pve-container.git] / src / PVE / LXC.pm
1 package PVE::LXC;
2
3 use strict;
4 use warnings;
5
6 use File::Path;
7 use Fcntl ':flock';
8
9 use PVE::Cluster qw(cfs_register_file cfs_read_file);
10 use PVE::SafeSyslog;
11 use PVE::INotify;
12 use PVE::Tools qw($IPV6RE $IPV4RE);
13
14 use Data::Dumper;
15
16 cfs_register_file('/lxc/', \&parse_lxc_config, \&write_lxc_config);
17
18 PVE::JSONSchema::register_format('pve-lxc-network', \&verify_lxc_network);
19 sub 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
29 my $nodename = PVE::INotify::nodename();
30
31 sub 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
46 my $valid_lxc_keys = {
47 'lxc.arch' => 'i386|x86|i686|x86_64|amd64',
48 'lxc.include' => 1,
49 'lxc.rootfs' => 1,
50 'lxc.mount' => 1,
51 'lxc.utsname' => 1,
52
53 'lxc.id_map' => 1,
54
55 'lxc.cgroup.memory.limit_in_bytes' => \&parse_lxc_size,
56 'lxc.cgroup.memory.memsw.usage_in_bytes' => \&parse_lxc_size,
57
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
100 # pve related keys
101 'pve.comment' => 1,
102 };
103
104 my $valid_lxc_network_keys = {
105 type => 1,
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,
111 };
112
113 my $valid_pve_network_keys = {
114 ip => 1,
115 gw => 1,
116 ip6 => 1,
117 gw6 => 1,
118 };
119
120 my $lxc_array_configs = {
121 'lxc.network' => 1,
122 'lxc.mount' => 1,
123 'lxc.include' => 1,
124 };
125
126 sub write_lxc_config {
127 my ($filename, $data) = @_;
128
129 my $raw = "";
130
131 return $raw if !$data;
132
133 my $done_hash = { digest => 1};
134
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 }
140
141 foreach my $k (sort keys %$data) {
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) {
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';
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 }
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
169 return $raw;
170 }
171
172 sub 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
192 sub 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 };
221
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;
238
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 };
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}) {
260 $network->{$subkey} = $value;
261 } else {
262 die "unable to parse config line: $line\n";
263 }
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 }
271 if ($line =~ m/^((?:pve|lxc)\.\S+)\s*=\s*(\S.*)\s*$/) {
272 my ($name, $value) = ($1, $2);
273
274 die "multiple definitions for $name\n" if defined($data->{$name});
275
276 $data->{$name} = parse_lxc_option($name, $value);
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 }
293 }
294
295 return $data;
296 }
297
298 sub 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
314 sub cfs_config_path {
315 my ($vmid, $node) = @_;
316
317 $node = $nodename if !$node;
318 return "nodes/$node/lxc/$vmid/config";
319 }
320
321 sub config_file {
322 my ($vmid, $node) = @_;
323
324 my $cfspath = cfs_config_path($vmid, $node);
325 return "/etc/pve/$cfspath";
326 }
327
328 sub 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
339 sub 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
347 my $tempcounter = 0;
348 sub write_temp_config {
349 my ($vmid, $conf) = @_;
350
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);
357
358 return $filename;
359 }
360
361 sub 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
376 my $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 },
440 };
441
442 my $MAX_LXC_NETWORKS = 10;
443 for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) {
444 $confdesc->{"net$i"} = {
445 optional => 1,
446 type => 'string', format => 'pve-lxc-network',
447 description => "Specifies network interfaces for the container.",
448 };
449 }
450
451 sub option_exists {
452 my ($name) = @_;
453
454 return defined($confdesc->{$name});
455 }
456
457 # add JSON properties for create and set function
458 sub 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
468 # container status helpers
469
470 sub 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 }
493
494 # warning: this is slow
495 sub 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
505 sub vmstatus {
506 my ($opt_vmid) = @_;
507
508 my $list = $opt_vmid ? { $opt_vmid => { type => 'lxc' }} : config_list();
509
510 my $active_hash = list_active_containers();
511
512 foreach my $vmid (keys %$list) {
513 my $d = $list->{$vmid};
514 $d->{status} = $active_hash->{$vmid} ? 'running' : 'stopped';
515
516 my $cfspath = cfs_config_path($vmid);
517 my $conf = PVE::Cluster::cfs_read_file($cfspath) || {};
518
519 $d->{name} = $conf->{'lxc.utsname'} || "CT$vmid";
520 $d->{name} =~ s/[\s]//g;
521
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);
536
537 $d->{uptime} = 0;
538 $d->{cpu} = 0;
539
540 $d->{netout} = 0;
541 $d->{netin} = 0;
542
543 $d->{diskread} = 0;
544 $d->{diskwrite} = 0;
545 }
546
547 foreach my $vmid (keys %$list) {
548 my $d = $list->{$vmid};
549 next if $d->{status} ne 'running';
550
551 $d->{uptime} = 100; # fixme:
552
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
557 return $list;
558 }
559
560
561 sub print_lxc_network {
562 my $net = shift;
563
564 die "no network link defined\n" if !$net->{link};
565
566 my $res = "link=$net->{link}";
567
568 foreach my $k (qw(hwaddr mtu name ip gw ip6 gw6)) {
569 next if !defined($net->{$k});
570 $res .= ",$k=$net->{$k}";
571 }
572
573 return $res;
574 }
575
576 sub parse_lxc_network {
577 my ($data) = @_;
578
579 my $res = {};
580
581 return $res if !$data;
582
583 foreach my $pv (split (/,/, $data)) {
584 if ($pv =~ m/^(link|hwaddr|mtu|name|ip|ip6|gw|gw6)=(\S+)$/) {
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 }
596
597 sub 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
607 sub 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
633 my $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
671 sub 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 }
682
683 sub 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 }
725
726 1;