]>
Commit | Line | Data |
---|---|---|
f76a2828 DM |
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 | ||
7dfc49cc DM |
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 | ||
f76a2828 DM |
28 | my $nodename = PVE::INotify::nodename(); |
29 | ||
30 | my $valid_lxc_keys = { | |
31 | 'lxc.arch' => 1, | |
32 | 'lxc.include' => 1, | |
33 | 'lxc.rootfs' => 1, | |
34 | 'lxc.mount' => 1, | |
35 | 'lxc.utsname' => 1, | |
36 | ||
37 | 'lxc.cgroup.memory.limit_in_bytes' => 1, | |
38 | ||
39 | # pve related keys | |
40 | 'pve.comment' => 1, | |
41 | }; | |
42 | ||
43 | my $valid_network_keys = { | |
44 | type => 1, | |
45 | flags => 1, | |
46 | link => 1, | |
47 | mtu => 1, | |
48 | name => 1, # ifname inside container | |
49 | 'veth.pair' => 1, # ifname at host (eth${vmid}.X) | |
50 | hwaddr => 1, | |
51 | ipv4 => 1, | |
52 | 'ipv4.gateway' => 1, | |
53 | ipv6 => 1, | |
54 | 'ipv6.gateway' => 1, | |
55 | }; | |
56 | ||
57 | my $lxc_array_configs = { | |
58 | 'lxc.network' => 1, | |
59 | 'lxc.mount' => 1, | |
60 | 'lxc.include' => 1, | |
61 | }; | |
62 | ||
63 | sub write_lxc_config { | |
64 | my ($filename, $data) = @_; | |
65 | ||
66 | my $raw = ""; | |
67 | ||
68 | return $raw if !$data; | |
69 | ||
70 | my $done_hash = { digest => 1}; | |
7dfc49cc | 71 | |
f76a2828 DM |
72 | foreach my $k (sort keys %$data) { |
73 | next if $k !~ m/^lxc\./; | |
74 | $done_hash->{$k} = 1; | |
75 | $raw .= "$k = $data->{$k}\n"; | |
76 | } | |
7dfc49cc | 77 | |
f76a2828 DM |
78 | foreach my $k (sort keys %$data) { |
79 | next if $k !~ m/^net\d+$/; | |
80 | $done_hash->{$k} = 1; | |
81 | my $net = $data->{$k}; | |
82 | $raw .= "lxc.network.type = $net->{type}\n"; | |
83 | foreach my $subkey (sort keys %$net) { | |
84 | next if $subkey eq 'type'; | |
85 | $raw .= "lxc.network.$subkey = $net->{$subkey}\n"; | |
86 | } | |
87 | } | |
88 | ||
89 | foreach my $k (sort keys %$data) { | |
90 | next if $done_hash->{$k}; | |
91 | die "found un-written value in config - implement this!"; | |
92 | } | |
93 | ||
f76a2828 DM |
94 | return $raw; |
95 | } | |
96 | ||
97 | sub parse_lxc_config { | |
98 | my ($filename, $raw) = @_; | |
99 | ||
100 | return undef if !defined($raw); | |
101 | ||
102 | my $data = { | |
103 | digest => Digest::SHA::sha1_hex($raw), | |
104 | }; | |
105 | ||
106 | $filename =~ m|/lxc/(\d+)/config$| | |
107 | || die "got strange filename '$filename'"; | |
108 | ||
109 | my $vmid = $1; | |
110 | ||
111 | my $network_counter = 0; | |
112 | my $network_list = []; | |
113 | my $host_ifnames = {}; | |
114 | ||
115 | my $find_next_hostif_name = sub { | |
116 | for (my $i = 0; $i < 10; $i++) { | |
117 | my $name = "veth${vmid}.$i"; | |
118 | if (!$host_ifnames->{$name}) { | |
119 | $host_ifnames->{$name} = 1; | |
120 | return $name; | |
121 | } | |
122 | } | |
123 | ||
124 | die "unable to find free host_ifname"; # should not happen | |
125 | }; | |
7dfc49cc | 126 | |
f76a2828 DM |
127 | my $push_network = sub { |
128 | my ($netconf) = @_; | |
129 | return if !$netconf; | |
130 | push @{$network_list}, $netconf; | |
131 | $network_counter++; | |
132 | if (my $netname = $netconf->{'veth.pair'}) { | |
133 | if ($netname =~ m/^veth(\d+).(\d)$/) { | |
134 | die "wrong vmid for network interface pair\n" if $1 != $vmid; | |
135 | my $host_ifnames->{$netname} = 1; | |
136 | } else { | |
137 | die "wrong network interface pair\n"; | |
138 | } | |
139 | } | |
140 | }; | |
141 | ||
142 | my $network; | |
7dfc49cc | 143 | |
f76a2828 DM |
144 | while ($raw && $raw =~ s/^(.*?)(\n|$)//) { |
145 | my $line = $1; | |
146 | ||
147 | next if $line =~ m/^\#/; | |
148 | next if $line =~ m/^\s*$/; | |
149 | ||
150 | if ($line =~ m/^lxc\.network\.(\S+)\s*=\s*(\S+)\s*$/) { | |
151 | my ($subkey, $value) = ($1, $2); | |
152 | if ($subkey eq 'type') { | |
153 | &$push_network($network); | |
154 | $network = { type => $value }; | |
155 | } elsif ($valid_network_keys->{$subkey}) { | |
156 | $network->{$subkey} = $value; | |
157 | } else { | |
158 | die "unable to parse config line: $line\n"; | |
159 | } | |
7dfc49cc | 160 | |
f76a2828 DM |
161 | next; |
162 | } | |
163 | if ($line =~ m/^(pve.comment)\s*=\s*(\S.*)\s*$/) { | |
164 | my ($name, $value) = ($1, $2); | |
165 | $data->{$name} = $value; | |
166 | next; | |
167 | } | |
168 | if ($line =~ m/^((?:pve|lxc)\.\S+)\s*=\s*(\S+)\s*$/) { | |
169 | my ($name, $value) = ($1, $2); | |
170 | ||
7dfc49cc | 171 | die "inavlid key '$name'\n" if !$valid_lxc_keys->{$name}; |
f76a2828 DM |
172 | |
173 | die "multiple definitions for $name\n" if defined($data->{$name}); | |
174 | ||
175 | $data->{$name} = $value; | |
176 | next; | |
177 | } | |
178 | ||
179 | die "unable to parse config line: $line\n"; | |
180 | } | |
181 | ||
182 | &$push_network($network); | |
183 | ||
184 | foreach my $net (@{$network_list}) { | |
185 | $net->{'veth.pair'} = &$find_next_hostif_name() if !$net->{'veth.pair'}; | |
186 | $net->{hwaddr} = PVE::Tools::random_ether_addr() if !$net->{hwaddr}; | |
187 | die "unsupported network type '$net->{type}'\n" if $net->{type} ne 'veth'; | |
188 | ||
189 | if ($net->{'veth.pair'} =~ m/^veth\d+.(\d+)$/) { | |
190 | $data->{"net$1"} = $net; | |
191 | } | |
7dfc49cc DM |
192 | } |
193 | ||
194 | return $data; | |
f76a2828 DM |
195 | } |
196 | ||
197 | sub config_list { | |
198 | my $vmlist = PVE::Cluster::get_vmlist(); | |
199 | my $res = {}; | |
200 | return $res if !$vmlist || !$vmlist->{ids}; | |
201 | my $ids = $vmlist->{ids}; | |
202 | ||
203 | foreach my $vmid (keys %$ids) { | |
204 | next if !$vmid; # skip CT0 | |
205 | my $d = $ids->{$vmid}; | |
206 | next if !$d->{node} || $d->{node} ne $nodename; | |
207 | next if !$d->{type} || $d->{type} ne 'lxc'; | |
208 | $res->{$vmid}->{type} = 'lxc'; | |
209 | } | |
210 | return $res; | |
211 | } | |
212 | ||
213 | sub cfs_config_path { | |
214 | my ($vmid, $node) = @_; | |
215 | ||
216 | $node = $nodename if !$node; | |
217 | return "nodes/$node/lxc/$vmid/config"; | |
218 | } | |
219 | ||
9c2d4ce9 DM |
220 | sub config_file { |
221 | my ($vmid, $node) = @_; | |
222 | ||
223 | my $cfspath = cfs_config_path($vmid, $node); | |
224 | return "/etc/pve/$cfspath"; | |
225 | } | |
226 | ||
f76a2828 DM |
227 | sub load_config { |
228 | my ($vmid) = @_; | |
229 | ||
230 | my $cfspath = cfs_config_path($vmid); | |
231 | ||
232 | my $conf = PVE::Cluster::cfs_read_file($cfspath); | |
233 | die "container $vmid does not exists\n" if !defined($conf); | |
234 | ||
235 | return $conf; | |
236 | } | |
237 | ||
238 | sub write_config { | |
239 | my ($vmid, $conf) = @_; | |
240 | ||
241 | my $cfspath = cfs_config_path($vmid); | |
242 | ||
243 | PVE::Cluster::cfs_write_file($cfspath, $conf); | |
244 | } | |
245 | ||
9c2d4ce9 DM |
246 | my $tempcounter = 0; |
247 | sub write_temp_config { | |
248 | my ($vmid, $conf) = @_; | |
7dfc49cc | 249 | |
9c2d4ce9 DM |
250 | $tempcounter++; |
251 | my $filename = "/tmp/temp-lxc-conf-$vmid-$$-$tempcounter.conf"; | |
252 | ||
253 | my $raw = write_lxc_config($filename, $conf); | |
254 | ||
255 | PVE::Tools::file_set_contents($filename, $raw); | |
7dfc49cc | 256 | |
9c2d4ce9 DM |
257 | return $filename; |
258 | } | |
259 | ||
f76a2828 DM |
260 | sub lock_container { |
261 | my ($vmid, $timeout, $code, @param) = @_; | |
262 | ||
263 | my $lockdir = "/run/lock/lxc"; | |
264 | my $lockfile = "$lockdir/pve-config-{$vmid}.lock"; | |
265 | ||
266 | File::Path::make_path($lockdir); | |
267 | ||
268 | my $res = PVE::Tools::lock_file($lockfile, $timeout, $code, @param); | |
269 | ||
270 | die $@ if $@; | |
271 | ||
272 | return $res; | |
273 | } | |
274 | ||
275 | my $confdesc = { | |
276 | onboot => { | |
277 | optional => 1, | |
278 | type => 'boolean', | |
279 | description => "Specifies whether a VM will be started during system bootup.", | |
280 | default => 0, | |
281 | }, | |
282 | cpus => { | |
283 | optional => 1, | |
284 | type => 'integer', | |
285 | description => "The number of CPUs for this container.", | |
286 | minimum => 1, | |
287 | default => 1, | |
288 | }, | |
289 | cpuunits => { | |
290 | optional => 1, | |
291 | type => 'integer', | |
292 | 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.", | |
293 | minimum => 0, | |
294 | maximum => 500000, | |
295 | default => 1000, | |
296 | }, | |
297 | memory => { | |
298 | optional => 1, | |
299 | type => 'integer', | |
300 | description => "Amount of RAM for the VM in MB.", | |
301 | minimum => 16, | |
302 | default => 512, | |
303 | }, | |
304 | swap => { | |
305 | optional => 1, | |
306 | type => 'integer', | |
307 | description => "Amount of SWAP for the VM in MB.", | |
308 | minimum => 0, | |
309 | default => 512, | |
310 | }, | |
311 | disk => { | |
312 | optional => 1, | |
313 | type => 'number', | |
314 | description => "Amount of disk space for the VM in GB. A zero indicates no limits.", | |
315 | minimum => 0, | |
316 | default => 2, | |
317 | }, | |
318 | hostname => { | |
319 | optional => 1, | |
320 | description => "Set a host name for the container.", | |
321 | type => 'string', | |
322 | maxLength => 255, | |
323 | }, | |
324 | description => { | |
325 | optional => 1, | |
326 | type => 'string', | |
327 | description => "Container description. Only used on the configuration web interface.", | |
328 | }, | |
329 | searchdomain => { | |
330 | optional => 1, | |
331 | type => 'string', | |
332 | description => "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain or nameserver.", | |
333 | }, | |
334 | nameserver => { | |
335 | optional => 1, | |
336 | type => 'string', | |
337 | 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.", | |
338 | }, | |
ec52ac21 DM |
339 | }; |
340 | ||
341 | my $MAX_LXC_NETWORKS = 10; | |
342 | for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) { | |
343 | $confdesc->{"net$i"} = { | |
f76a2828 DM |
344 | optional => 1, |
345 | type => 'string', format => 'pve-lxc-network', | |
346 | description => "Specifies network interfaces for the container.", | |
7dfc49cc | 347 | }; |
ec52ac21 DM |
348 | } |
349 | ||
350 | sub option_exists { | |
351 | my ($name) = @_; | |
352 | ||
353 | return defined($confdesc->{$name}); | |
354 | } | |
f76a2828 DM |
355 | |
356 | # add JSON properties for create and set function | |
357 | sub json_config_properties { | |
358 | my $prop = shift; | |
359 | ||
360 | foreach my $opt (keys %$confdesc) { | |
361 | $prop->{$opt} = $confdesc->{$opt}; | |
362 | } | |
363 | ||
364 | return $prop; | |
365 | } | |
366 | ||
367 | ||
368 | sub vmstatus { | |
369 | my ($opt_vmid) = @_; | |
370 | ||
371 | my $list = $opt_vmid ? { $opt_vmid => { type => 'lxc' }} : config_list(); | |
372 | ||
373 | foreach my $vmid (keys %$list) { | |
374 | next if $opt_vmid && ($vmid ne $opt_vmid); | |
375 | ||
376 | my $d = $list->{$vmid}; | |
377 | $d->{status} = 'stopped'; | |
378 | ||
379 | my $cfspath = cfs_config_path($vmid); | |
380 | if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) { | |
f76a2828 DM |
381 | $d->{name} = $conf->{'lxc.utsname'} || "CT$vmid"; |
382 | $d->{name} =~ s/[\s]//g; | |
e901d418 DM |
383 | |
384 | $d->{cpus} = 1; | |
385 | ||
386 | $d->{disk} = 0; | |
387 | $d->{maxdisk} = 1; | |
388 | ||
389 | $d->{mem} = 0; | |
390 | $d->{maxmem} = 1024; | |
391 | ||
392 | $d->{uptime} = 0; | |
393 | $d->{cpu} = 0; | |
394 | ||
395 | $d->{netout} = 0; | |
396 | $d->{netin} = 0; | |
397 | ||
398 | $d->{diskread} = 0; | |
399 | $d->{diskwrite} = 0; | |
f76a2828 DM |
400 | |
401 | } | |
402 | } | |
403 | ||
404 | return $list; | |
405 | } | |
406 | ||
7dfc49cc DM |
407 | |
408 | sub print_lxc_network { | |
f76a2828 DM |
409 | my $net = shift; |
410 | ||
7dfc49cc | 411 | die "no network link defined\n" if !$net->{link}; |
f76a2828 | 412 | |
7dfc49cc DM |
413 | my $res = "link=$net->{link}"; |
414 | ||
415 | foreach my $k (qw(hwaddr mtu name ipv4 ipv4.gateway ipv6 ipv6.gateway)) { | |
f76a2828 DM |
416 | next if !defined($net->{$k}); |
417 | $res .= ",$k=$net->{$k}"; | |
418 | } | |
7dfc49cc | 419 | |
f76a2828 DM |
420 | return $res; |
421 | } | |
422 | ||
7dfc49cc DM |
423 | sub parse_lxc_network { |
424 | my ($data) = @_; | |
425 | ||
426 | my $res = {}; | |
427 | ||
428 | return $res if !$data; | |
429 | ||
430 | foreach my $pv (split (/,/, $data)) { | |
431 | if ($pv =~ m/^(link|hwaddr|mtu|name|ipv4|ipv6|ipv4\.gateway|ipv6\.gateway)=(\S+)$/) { | |
432 | $res->{$1} = $2; | |
433 | } else { | |
434 | return undef; | |
435 | } | |
436 | } | |
437 | ||
438 | $res->{type} = 'veth'; | |
439 | $res->{hwaddr} = PVE::Tools::random_ether_addr() if !$res->{mac}; | |
440 | ||
441 | return $res; | |
442 | } | |
f76a2828 DM |
443 | |
444 | 1; |