]> git.proxmox.com Git - pve-container.git/blob - src/PVE/LXC/Setup/Debian.pm
76d336a12e69c3ad70c03bd4f0e37bc3105e3bbb
[pve-container.git] / src / PVE / LXC / Setup / Debian.pm
1 package PVE::LXC::Setup::Debian;
2
3 use strict;
4 use warnings;
5
6 use PVE::Tools qw($IPV6RE);
7 use PVE::LXC;
8 use PVE::Network;
9 use File::Path;
10
11 use PVE::LXC::Setup::Base;
12
13 use base qw(PVE::LXC::Setup::Base);
14
15
16 sub new {
17 my ($class, $conf, $rootdir) = @_;
18
19 my $version = PVE::Tools::file_read_firstline("$rootdir/etc/debian_version");
20
21 die "unable to read version info\n" if !defined($version);
22
23 # translate testing version and os-release incompat derivates names
24 my $version_map = {
25 'stretch/sid' => 9.1,
26 'buster/sid' => 10,
27 'bullseye/sid' => 11,
28 'bookworm/sid' => 12,
29 'trixie/sid' => 13,
30 'forky/sid' => 14,
31 'kali-rolling' => 12,
32 };
33 $version = $version_map->{$version} if exists($version_map->{$version});
34
35 die "unable to parse version info '$version'\n"
36 if $version !~ m/^(\d+(\.\d+)?)(\.\d+)?/;
37
38 $version = $1;
39
40 die "unsupported debian version '$version'\n" if !($version >= 4 && $version <= 13);
41
42 my $self = { conf => $conf, rootdir => $rootdir, version => $version };
43
44 $conf->{ostype} = "debian";
45
46 return bless $self, $class;
47 }
48
49 # Debian doesn't support the /dev/lxc/ subdirectory.
50 sub devttydir {
51 return '';
52 }
53
54 sub setup_init {
55 my ($self, $conf) = @_;
56
57 my $systemd = $self->ct_readlink('/sbin/init');
58 if (defined($systemd) && $systemd =~ m@/systemd$@) {
59 $self->setup_container_getty_service($conf);
60 }
61
62 my $filename = "/etc/inittab";
63 return if !$self->ct_file_exists($filename);
64
65 my $ttycount = PVE::LXC::Config->get_tty_count($conf);
66 my $inittab = $self->ct_file_get_contents($filename);
67
68 my @lines = grep {
69 # remove getty lines
70 !/^\s*\d+:\d*:[^:]*:.*getty/ &&
71 # remove power lines
72 !/^\s*p[fno0]:/
73 } split(/\n/, $inittab);
74
75 $inittab = join("\n", @lines) . "\n";
76
77 $inittab .= "p0::powerfail:/sbin/init 0\n";
78
79 my $version = $self->{version};
80 for (my $id = 1; $id <= $ttycount; $id++) {
81 next if $id == 7; # reserved for X11
82 my $levels = ($id == 1) ? '2345' : '23';
83 if ($version < 7) {
84 $inittab .= "$id:$levels:respawn:/sbin/getty -L 38400 tty$id\n";
85 } else {
86 $inittab .= "$id:$levels:respawn:/sbin/getty --noclear 38400 tty$id\n";
87 }
88 }
89
90 $self->ct_file_set_contents($filename, $inittab);
91 }
92
93 sub remove_gateway_scripts {
94 my ($attr) = @_;
95 my $length = scalar(@$attr);
96
97 my $found_section = 0;
98 my $keep = 1;
99 @$attr = grep {
100 if ($_ eq '# --- BEGIN PVE ---') {
101 $found_section = 1;
102 $keep = 0;
103 0; # remove this line
104 } elsif ($_ eq '# --- END PVE ---') {
105 $found_section = 1;
106 $keep = 1;
107 0; # remove this line
108 } else {
109 $keep;
110 }
111 } @$attr;
112
113 return if $found_section;
114 # XXX: To deal with existing setups we perform two types of removal for
115 # now. Newly started containers have their routing sections marked with
116 # begin/end comments. For older containers we perform a strict matching on
117 # the routing rules we added. We can probably remove this part at some point
118 # when it is unlikely that old debian setups are still around.
119
120 for (my $i = 0; $i < $length-3; ++$i) {
121 next if $attr->[$i+0] !~ m@^\s*post-up\s+ip\s+route\s+add\s+(\S+)\s+dev\s+(\S+)$@;
122 my ($ip, $dev) = ($1, $2);
123 if ($attr->[$i+1] =~ m@^\s*post-up\s+ip\s+route\s+add\s+default\s+via\s+(\S+)\s+dev\s+(\S+)$@ &&
124 ($ip eq $1 && $dev eq $2) &&
125 $attr->[$i+2] =~ m@^\s*pre-down\s+ip\s+route\s+del\s+default\s+via\s+(\S+)\s+dev\s+(\S+)$@ &&
126 ($ip eq $1 && $dev eq $2) &&
127 $attr->[$i+3] =~ m@^\s*pre-down\s+ip\s+route\s+del\s+(\S+)\s+dev\s+(\S+)$@ &&
128 ($ip eq $1 && $dev eq $2))
129 {
130 splice @$attr, $i, 4;
131 $length -= 4;
132 --$i;
133 }
134 }
135 }
136
137 sub make_gateway_scripts {
138 my ($ifname, $gw) = @_;
139 return <<"SCRIPTS";
140 # --- BEGIN PVE ---
141 \tpost-up ip route add $gw dev $ifname
142 \tpost-up ip route add default via $gw dev $ifname
143 \tpre-down ip route del default via $gw dev $ifname
144 \tpre-down ip route del $gw dev $ifname
145 # --- END PVE ---
146 SCRIPTS
147 }
148
149 my sub at_least : prototype($$$) {
150 my ($str, $want_maj, $want_min) = @_;
151 return if !defined($str) || !defined($want_maj);
152
153 my ($maj, $min) = $str =~ /^(\d+)(?:\.(\d+))?/;
154 return if !defined($maj);
155
156 return $want_maj < $maj || $want_maj == $maj && (
157 (!defined($min) && $want_min == 0) || (defined($min) && $want_min <= $min)
158 );
159 }
160
161 # NOTE: this is re-used by Alpine Linux, please have that in mind when changing things.
162 sub setup_network {
163 my ($self, $conf) = @_;
164
165 my $networks = {};
166 foreach my $k (keys %$conf) {
167 next if $k !~ m/^net(\d+)$/;
168 my $ind = $1;
169 my $d = PVE::LXC::Config->parse_lxc_network($conf->{$k});
170 if ($d->{name}) {
171 my $net = {};
172 my $cidr;
173 if (defined($d->{ip})) {
174 if ($d->{ip} =~ /^(?:dhcp|manual)$/) {
175 $net->{address} = $d->{ip};
176 } else {
177 my $ipinfo = PVE::LXC::parse_ipv4_cidr($d->{ip});
178 $net->{address} = $ipinfo->{address};
179 $net->{netmask} = $ipinfo->{netmask};
180 $net->{cidr} = $d->{ip};
181 $cidr = $d->{ip};
182 }
183 }
184 if (defined($d->{'gw'})) {
185 $net->{gateway} = $d->{'gw'};
186 if (defined($cidr) && !PVE::Network::is_ip_in_cidr($d->{gw}, $cidr, 4)) {
187 # gateway is not reachable, need an extra route
188 $net->{needsroute} = 1;
189 }
190 }
191 $cidr = undef;
192 if (defined($d->{ip6})) {
193 if ($d->{ip6} =~ /^(?:auto|dhcp|manual)$/) {
194 $net->{address6} = $d->{ip6};
195 } elsif ($d->{ip6} !~ /^($IPV6RE)\/(\d+)$/) {
196 die "unable to parse ipv6 address/prefix\n";
197 } else {
198 $net->{address6} = $1;
199 $net->{netmask6} = $2;
200 $net->{cidr6} = $d->{ip6};
201 $cidr = $d->{ip6};
202 }
203 }
204 if (defined($d->{'gw6'})) {
205 $net->{gateway6} = $d->{'gw6'};
206 if (defined($cidr) && !PVE::Network::is_ip_in_cidr($d->{gw6}, $cidr, 6) &&
207 !PVE::Network::is_ip_in_cidr($d->{gw6}, 'fe80::/10', 6)) {
208 # gateway is not reachable, need an extra route
209 $net->{needsroute6} = 1;
210 }
211 }
212 $networks->{$d->{name}} = $net if keys %$net;
213 }
214 }
215
216 return if !scalar(keys %$networks);
217
218 my $filename = "/etc/network/interfaces";
219 my $interfaces = "";
220
221 my $section;
222
223 my $done_auto = {};
224 my $done_v4_hash = {};
225 my $done_v6_hash = {};
226
227 my ($os, $version) = ($conf->{ostype}, $self->{version});
228 my $print_section = sub {
229 return if !$section;
230
231 my $ifname = $section->{ifname};
232 my $net = $networks->{$ifname};
233
234 if (!$done_auto->{$ifname}) {
235 $interfaces .= "auto $ifname\n";
236 $done_auto->{$ifname} = 1;
237 }
238
239 if ($section->{type} eq 'ipv4') {
240 $done_v4_hash->{$ifname} = 1;
241
242 if (!defined($net->{address})) {
243 # no address => no iface line
244 } elsif ($net->{address} =~ /^(dhcp|manual)$/) {
245 $interfaces .= "iface $ifname inet $1\n\n";
246 } else {
247 $interfaces .= "iface $ifname inet static\n";
248 if ($os eq "debian" && at_least($version, 10, 0)
249 || $os eq "alpine" && at_least($version, 3, 13)
250 ) {
251 $interfaces .= "\taddress $net->{cidr}\n" if defined($net->{cidr});
252 } else {
253 $interfaces .= "\taddress $net->{address}\n" if defined($net->{address});
254 $interfaces .= "\tnetmask $net->{netmask}\n" if defined($net->{netmask});
255 }
256 remove_gateway_scripts($section->{attr});
257 if (defined(my $gw = $net->{gateway})) {
258 if ($net->{needsroute}) {
259 $interfaces .= make_gateway_scripts($ifname, $gw);
260 } else {
261 $interfaces .= "\tgateway $gw\n";
262 }
263 }
264 foreach my $attr (@{$section->{attr}}) {
265 $interfaces .= "\t$attr\n";
266 }
267 $interfaces .= "\n";
268 }
269 } elsif ($section->{type} eq 'ipv6') {
270 $done_v6_hash->{$ifname} = 1;
271
272 if (!defined($net->{address6})) {
273 # no address => no iface line
274 } elsif ($net->{address6} =~ /^(auto|dhcp|manual)$/) {
275 $interfaces .= "iface $ifname inet6 $1\n\n";
276 } else {
277 $interfaces .= "iface $ifname inet6 static\n";
278 if ($os eq "debian" && at_least($version, 10, 0)
279 || $os eq "alpine" && at_least($version, 3, 13)
280 ) {
281 $interfaces .= "\taddress $net->{cidr6}\n" if defined($net->{cidr6});
282 } else {
283 $interfaces .= "\taddress $net->{address6}\n" if defined($net->{address6});
284 $interfaces .= "\tnetmask $net->{netmask6}\n" if defined($net->{netmask6});
285 }
286 remove_gateway_scripts($section->{attr});
287 if (defined(my $gw = $net->{gateway6})) {
288 if ($net->{needsroute6}) {
289 $interfaces .= make_gateway_scripts($ifname, $gw);
290 } else {
291 $interfaces .= "\tgateway $net->{gateway6}\n" if defined($net->{gateway6});
292 }
293 }
294 foreach my $attr (@{$section->{attr}}) {
295 $interfaces .= "\t$attr\n";
296 }
297 $interfaces .= "\n";
298 }
299 } else {
300 die "unknown section type '$section->{type}'";
301 }
302
303 $section = undef;
304 };
305
306 if (my $fh = $self->ct_open_file_read($filename)) {
307 while (defined (my $line = <$fh>)) {
308 chomp $line;
309 if ($line =~ m/^# --- (?:BEGIN|END) PVE ---/) {
310 # Include markers in the attribute section so
311 # remove_gateway_scripts() can find them.
312 push @{$section->{attr}}, $line if $section;
313 next;
314 }
315 if ($line =~ m/^#/) {
316 $interfaces .= "$line\n";
317 next;
318 }
319 if ($line =~ m/^\s*$/) {
320 if ($section) {
321 &$print_section();
322 } else {
323 $interfaces .= "$line\n";
324 }
325 next;
326 }
327 if ($line =~ m/^\s*iface\s+(\S+)\s+inet\s+(\S+)\s*$/) {
328 my $ifname = $1;
329 &$print_section(); # print previous section
330 if (!$networks->{$ifname}) {
331 $interfaces .= "$line\n";
332 next;
333 }
334 $section = { type => 'ipv4', ifname => $ifname, attr => []};
335 next;
336 }
337 if ($line =~ m/^\s*iface\s+(\S+)\s+inet6\s+(\S+)\s*$/) {
338 my $ifname = $1;
339 &$print_section(); # print previous section
340 if (!$networks->{$ifname}) {
341 $interfaces .= "$line\n";
342 next;
343 }
344 $section = { type => 'ipv6', ifname => $ifname, attr => []};
345 next;
346 }
347 # Handle 'auto'
348 if ($line =~ m/^\s*auto\s*(.*)$/) {
349 foreach my $iface (split(/\s+/, $1)) {
350 $done_auto->{$iface} = 1;
351 }
352 &$print_section();
353 $interfaces .= "$line\n";
354 next;
355 }
356 # Handle other section delimiters:
357 if ($line =~ m/^\s*(?:mapping\s
358 |allow-
359 |source\s
360 |source-directory\s
361 )/x) {
362 &$print_section();
363 $interfaces .= "$line\n";
364 next;
365 }
366 if ($section && $line =~ m/^\s*((\S+)\s(.*))$/) {
367 my ($adata, $aname) = ($1, $2);
368 if ($aname eq 'address' || $aname eq 'netmask' ||
369 $aname eq 'gateway' || $aname eq 'broadcast') {
370 # skip
371 } else {
372 push @{$section->{attr}}, $adata;
373 }
374 next;
375 }
376
377 $interfaces .= "$line\n";
378 }
379 &$print_section();
380 }
381
382 my $need_separator = length($interfaces) && ($interfaces !~ /\n\n$/);
383 foreach my $ifname (sort keys %$networks) {
384 my $net = $networks->{$ifname};
385
386 if (!$done_v4_hash->{$ifname} && defined($net->{address})) {
387 if ($need_separator) { $interfaces .= "\n"; $need_separator = 0; };
388 $section = { type => 'ipv4', ifname => $ifname, attr => []};
389 &$print_section();
390 }
391 if (!$done_v6_hash->{$ifname} && defined($net->{address6})) {
392 if ($need_separator) { $interfaces .= "\n"; $need_separator = 0; };
393 $section = { type => 'ipv6', ifname => $ifname, attr => []};
394 &$print_section();
395 }
396 }
397
398 # older templates (< Debian 8) do not configure the loopback interface
399 # if not explicitly told to do so
400 if (!$done_auto->{lo}) {
401 $interfaces = "auto lo\niface lo inet loopback\n" .
402 "iface lo inet6 loopback\n\n" .
403 $interfaces;
404 }
405
406 $self->ct_file_set_contents($filename, $interfaces);
407 }
408
409 1;