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