]> git.proxmox.com Git - pve-container.git/blob - src/PVE/LXC.pm
LXC: more compact network configuration
[pve-container.git] / src / PVE / LXC.pm
1 package PVE::LXC;
2
3 use strict;
4 use warnings;
5 use POSIX qw(EINTR);
6
7 use File::Path;
8 use Fcntl ':flock';
9
10 use PVE::Cluster qw(cfs_register_file cfs_read_file);
11 use PVE::Storage;
12 use PVE::SafeSyslog;
13 use PVE::INotify;
14 use PVE::JSONSchema qw(get_standard_option);
15 use PVE::Tools qw($IPV6RE $IPV4RE);
16 use PVE::Network;
17
18 use Data::Dumper;
19
20 cfs_register_file('/lxc/', \&parse_lxc_config, \&write_lxc_config);
21
22 PVE::JSONSchema::register_format('pve-lxc-network', \&verify_lxc_network);
23 sub verify_lxc_network {
24 my ($value, $noerr) = @_;
25
26 return $value if parse_lxc_network($value);
27
28 return undef if $noerr;
29
30 die "unable to parse network setting\n";
31 }
32
33 my $nodename = PVE::INotify::nodename();
34
35 sub parse_lxc_size {
36 my ($name, $value) = @_;
37
38 if ($value =~ m/^(\d+)(b|k|m|g)?$/i) {
39 my ($res, $unit) = ($1, lc($2 || 'b'));
40
41 return $res if $unit eq 'b';
42 return $res*1024 if $unit eq 'k';
43 return $res*1024*1024 if $unit eq 'm';
44 return $res*1024*1024*1024 if $unit eq 'g';
45 }
46
47 return undef;
48 }
49
50 my $valid_lxc_keys = {
51 'lxc.arch' => 'i386|x86|i686|x86_64|amd64',
52 'lxc.include' => 1,
53 'lxc.rootfs' => 1,
54 'lxc.mount' => 1,
55 'lxc.utsname' => 1,
56
57 'lxc.id_map' => 1,
58
59 'lxc.cgroup.memory.limit_in_bytes' => \&parse_lxc_size,
60 'lxc.cgroup.memory.memsw.limit_in_bytes' => \&parse_lxc_size,
61 'lxc.cgroup.cpu.cfs_period_us' => '\d+',
62 'lxc.cgroup.cpu.cfs_quota_us' => '\d+',
63 'lxc.cgroup.cpu.shares' => '\d+',
64
65 # mount related
66 'lxc.mount' => 1,
67 'lxc.mount.entry' => 1,
68 'lxc.mount.auto' => 1,
69
70 # not used by pve
71 'lxc.tty' => '\d+',
72 'lxc.pts' => 1,
73 'lxc.haltsignal' => 1,
74 'lxc.rebootsignal' => 1,
75 'lxc.stopsignal' => 1,
76 'lxc.init_cmd' => 1,
77 'lxc.console' => 1,
78 'lxc.console.logfile' => 1,
79 'lxc.devttydir' => 1,
80 'lxc.autodev' => 1,
81 'lxc.kmsg' => 1,
82 'lxc.cap.drop' => 1,
83 'lxc.cap.keep' => 1,
84 'lxc.aa_profile' => 1,
85 'lxc.aa_allow_incomplete' => 1,
86 'lxc.se_context' => 1,
87 'lxc.loglevel' => 1,
88 'lxc.logfile' => 1,
89 'lxc.environment' => 1,
90 'lxc.cgroup.devices.deny' => 1,
91
92 # autostart
93 'lxc.start.auto' => 1,
94 'lxc.start.delay' => 1,
95 'lxc.start.order' => 1,
96 'lxc.group' => 1,
97
98 # hooks
99 'lxc.hook.pre-start' => 1,
100 'lxc.hook.pre-mount' => 1,
101 'lxc.hook.mount' => 1,
102 'lxc.hook.autodev' => 1,
103 'lxc.hook.start' => 1,
104 'lxc.hook.post-stop' => 1,
105 'lxc.hook.clone' => 1,
106
107 # pve related keys
108 'pve.nameserver' => sub {
109 my ($name, $value) = @_;
110 return verify_nameserver_list($value);
111 },
112 'pve.searchdomain' => sub {
113 my ($name, $value) = @_;
114 return verify_searchdomain_list($value);
115 },
116 'pve.onboot' => '(0|1)',
117 'pve.startup' => sub {
118 my ($name, $value) = @_;
119 return PVE::JSONSchema::pve_verify_startup_order($value);
120 },
121 'pve.comment' => 1,
122 'pve.disksize' => '\d+(\.\d+)?',
123 'pve.volid' => sub {
124 my ($name, $value) = @_;
125 PVE::Storage::parse_volume_id($value);
126 return $value;
127 },
128 };
129
130 my $valid_lxc_network_keys = {
131 type => 1,
132 mtu => 1,
133 name => 1, # ifname inside container
134 'veth.pair' => 1, # ifname at host (eth${vmid}.X)
135 hwaddr => 1,
136 };
137
138 my $valid_pve_network_keys = {
139 bridge => 1,
140 tag => 1,
141 firewall => 1,
142 ip => 1,
143 gw => 1,
144 ip6 => 1,
145 gw6 => 1,
146 };
147
148 my $lxc_array_configs = {
149 'lxc.network' => 1,
150 'lxc.mount' => 1,
151 'lxc.include' => 1,
152 'lxc.id_map' => 1,
153 'lxc.cgroup.devices.deny' => 1,
154 };
155
156 sub write_lxc_config {
157 my ($filename, $data) = @_;
158
159 my $raw = "";
160
161 return $raw if !$data;
162
163 my $done_hash = { digest => 1};
164
165 my $dump_entry = sub {
166 my ($k) = @_;
167 my $value = $data->{$k};
168 return if !defined($value);
169 return if $done_hash->{$k};
170 $done_hash->{$k} = 1;
171 if (ref($value)) {
172 die "got unexpected reference for '$k'"
173 if !$lxc_array_configs->{$k};
174 foreach my $v (@$value) {
175 $raw .= "$k = $v\n";
176 }
177 } else {
178 $raw .= "$k = $value\n";
179 }
180 };
181
182 # Note: Order is important! Include defaults first, so that we
183 # can overwrite them later.
184 &$dump_entry('lxc.include');
185
186 foreach my $k (sort keys %$data) {
187 next if $k !~ m/^lxc\./;
188 &$dump_entry($k);
189 }
190
191 foreach my $k (sort keys %$data) {
192 next if $k !~ m/^pve\./;
193 &$dump_entry($k);
194 }
195
196 my $network_count = 0;
197 foreach my $k (sort keys %$data) {
198 next if $k !~ m/^net\d+$/;
199 $done_hash->{$k} = 1;
200 my $net = $data->{$k};
201 $network_count++;
202 $raw .= "lxc.network.type = $net->{type}\n";
203 foreach my $subkey (sort keys %$net) {
204 next if $subkey eq 'type';
205 if ($valid_lxc_network_keys->{$subkey}) {
206 $raw .= "lxc.network.$subkey = $net->{$subkey}\n";
207 } elsif ($valid_pve_network_keys->{$subkey}) {
208 $raw .= "pve.network.$subkey = $net->{$subkey}\n";
209 } else {
210 die "found invalid network key '$subkey'";
211 }
212 }
213 }
214
215 if (!$network_count) {
216 $raw .= "lxc.network.type = empty\n";
217 }
218
219 foreach my $k (sort keys %$data) {
220 next if $done_hash->{$k};
221 die "found un-written value in config - implement this!";
222 }
223
224 return $raw;
225 }
226
227 sub parse_lxc_option {
228 my ($name, $value) = @_;
229
230 my $parser = $valid_lxc_keys->{$name};
231
232 die "invalid key '$name'\n" if !defined($parser);
233
234 if ($parser eq '1') {
235 return $value;
236 } elsif (ref($parser)) {
237 my $res = &$parser($name, $value);
238 return $res if defined($res);
239 } else {
240 # assume regex
241 return $value if $value =~ m/^$parser$/;
242 }
243
244 die "unable to parse value '$value' for option '$name'\n";
245 }
246
247 sub parse_lxc_config {
248 my ($filename, $raw) = @_;
249
250 return undef if !defined($raw);
251
252 my $data = {
253 digest => Digest::SHA::sha1_hex($raw),
254 };
255
256 $filename =~ m|/lxc/(\d+)/config$|
257 || die "got strange filename '$filename'";
258
259 my $vmid = $1;
260
261 my $network_counter = 0;
262 my $network_list = [];
263 my $host_ifnames = {};
264
265 my $find_next_hostif_name = sub {
266 for (my $i = 0; $i < 10; $i++) {
267 my $name = "veth${vmid}.$i";
268 if (!$host_ifnames->{$name}) {
269 $host_ifnames->{$name} = 1;
270 return $name;
271 }
272 }
273
274 die "unable to find free host_ifname"; # should not happen
275 };
276
277 my $push_network = sub {
278 my ($netconf) = @_;
279 return if !$netconf;
280 push @{$network_list}, $netconf;
281 $network_counter++;
282 if (my $netname = $netconf->{'veth.pair'}) {
283 if ($netname =~ m/^veth(\d+).(\d)$/) {
284 die "wrong vmid for network interface pair\n" if $1 != $vmid;
285 my $host_ifnames->{$netname} = 1;
286 } else {
287 die "wrong network interface pair\n";
288 }
289 }
290 };
291
292 my $network;
293
294 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
295 my $line = $1;
296
297 next if $line =~ m/^\#/;
298 next if $line =~ m/^\s*$/;
299
300 if ($line =~ m/^lxc\.network\.(\S+)\s*=\s*(\S+)\s*$/) {
301 my ($subkey, $value) = ($1, $2);
302 if ($subkey eq 'type') {
303 &$push_network($network);
304 $network = { type => $value };
305 } elsif ($valid_lxc_network_keys->{$subkey}) {
306 $network->{$subkey} = $value;
307 } else {
308 die "unable to parse config line: $line\n";
309 }
310 next;
311 }
312 if ($line =~ m/^pve\.network\.(\S+)\s*=\s*(\S+)\s*$/) {
313 my ($subkey, $value) = ($1, $2);
314 if ($valid_pve_network_keys->{$subkey}) {
315 $network->{$subkey} = $value;
316 } else {
317 die "unable to parse config line: $line\n";
318 }
319 next;
320 }
321 if ($line =~ m/^(pve.comment)\s*=\s*(\S.*)\s*$/) {
322 my ($name, $value) = ($1, $2);
323 $data->{$name} = $value;
324 next;
325 }
326 if ($line =~ m/^((?:pve|lxc)\.\S+)\s*=\s*(\S.*)\s*$/) {
327 my ($name, $value) = ($1, $2);
328
329 if ($lxc_array_configs->{$name}) {
330 $data->{$name} = [] if !defined($data->{$name});
331 push @{$data->{$name}}, parse_lxc_option($name, $value);
332 } else {
333 die "multiple definitions for $name\n" if defined($data->{$name});
334 $data->{$name} = parse_lxc_option($name, $value);
335 }
336
337 next;
338 }
339
340 die "unable to parse config line: $line\n";
341 }
342
343 &$push_network($network);
344
345 foreach my $net (@{$network_list}) {
346 next if $net->{type} eq 'empty'; # skip
347 $net->{'veth.pair'} = &$find_next_hostif_name() if !$net->{'veth.pair'};
348 $net->{hwaddr} = PVE::Tools::random_ether_addr() if !$net->{hwaddr};
349 die "unsupported network type '$net->{type}'\n" if $net->{type} ne 'veth';
350
351 if ($net->{'veth.pair'} =~ m/^veth\d+.(\d+)$/) {
352 $data->{"net$1"} = $net;
353 }
354 }
355
356 return $data;
357 }
358
359 sub config_list {
360 my $vmlist = PVE::Cluster::get_vmlist();
361 my $res = {};
362 return $res if !$vmlist || !$vmlist->{ids};
363 my $ids = $vmlist->{ids};
364
365 foreach my $vmid (keys %$ids) {
366 next if !$vmid; # skip CT0
367 my $d = $ids->{$vmid};
368 next if !$d->{node} || $d->{node} ne $nodename;
369 next if !$d->{type} || $d->{type} ne 'lxc';
370 $res->{$vmid}->{type} = 'lxc';
371 }
372 return $res;
373 }
374
375 sub cfs_config_path {
376 my ($vmid, $node) = @_;
377
378 $node = $nodename if !$node;
379 return "nodes/$node/lxc/$vmid/config";
380 }
381
382 sub config_file {
383 my ($vmid, $node) = @_;
384
385 my $cfspath = cfs_config_path($vmid, $node);
386 return "/etc/pve/$cfspath";
387 }
388
389 sub load_config {
390 my ($vmid) = @_;
391
392 my $cfspath = cfs_config_path($vmid);
393
394 my $conf = PVE::Cluster::cfs_read_file($cfspath);
395 die "container $vmid does not exists\n" if !defined($conf);
396
397 return $conf;
398 }
399
400 sub create_config {
401 my ($vmid, $conf) = @_;
402
403 my $dir = "/etc/pve/nodes/$nodename/lxc";
404 mkdir $dir;
405
406 $dir .= "/$vmid";
407 mkdir($dir) || die "unable to create container configuration directory - $!\n";
408
409 write_config($vmid, $conf);
410 }
411
412 sub destroy_config {
413 my ($vmid) = @_;
414
415 my $dir = "/etc/pve/nodes/$nodename/lxc/$vmid";
416 File::Path::rmtree($dir);
417 }
418
419 sub write_config {
420 my ($vmid, $conf) = @_;
421
422 my $cfspath = cfs_config_path($vmid);
423
424 PVE::Cluster::cfs_write_file($cfspath, $conf);
425 }
426
427 my $tempcounter = 0;
428 sub write_temp_config {
429 my ($vmid, $conf) = @_;
430
431 $tempcounter++;
432 my $filename = "/tmp/temp-lxc-conf-$vmid-$$-$tempcounter.conf";
433
434 my $raw = write_lxc_config($filename, $conf);
435
436 PVE::Tools::file_set_contents($filename, $raw);
437
438 return $filename;
439 }
440
441 # flock: we use one file handle per process, so lock file
442 # can be called multiple times and succeeds for the same process.
443
444 my $lock_handles = {};
445 my $lockdir = "/run/lock/lxc";
446
447 sub lock_filename {
448 my ($vmid) = @_;
449
450 return "$lockdir/pve-config-{$vmid}.lock";
451 }
452
453 sub lock_aquire {
454 my ($vmid, $timeout) = @_;
455
456 $timeout = 10 if !$timeout;
457 my $mode = LOCK_EX;
458
459 my $filename = lock_filename($vmid);
460
461 mkdir $lockdir if !-d $lockdir;
462
463 my $lock_func = sub {
464 if (!$lock_handles->{$$}->{$filename}) {
465 my $fh = new IO::File(">>$filename") ||
466 die "can't open file - $!\n";
467 $lock_handles->{$$}->{$filename} = { fh => $fh, refcount => 0};
468 }
469
470 if (!flock($lock_handles->{$$}->{$filename}->{fh}, $mode |LOCK_NB)) {
471 print STDERR "trying to aquire lock...";
472 my $success;
473 while(1) {
474 $success = flock($lock_handles->{$$}->{$filename}->{fh}, $mode);
475 # try again on EINTR (see bug #273)
476 if ($success || ($! != EINTR)) {
477 last;
478 }
479 }
480 if (!$success) {
481 print STDERR " failed\n";
482 die "can't aquire lock - $!\n";
483 }
484
485 $lock_handles->{$$}->{$filename}->{refcount}++;
486
487 print STDERR " OK\n";
488 }
489 };
490
491 eval { PVE::Tools::run_with_timeout($timeout, $lock_func); };
492 my $err = $@;
493 if ($err) {
494 die "can't lock file '$filename' - $err";
495 }
496 }
497
498 sub lock_release {
499 my ($vmid) = @_;
500
501 my $filename = lock_filename($vmid);
502
503 if (my $fh = $lock_handles->{$$}->{$filename}->{fh}) {
504 my $refcount = --$lock_handles->{$$}->{$filename}->{refcount};
505 if ($refcount <= 0) {
506 $lock_handles->{$$}->{$filename} = undef;
507 close ($fh);
508 }
509 }
510 }
511
512 sub lock_container {
513 my ($vmid, $timeout, $code, @param) = @_;
514
515 my $res;
516
517 lock_aquire($vmid, $timeout);
518 eval { $res = &$code(@param) };
519 my $err = $@;
520 lock_release($vmid);
521
522 die $err if $err;
523
524 return $res;
525 }
526
527 my $confdesc = {
528 onboot => {
529 optional => 1,
530 type => 'boolean',
531 description => "Specifies whether a VM will be started during system bootup.",
532 default => 0,
533 },
534 startup => get_standard_option('pve-startup-order'),
535 cpulimit => {
536 optional => 1,
537 type => 'number',
538 description => "Limit of CPU usage. Note if the computer has 2 CPUs, it has total of '2' CPU time. Value '0' indicates no CPU limit.",
539 minimum => 0,
540 maximum => 128,
541 default => 0,
542 },
543 cpuunits => {
544 optional => 1,
545 type => 'integer',
546 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.",
547 minimum => 0,
548 maximum => 500000,
549 default => 1000,
550 },
551 memory => {
552 optional => 1,
553 type => 'integer',
554 description => "Amount of RAM for the VM in MB.",
555 minimum => 16,
556 default => 512,
557 },
558 swap => {
559 optional => 1,
560 type => 'integer',
561 description => "Amount of SWAP for the VM in MB.",
562 minimum => 0,
563 default => 512,
564 },
565 disk => {
566 optional => 1,
567 type => 'number',
568 description => "Amount of disk space for the VM in GB. A zero indicates no limits.",
569 minimum => 0,
570 default => 4,
571 },
572 hostname => {
573 optional => 1,
574 description => "Set a host name for the container.",
575 type => 'string',
576 maxLength => 255,
577 },
578 description => {
579 optional => 1,
580 type => 'string',
581 description => "Container description. Only used on the configuration web interface.",
582 },
583 searchdomain => {
584 optional => 1,
585 type => 'string',
586 description => "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain or nameserver.",
587 },
588 nameserver => {
589 optional => 1,
590 type => 'string',
591 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.",
592 },
593 };
594
595 my $MAX_LXC_NETWORKS = 10;
596 for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) {
597 $confdesc->{"net$i"} = {
598 optional => 1,
599 type => 'string', format => 'pve-lxc-network',
600 description => "Specifies network interfaces for the container.\n\n".
601 "The string should have the follow format:\n\n".
602 "-net<[0-9]> bridge=<vmbr<Nummber>>[,hwaddr=<MAC>]\n".
603 "[,mtu=<Number>][,name=<String>][,ip=<IPv4Format/CIDR>]\n".
604 ",ip6=<IPv6Format/CIDR>][,gw=<GatwayIPv4>]\n".
605 ",gw6=<GatwayIPv6>][,firewall=<[1|0]>][,tag=<VlanNo>]",
606 };
607 }
608
609 sub option_exists {
610 my ($name) = @_;
611
612 return defined($confdesc->{$name});
613 }
614
615 # add JSON properties for create and set function
616 sub json_config_properties {
617 my $prop = shift;
618
619 foreach my $opt (keys %$confdesc) {
620 $prop->{$opt} = $confdesc->{$opt};
621 }
622
623 return $prop;
624 }
625
626 # container status helpers
627
628 sub list_active_containers {
629
630 my $filename = "/proc/net/unix";
631
632 # similar test is used by lcxcontainers.c: list_active_containers
633 my $res = {};
634
635 my $fh = IO::File->new ($filename, "r");
636 return $res if !$fh;
637
638 while (defined(my $line = <$fh>)) {
639 if ($line =~ m/^[a-f0-9]+:\s\S+\s\S+\s\S+\s\S+\s\S+\s\d+\s(\S+)$/) {
640 my $path = $1;
641 if ($path =~ m!^@/etc/pve/lxc/(\d+)/command$!) {
642 $res->{$1} = 1;
643 }
644 }
645 }
646
647 close($fh);
648
649 return $res;
650 }
651
652 # warning: this is slow
653 sub check_running {
654 my ($vmid) = @_;
655
656 my $active_hash = list_active_containers();
657
658 return 1 if defined($active_hash->{$vmid});
659
660 return undef;
661 }
662
663 sub get_container_disk_usage {
664 my ($vmid) = @_;
665
666 my $cmd = ['lxc-attach', '-n', $vmid, '--', 'df', '-P', '-B', '1', '/'];
667
668 my $res = {
669 total => 0,
670 used => 0,
671 avail => 0,
672 };
673
674 my $parser = sub {
675 my $line = shift;
676 if (my ($fsid, $total, $used, $avail) = $line =~
677 m/^(\S+.*)\s+(\d+)\s+(\d+)\s+(\d+)\s+\d+%\s.*$/) {
678 $res = {
679 total => $total,
680 used => $used,
681 avail => $avail,
682 };
683 }
684 };
685 eval { PVE::Tools::run_command($cmd, timeout => 1, outfunc => $parser); };
686 warn $@ if $@;
687
688 return $res;
689 }
690
691 sub vmstatus {
692 my ($opt_vmid) = @_;
693
694 my $list = $opt_vmid ? { $opt_vmid => { type => 'lxc' }} : config_list();
695
696 my $active_hash = list_active_containers();
697
698 foreach my $vmid (keys %$list) {
699 my $d = $list->{$vmid};
700
701 my $running = defined($active_hash->{$vmid});
702
703 $d->{status} = $running ? 'running' : 'stopped';
704
705 my $cfspath = cfs_config_path($vmid);
706 my $conf = PVE::Cluster::cfs_read_file($cfspath) || {};
707
708 $d->{name} = $conf->{'lxc.utsname'} || "CT$vmid";
709 $d->{name} =~ s/[\s]//g;
710
711 $d->{cpus} = 0;
712
713 my $cfs_period_us = $conf->{'lxc.cgroup.cpu.cfs_period_us'};
714 my $cfs_quota_us = $conf->{'lxc.cgroup.cpu.cfs_quota_us'};
715
716 if ($cfs_period_us && $cfs_quota_us) {
717 $d->{cpus} = int($cfs_quota_us/$cfs_period_us);
718 }
719
720 $d->{disk} = 0;
721 $d->{maxdisk} = defined($conf->{'pve.disksize'}) ?
722 int($conf->{'pve.disksize'}*1024*1024)*1024 : 1024*1024*1024*1024*1024;
723
724 if (my $private = $conf->{'lxc.rootfs'}) {
725 if ($private =~ m!^/!) {
726 my $res = PVE::Tools::df($private, 2);
727 $d->{disk} = $res->{used};
728 $d->{maxdisk} = $res->{total};
729 } elsif ($running) {
730 if ($private =~ m!^(?:loop|nbd):(?:\S+)$!) {
731 my $res = get_container_disk_usage($vmid);
732 $d->{disk} = $res->{used};
733 $d->{maxdisk} = $res->{total};
734 }
735 }
736 }
737
738 $d->{mem} = 0;
739 $d->{swap} = 0;
740 $d->{maxmem} = ($conf->{'lxc.cgroup.memory.limit_in_bytes'}||0) +
741 ($conf->{'lxc.cgroup.memory.memsw.limit_in_bytes'}||0);
742
743 $d->{uptime} = 0;
744 $d->{cpu} = 0;
745
746 $d->{netout} = 0;
747 $d->{netin} = 0;
748
749 $d->{diskread} = 0;
750 $d->{diskwrite} = 0;
751 }
752
753 foreach my $vmid (keys %$list) {
754 my $d = $list->{$vmid};
755 next if $d->{status} ne 'running';
756
757 $d->{uptime} = 100; # fixme:
758
759 $d->{mem} = read_cgroup_value('memory', $vmid, 'memory.usage_in_bytes');
760 $d->{swap} = read_cgroup_value('memory', $vmid, 'memory.memsw.usage_in_bytes') - $d->{mem};
761
762 my $blkio_bytes = read_cgroup_value('blkio', $vmid, 'blkio.throttle.io_service_bytes', 1);
763 my @bytes = split(/\n/, $blkio_bytes);
764 foreach my $byte (@bytes) {
765 if (my ($key, $value) = $byte =~ /(Read|Write)\s+(\d+)/) {
766 $d->{diskread} = $2 if $key eq 'Read';
767 $d->{diskwrite} = $2 if $key eq 'Write';
768 }
769 }
770 }
771
772 return $list;
773 }
774
775
776 sub print_lxc_network {
777 my $net = shift;
778
779 die "no network name defined\n" if !$net->{name};
780
781 my $res = "name=$net->{name}";
782
783 foreach my $k (qw(hwaddr mtu bridge ip gw ip6 gw6 firewall tag)) {
784 next if !defined($net->{$k});
785 $res .= ",$k=$net->{$k}";
786 }
787
788 return $res;
789 }
790
791 sub parse_lxc_network {
792 my ($data) = @_;
793
794 my $res = {};
795
796 return $res if !$data;
797
798 foreach my $pv (split (/,/, $data)) {
799 if ($pv =~ m/^(bridge|hwaddr|mtu|name|ip|ip6|gw|gw6|firewall|tag)=(\S+)$/) {
800 $res->{$1} = $2;
801 } else {
802 return undef;
803 }
804 }
805
806 $res->{type} = 'veth';
807 $res->{hwaddr} = PVE::Tools::random_ether_addr() if !$res->{hwaddr};
808
809 return $res;
810 }
811
812 sub read_cgroup_value {
813 my ($group, $vmid, $name, $full) = @_;
814
815 my $path = "/sys/fs/cgroup/$group/lxc/$vmid/$name";
816
817 return PVE::Tools::file_get_contents($path) if $full;
818
819 return PVE::Tools::file_read_firstline($path);
820 }
821
822 sub write_cgroup_value {
823 my ($group, $vmid, $name, $value) = @_;
824
825 my $path = "/sys/fs/cgroup/$group/lxc/$vmid/$name";
826 PVE::ProcFSTools::write_proc_entry($path, $value) if -e $path;
827
828 }
829
830 sub find_lxc_console_pids {
831
832 my $res = {};
833
834 PVE::Tools::dir_glob_foreach('/proc', '\d+', sub {
835 my ($pid) = @_;
836
837 my $cmdline = PVE::Tools::file_read_firstline("/proc/$pid/cmdline");
838 return if !$cmdline;
839
840 my @args = split(/\0/, $cmdline);
841
842 # serach for lxc-console -n <vmid>
843 return if scalar(@args) != 3;
844 return if $args[1] ne '-n';
845 return if $args[2] !~ m/^\d+$/;
846 return if $args[0] !~ m|^(/usr/bin/)?lxc-console$|;
847
848 my $vmid = $args[2];
849
850 push @{$res->{$vmid}}, $pid;
851 });
852
853 return $res;
854 }
855
856 sub find_lxc_pid {
857 my ($vmid) = @_;
858
859 my $pid = undef;
860 my $parser = sub {
861 my $line = shift;
862 $pid = $1 if $line =~ m/^PID:\s+(\d+)$/;
863 };
864 PVE::Tools::run_command(['lxc-info', '-n', $vmid], outfunc => $parser);
865
866 die "unable to get PID for CT $vmid (not running?)\n" if !$pid;
867
868 return $pid;
869 }
870
871 my $ipv4_reverse_mask = [
872 '0.0.0.0',
873 '128.0.0.0',
874 '192.0.0.0',
875 '224.0.0.0',
876 '240.0.0.0',
877 '248.0.0.0',
878 '252.0.0.0',
879 '254.0.0.0',
880 '255.0.0.0',
881 '255.128.0.0',
882 '255.192.0.0',
883 '255.224.0.0',
884 '255.240.0.0',
885 '255.248.0.0',
886 '255.252.0.0',
887 '255.254.0.0',
888 '255.255.0.0',
889 '255.255.128.0',
890 '255.255.192.0',
891 '255.255.224.0',
892 '255.255.240.0',
893 '255.255.248.0',
894 '255.255.252.0',
895 '255.255.254.0',
896 '255.255.255.0',
897 '255.255.255.128',
898 '255.255.255.192',
899 '255.255.255.224',
900 '255.255.255.240',
901 '255.255.255.248',
902 '255.255.255.252',
903 '255.255.255.254',
904 '255.255.255.255',
905 ];
906
907 # Note: we cannot use Net:IP, because that only allows strict
908 # CIDR networks
909 sub parse_ipv4_cidr {
910 my ($cidr, $noerr) = @_;
911
912 if ($cidr =~ m!^($IPV4RE)(?:/(\d+))$! && ($2 > 7) && ($2 < 32)) {
913 return { address => $1, netmask => $ipv4_reverse_mask->[$2] };
914 }
915
916 return undef if $noerr;
917
918 die "unable to parse ipv4 address/mask\n";
919 }
920
921 sub lxc_conf_to_pve {
922 my ($vmid, $lxc_conf) = @_;
923
924 my $properties = json_config_properties();
925
926 my $conf = { digest => $lxc_conf->{digest} };
927
928 foreach my $k (keys %$properties) {
929
930 if ($k eq 'description') {
931 if (my $raw = $lxc_conf->{'pve.comment'}) {
932 $conf->{$k} = PVE::Tools::decode_text($raw);
933 }
934 } elsif ($k eq 'onboot') {
935 $conf->{$k} = $lxc_conf->{'pve.onboot'} if $lxc_conf->{'pve.onboot'};
936 } elsif ($k eq 'startup') {
937 $conf->{$k} = $lxc_conf->{'pve.startup'} if $lxc_conf->{'pve.startup'};
938 } elsif ($k eq 'hostname') {
939 $conf->{$k} = $lxc_conf->{'lxc.utsname'} if $lxc_conf->{'lxc.utsname'};
940 } elsif ($k eq 'nameserver') {
941 $conf->{$k} = $lxc_conf->{'pve.nameserver'} if $lxc_conf->{'pve.nameserver'};
942 } elsif ($k eq 'searchdomain') {
943 $conf->{$k} = $lxc_conf->{'pve.searchdomain'} if $lxc_conf->{'pve.searchdomain'};
944 } elsif ($k eq 'memory') {
945 if (my $value = $lxc_conf->{'lxc.cgroup.memory.limit_in_bytes'}) {
946 $conf->{$k} = int($value / (1024*1024));
947 }
948 } elsif ($k eq 'swap') {
949 if (my $value = $lxc_conf->{'lxc.cgroup.memory.memsw.limit_in_bytes'}) {
950 my $mem = $lxc_conf->{'lxc.cgroup.memory.limit_in_bytes'} || 0;
951 $conf->{$k} = int(($value -$mem) / (1024*1024));
952 }
953 } elsif ($k eq 'cpulimit') {
954 my $cfs_period_us = $lxc_conf->{'lxc.cgroup.cpu.cfs_period_us'};
955 my $cfs_quota_us = $lxc_conf->{'lxc.cgroup.cpu.cfs_quota_us'};
956
957 if ($cfs_period_us && $cfs_quota_us) {
958 $conf->{$k} = $cfs_quota_us/$cfs_period_us;
959 } else {
960 $conf->{$k} = 0;
961 }
962 } elsif ($k eq 'cpuunits') {
963 $conf->{$k} = $lxc_conf->{'lxc.cgroup.cpu.shares'} || 1024;
964 } elsif ($k eq 'disk') {
965 $conf->{$k} = defined($lxc_conf->{'pve.disksize'}) ?
966 $lxc_conf->{'pve.disksize'} : 0;
967 } elsif ($k =~ m/^net\d$/) {
968 my $net = $lxc_conf->{$k};
969 next if !$net;
970 $conf->{$k} = print_lxc_network($net);
971 }
972 }
973
974 return $conf;
975 }
976
977 # verify and cleanup nameserver list (replace \0 with ' ')
978 sub verify_nameserver_list {
979 my ($nameserver_list) = @_;
980
981 my @list = ();
982 foreach my $server (PVE::Tools::split_list($nameserver_list)) {
983 PVE::JSONSchema::pve_verify_ip($server);
984 push @list, $server;
985 }
986
987 return join(' ', @list);
988 }
989
990 sub verify_searchdomain_list {
991 my ($searchdomain_list) = @_;
992
993 my @list = ();
994 foreach my $server (PVE::Tools::split_list($searchdomain_list)) {
995 # todo: should we add checks for valid dns domains?
996 push @list, $server;
997 }
998
999 return join(' ', @list);
1000 }
1001
1002 sub update_lxc_config {
1003 my ($vmid, $conf, $running, $param, $delete) = @_;
1004
1005 my @nohotplug;
1006
1007 my $rootdir;
1008 if ($running) {
1009 my $pid = find_lxc_pid($vmid);
1010 $rootdir = "/proc/$pid/root";
1011 }
1012
1013 if (defined($delete)) {
1014 foreach my $opt (@$delete) {
1015 if ($opt eq 'hostname' || $opt eq 'memory') {
1016 die "unable to delete required option '$opt'\n";
1017 } elsif ($opt eq 'swap') {
1018 delete $conf->{'lxc.cgroup.memory.memsw.limit_in_bytes'};
1019 write_cgroup_value("memory", $vmid, "memory.memsw.limit_in_bytes", -1);
1020 } elsif ($opt eq 'description') {
1021 delete $conf->{'pve.comment'};
1022 } elsif ($opt eq 'onboot') {
1023 delete $conf->{'pve.onboot'};
1024 } elsif ($opt eq 'startup') {
1025 delete $conf->{'pve.startup'};
1026 } elsif ($opt eq 'nameserver') {
1027 delete $conf->{'pve.nameserver'};
1028 push @nohotplug, $opt;
1029 next if $running;
1030 } elsif ($opt eq 'searchdomain') {
1031 delete $conf->{'pve.searchdomain'};
1032 push @nohotplug, $opt;
1033 next if $running;
1034 } elsif ($opt =~ m/^net(\d)$/) {
1035 delete $conf->{$opt};
1036 next if !$running;
1037 my $netid = $1;
1038 PVE::Network::veth_delete("veth${vmid}.$netid");
1039 } else {
1040 die "implement me"
1041 }
1042 PVE::LXC::write_config($vmid, $conf) if $running;
1043 }
1044 }
1045
1046 foreach my $opt (keys %$param) {
1047 my $value = $param->{$opt};
1048 if ($opt eq 'hostname') {
1049 $conf->{'lxc.utsname'} = $value;
1050 } elsif ($opt eq 'onboot') {
1051 $conf->{'pve.onboot'} = $value ? 1 : 0;
1052 } elsif ($opt eq 'startup') {
1053 $conf->{'pve.startup'} = $value;
1054 } elsif ($opt eq 'nameserver') {
1055 my $list = verify_nameserver_list($value);
1056 $conf->{'pve.nameserver'} = $list;
1057 push @nohotplug, $opt;
1058 next if $running;
1059 } elsif ($opt eq 'searchdomain') {
1060 my $list = verify_searchdomain_list($value);
1061 $conf->{'pve.searchdomain'} = $list;
1062 push @nohotplug, $opt;
1063 next if $running;
1064 } elsif ($opt eq 'memory') {
1065 $conf->{'lxc.cgroup.memory.limit_in_bytes'} = $value*1024*1024;
1066 write_cgroup_value("memory", $vmid, "memory.limit_in_bytes", $value*1024*1024);
1067 } elsif ($opt eq 'swap') {
1068 my $mem = $conf->{'lxc.cgroup.memory.limit_in_bytes'};
1069 $mem = $param->{memory}*1024*1024 if $param->{memory};
1070 $conf->{'lxc.cgroup.memory.memsw.limit_in_bytes'} = $mem + $value*1024*1024;
1071 write_cgroup_value("memory", $vmid, "memory.memsw.limit_in_bytes", $mem + $value*1024*1024);
1072
1073 } elsif ($opt eq 'cpulimit') {
1074 if ($value > 0) {
1075 my $cfs_period_us = 100000;
1076 $conf->{'lxc.cgroup.cpu.cfs_period_us'} = $cfs_period_us;
1077 $conf->{'lxc.cgroup.cpu.cfs_quota_us'} = $cfs_period_us*$value;
1078 write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", $cfs_period_us*$value);
1079 } else {
1080 delete $conf->{'lxc.cgroup.cpu.cfs_period_us'};
1081 delete $conf->{'lxc.cgroup.cpu.cfs_quota_us'};
1082 write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", -1);
1083 }
1084 } elsif ($opt eq 'cpuunits') {
1085 $conf->{'lxc.cgroup.cpu.shares'} = $value;
1086 write_cgroup_value("cpu", $vmid, "cpu.shares", $value);
1087 } elsif ($opt eq 'description') {
1088 $conf->{'pve.comment'} = PVE::Tools::encode_text($value);
1089 } elsif ($opt eq 'disk') {
1090 $conf->{'pve.disksize'} = $value;
1091 push @nohotplug, $opt;
1092 next if $running;
1093 } elsif ($opt =~ m/^net(\d+)$/) {
1094 my $netid = $1;
1095 my $net = PVE::LXC::parse_lxc_network($value);
1096 $net->{'veth.pair'} = "veth${vmid}.$netid";
1097 if (!$running) {
1098 $conf->{$opt} = $net;
1099 } else {
1100 update_net($vmid, $conf, $opt, $net, $netid, $rootdir);
1101 }
1102 } else {
1103 die "implement me"
1104 }
1105 PVE::LXC::write_config($vmid, $conf) if $running;
1106 }
1107
1108 if ($running && scalar(@nohotplug)) {
1109 die "unable to modify " . join(',', @nohotplug) . " while container is running\n";
1110 }
1111 }
1112
1113 sub get_primary_ips {
1114 my ($conf) = @_;
1115
1116 # return data from net0
1117
1118 my $net = $conf->{net0};
1119 return undef if !$net;
1120
1121 my $ipv4 = $net->{ip};
1122 $ipv4 =~ s!/\d+$!! if $ipv4;
1123 my $ipv6 = $net->{ip};
1124 $ipv6 =~ s!/\d+$!! if $ipv6;
1125
1126 return ($ipv4, $ipv6);
1127 }
1128
1129 sub destory_lxc_container {
1130 my ($storage_cfg, $vmid, $conf) = @_;
1131
1132 if (my $volid = $conf->{'pve.volid'}) {
1133
1134 my ($vtype, $name, $owner) = PVE::Storage::parse_volname($storage_cfg, $volid);
1135 die "got strange volid (containe is not owner!)\n" if $vmid != $owner;
1136
1137 PVE::Storage::vdisk_free($storage_cfg, $volid);
1138
1139 destroy_config($vmid);
1140
1141 } else {
1142 my $cmd = ['lxc-destroy', '-n', $vmid ];
1143
1144 PVE::Tools::run_command($cmd);
1145 }
1146 }
1147
1148 my $safe_num_ne = sub {
1149 my ($a, $b) = @_;
1150
1151 return 0 if !defined($a) && !defined($b);
1152 return 1 if !defined($a);
1153 return 1 if !defined($b);
1154
1155 return $a != $b;
1156 };
1157
1158 my $safe_string_ne = sub {
1159 my ($a, $b) = @_;
1160
1161 return 0 if !defined($a) && !defined($b);
1162 return 1 if !defined($a);
1163 return 1 if !defined($b);
1164
1165 return $a ne $b;
1166 };
1167
1168 sub update_net {
1169 my ($vmid, $conf, $opt, $newnet, $netid, $rootdir) = @_;
1170
1171 my $veth = $newnet->{'veth.pair'};
1172 my $vethpeer = $veth . "p";
1173 my $eth = $newnet->{name};
1174
1175 if ($conf->{$opt}) {
1176 if (&$safe_string_ne($conf->{$opt}->{hwaddr}, $newnet->{hwaddr}) ||
1177 &$safe_string_ne($conf->{$opt}->{name}, $newnet->{name})) {
1178
1179 PVE::Network::veth_delete($veth);
1180 delete $conf->{$opt};
1181 PVE::LXC::write_config($vmid, $conf);
1182
1183 hotplug_net($vmid, $conf, $opt, $newnet);
1184
1185 } elsif (&$safe_string_ne($conf->{$opt}->{bridge}, $newnet->{bridge}) ||
1186 &$safe_num_ne($conf->{$opt}->{tag}, $newnet->{tag}) ||
1187 &$safe_num_ne($conf->{$opt}->{firewall}, $newnet->{firewall})) {
1188
1189 if ($conf->{$opt}->{bridge}){
1190 PVE::Network::tap_unplug($veth);
1191 delete $conf->{$opt}->{bridge};
1192 delete $conf->{$opt}->{tag};
1193 delete $conf->{$opt}->{firewall};
1194 PVE::LXC::write_config($vmid, $conf);
1195 }
1196
1197 PVE::Network::tap_plug($veth, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall});
1198 $conf->{$opt}->{bridge} = $newnet->{bridge} if $newnet->{bridge};
1199 $conf->{$opt}->{tag} = $newnet->{tag} if $newnet->{tag};
1200 $conf->{$opt}->{firewall} = $newnet->{firewall} if $newnet->{firewall};
1201 PVE::LXC::write_config($vmid, $conf);
1202 }
1203 } else {
1204 hotplug_net($vmid, $conf, $opt, $newnet);
1205 }
1206
1207 update_ipconfig($vmid, $conf, $opt, $eth, $newnet, $rootdir);
1208 }
1209
1210 sub hotplug_net {
1211 my ($vmid, $conf, $opt, $newnet) = @_;
1212
1213 my $veth = $newnet->{'veth.pair'};
1214 my $vethpeer = $veth . "p";
1215 my $eth = $newnet->{name};
1216
1217 PVE::Network::veth_create($veth, $vethpeer, $newnet->{bridge}, $newnet->{hwaddr});
1218 PVE::Network::tap_plug($veth, $newnet->{bridge}, $newnet->{tag}, $newnet->{firewall});
1219
1220 # attach peer in container
1221 my $cmd = ['lxc-device', '-n', $vmid, 'add', $vethpeer, "$eth" ];
1222 PVE::Tools::run_command($cmd);
1223
1224 # link up peer in container
1225 $cmd = ['lxc-attach', '-n', $vmid, '-s', 'NETWORK', '--', '/sbin/ip', 'link', 'set', $eth ,'up' ];
1226 PVE::Tools::run_command($cmd);
1227
1228 $conf->{$opt}->{type} = 'veth';
1229 $conf->{$opt}->{bridge} = $newnet->{bridge} if $newnet->{bridge};
1230 $conf->{$opt}->{tag} = $newnet->{tag} if $newnet->{tag};
1231 $conf->{$opt}->{firewall} = $newnet->{firewall} if $newnet->{firewall};
1232 $conf->{$opt}->{hwaddr} = $newnet->{hwaddr} if $newnet->{hwaddr};
1233 $conf->{$opt}->{name} = $newnet->{name} if $newnet->{name};
1234 $conf->{$opt}->{'veth.pair'} = $newnet->{'veth.pair'} if $newnet->{'veth.pair'};
1235
1236 delete $conf->{$opt}->{ip} if $conf->{$opt}->{ip};
1237 delete $conf->{$opt}->{ip6} if $conf->{$opt}->{ip6};
1238 delete $conf->{$opt}->{gw} if $conf->{$opt}->{gw};
1239 delete $conf->{$opt}->{gw6} if $conf->{$opt}->{gw6};
1240
1241 PVE::LXC::write_config($vmid, $conf);
1242 }
1243
1244 sub update_ipconfig {
1245 my ($vmid, $conf, $opt, $eth, $newnet, $rootdir) = @_;
1246
1247 my $lxc_setup = PVE::LXCSetup->new($conf, $rootdir);
1248
1249 my $optdata = $conf->{$opt};
1250 my $deleted = [];
1251 my $added = [];
1252 my $netcmd = sub {
1253 my $cmd = ['lxc-attach', '-n', $vmid, '-s', 'NETWORK', '--', '/sbin/ip', @_];
1254 PVE::Tools::run_command($cmd);
1255 };
1256
1257 my $change_ip_config = sub {
1258 my ($family_opt, $suffix) = @_;
1259 $suffix = '' if !$suffix;
1260 my $gw= "gw$suffix";
1261 my $ip= "ip$suffix";
1262
1263 my $change_ip = &$safe_string_ne($optdata->{$ip}, $newnet->{$ip});
1264 my $change_gw = &$safe_string_ne($optdata->{$gw}, $newnet->{$gw});
1265
1266 return if !$change_ip && !$change_gw;
1267
1268 # step 1: add new IP, if this fails we cancel
1269 if ($change_ip && $newnet->{$ip}) {
1270 eval { &$netcmd($family_opt, 'addr', 'add', $newnet->{$ip}, 'dev', $eth); };
1271 if (my $err = $@) {
1272 warn $err;
1273 return;
1274 }
1275 }
1276
1277 # step 2: replace gateway
1278 # If this fails we delete the added IP and cancel.
1279 # If it succeeds we save the config and delete the old IP, ignoring
1280 # errors. The config is then saved.
1281 # Note: 'ip route replace' can add
1282 if ($change_gw) {
1283 if ($newnet->{$gw}) {
1284 eval { &$netcmd($family_opt, 'route', 'replace', 'default', 'via', $newnet->{$gw}); };
1285 if (my $err = $@) {
1286 warn $err;
1287 # the route was not replaced, the old IP is still available
1288 # rollback (delete new IP) and cancel
1289 if ($change_ip) {
1290 eval { &$netcmd($family_opt, 'addr', 'del', $newnet->{$ip}, 'dev', $eth); };
1291 warn $@ if $@; # no need to die here
1292 }
1293 return;
1294 }
1295 } else {
1296 eval { &$netcmd($family_opt, 'route', 'del', 'default'); };
1297 # if the route was not deleted, the guest might have deleted it manually
1298 # warn and continue
1299 warn $@ if $@;
1300 }
1301 }
1302
1303 # from this point on we safe the configuration
1304 # step 3: delete old IP ignoring errors
1305 if ($change_ip && $optdata->{$ip}) {
1306 eval { &$netcmd($family_opt, 'addr', 'del', $optdata->{$ip}, 'dev', $eth); };
1307 warn $@ if $@; # no need to die here
1308 }
1309
1310 foreach my $property ($ip, $gw) {
1311 if ($newnet->{$property}) {
1312 $optdata->{$property} = $newnet->{$property};
1313 } else {
1314 delete $optdata->{$property};
1315 }
1316 }
1317 PVE::LXC::write_config($vmid, $conf);
1318 $lxc_setup->setup_network($conf);
1319 };
1320
1321 &$change_ip_config('-4');
1322 &$change_ip_config('-6', '6');
1323 }
1324
1325 1;