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