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