]> git.proxmox.com Git - pve-container.git/blob - src/PVE/LXC/Setup/Debian.pm
backup: log errors from rsync
[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 my 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
66 my sub setup_inittab {
67 my ($self, $conf) = @_;
68
69 my $filename = "/etc/inittab";
70 return if !$self->ct_file_exists($filename);
71
72 my $ttycount = PVE::LXC::Config->get_tty_count($conf);
73 my $inittab = $self->ct_file_get_contents($filename);
74
75 my @lines = grep {
76 # remove getty lines
77 !/^\s*\d+:\d*:[^:]*:.*getty/ &&
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
86 my $version = $self->{version};
87 for (my $id = 1; $id <= $ttycount; $id++) {
88 next if $id == 7; # reserved for X11
89 my $levels = ($id == 1) ? '2345' : '23';
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";
94 }
95 }
96
97 $self->ct_file_set_contents($filename, $inittab);
98 }
99
100 sub 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/machine-id exists).
110 $self->setup_systemd_preset({
111 # systemd-networkd gets enabled by default, disable it, debian uses ifupdown
112 'systemd-networkd.service' => 0,
113 });
114 }
115 }
116
117 setup_inittab($self, $conf);
118 }
119
120 sub remove_gateway_scripts {
121 my ($attr) = @_;
122 my $length = scalar(@$attr);
123
124 my $found_section = 0;
125 my $keep = 1;
126 @$attr = grep {
127 if ($_ eq '# --- BEGIN PVE ---') {
128 $found_section = 1;
129 $keep = 0;
130 0; # remove this line
131 } elsif ($_ eq '# --- END PVE ---') {
132 $found_section = 1;
133 $keep = 1;
134 0; # remove this line
135 } else {
136 $keep;
137 }
138 } @$attr;
139
140 return if $found_section;
141 # XXX: To deal with existing setups we perform two types of removal for
142 # now. Newly started containers have their routing sections marked with
143 # begin/end comments. For older containers we perform a strict matching on
144 # the routing rules we added. We can probably remove this part at some point
145 # when it is unlikely that old debian setups are still around.
146
147 for (my $i = 0; $i < $length-3; ++$i) {
148 next if $attr->[$i+0] !~ m@^\s*post-up\s+ip\s+route\s+add\s+(\S+)\s+dev\s+(\S+)$@;
149 my ($ip, $dev) = ($1, $2);
150 if ($attr->[$i+1] =~ m@^\s*post-up\s+ip\s+route\s+add\s+default\s+via\s+(\S+)\s+dev\s+(\S+)$@ &&
151 ($ip eq $1 && $dev eq $2) &&
152 $attr->[$i+2] =~ m@^\s*pre-down\s+ip\s+route\s+del\s+default\s+via\s+(\S+)\s+dev\s+(\S+)$@ &&
153 ($ip eq $1 && $dev eq $2) &&
154 $attr->[$i+3] =~ m@^\s*pre-down\s+ip\s+route\s+del\s+(\S+)\s+dev\s+(\S+)$@ &&
155 ($ip eq $1 && $dev eq $2))
156 {
157 splice @$attr, $i, 4;
158 $length -= 4;
159 --$i;
160 }
161 }
162 }
163
164 sub make_gateway_scripts {
165 my ($ifname, $gw) = @_;
166 return <<"SCRIPTS";
167 # --- BEGIN PVE ---
168 \tpost-up ip route add $gw dev $ifname
169 \tpost-up ip route add default via $gw dev $ifname
170 \tpre-down ip route del default via $gw dev $ifname
171 \tpre-down ip route del $gw dev $ifname
172 # --- END PVE ---
173 SCRIPTS
174 }
175
176 # NOTE: this is re-used by Alpine Linux, please have that in mind when changing things.
177 sub setup_network {
178 my ($self, $conf) = @_;
179
180 my $networks = {};
181 foreach my $k (keys %$conf) {
182 next if $k !~ m/^net(\d+)$/;
183 my $ind = $1;
184 my $d = PVE::LXC::Config->parse_lxc_network($conf->{$k});
185 if ($d->{name}) {
186 my $net = {};
187 my $cidr;
188 if (defined($d->{ip})) {
189 if ($d->{ip} =~ /^(?:dhcp|manual)$/) {
190 $net->{address} = $d->{ip};
191 } else {
192 my $ipinfo = PVE::LXC::parse_ipv4_cidr($d->{ip});
193 $net->{address} = $ipinfo->{address};
194 $net->{netmask} = $ipinfo->{netmask};
195 $net->{cidr} = $d->{ip};
196 $cidr = $d->{ip};
197 }
198 }
199 if (defined($d->{'gw'})) {
200 $net->{gateway} = $d->{'gw'};
201 if (defined($cidr) && !PVE::Network::is_ip_in_cidr($d->{gw}, $cidr, 4)) {
202 # gateway is not reachable, need an extra route
203 $net->{needsroute} = 1;
204 }
205 }
206 $cidr = undef;
207 if (defined($d->{ip6})) {
208 if ($d->{ip6} =~ /^(?:auto|dhcp|manual)$/) {
209 $net->{address6} = $d->{ip6};
210 } elsif ($d->{ip6} !~ /^($IPV6RE)\/(\d+)$/) {
211 die "unable to parse ipv6 address/prefix\n";
212 } else {
213 $net->{address6} = $1;
214 $net->{netmask6} = $2;
215 $net->{cidr6} = $d->{ip6};
216 $cidr = $d->{ip6};
217 }
218 }
219 if (defined($d->{'gw6'})) {
220 $net->{gateway6} = $d->{'gw6'};
221 if (defined($cidr) && !PVE::Network::is_ip_in_cidr($d->{gw6}, $cidr, 6) &&
222 !PVE::Network::is_ip_in_cidr($d->{gw6}, 'fe80::/10', 6)) {
223 # gateway is not reachable, need an extra route
224 $net->{needsroute6} = 1;
225 }
226 }
227 $networks->{$d->{name}} = $net if keys %$net;
228 }
229 }
230
231 return if !scalar(keys %$networks);
232
233 my $filename = "/etc/network/interfaces";
234 my $interfaces = "";
235
236 my $section;
237
238 my $done_auto = {};
239 my $done_v4_hash = {};
240 my $done_v6_hash = {};
241
242 my ($os, $version) = ($conf->{ostype}, $self->{version});
243 my $print_section = sub {
244 return if !$section;
245
246 my $ifname = $section->{ifname};
247 my $net = $networks->{$ifname};
248
249 if (!$done_auto->{$ifname}) {
250 $interfaces .= "auto $ifname\n";
251 $done_auto->{$ifname} = 1;
252 }
253
254 if ($section->{type} eq 'ipv4') {
255 $done_v4_hash->{$ifname} = 1;
256
257 if (!defined($net->{address})) {
258 # no address => no iface line
259 } elsif ($net->{address} =~ /^(dhcp|manual)$/) {
260 $interfaces .= "iface $ifname inet $1\n\n";
261 } else {
262 $interfaces .= "iface $ifname inet static\n";
263 if ($os eq "debian" && at_least($version, 10, 0)
264 || $os eq "alpine" && at_least($version, 3, 13)
265 ) {
266 $interfaces .= "\taddress $net->{cidr}\n" if defined($net->{cidr});
267 } else {
268 $interfaces .= "\taddress $net->{address}\n" if defined($net->{address});
269 $interfaces .= "\tnetmask $net->{netmask}\n" if defined($net->{netmask});
270 }
271 remove_gateway_scripts($section->{attr});
272 if (defined(my $gw = $net->{gateway})) {
273 if ($net->{needsroute}) {
274 $interfaces .= make_gateway_scripts($ifname, $gw);
275 } else {
276 $interfaces .= "\tgateway $gw\n";
277 }
278 }
279 foreach my $attr (@{$section->{attr}}) {
280 $interfaces .= "\t$attr\n";
281 }
282 $interfaces .= "\n";
283 }
284 } elsif ($section->{type} eq 'ipv6') {
285 $done_v6_hash->{$ifname} = 1;
286
287 if (!defined($net->{address6})) {
288 # no address => no iface line
289 } elsif ($net->{address6} =~ /^(auto|dhcp|manual)$/) {
290 $interfaces .= "iface $ifname inet6 $1\n\n";
291 } else {
292 $interfaces .= "iface $ifname inet6 static\n";
293 if ($os eq "debian" && at_least($version, 10, 0)
294 || $os eq "alpine" && at_least($version, 3, 13)
295 ) {
296 $interfaces .= "\taddress $net->{cidr6}\n" if defined($net->{cidr6});
297 } else {
298 $interfaces .= "\taddress $net->{address6}\n" if defined($net->{address6});
299 $interfaces .= "\tnetmask $net->{netmask6}\n" if defined($net->{netmask6});
300 }
301 remove_gateway_scripts($section->{attr});
302 if (defined(my $gw = $net->{gateway6})) {
303 if ($net->{needsroute6}) {
304 $interfaces .= make_gateway_scripts($ifname, $gw);
305 } else {
306 $interfaces .= "\tgateway $net->{gateway6}\n" if defined($net->{gateway6});
307 }
308 }
309 foreach my $attr (@{$section->{attr}}) {
310 $interfaces .= "\t$attr\n";
311 }
312 $interfaces .= "\n";
313 }
314 } else {
315 die "unknown section type '$section->{type}'";
316 }
317
318 $section = undef;
319 };
320
321 if (my $fh = $self->ct_open_file_read($filename)) {
322 while (defined (my $line = <$fh>)) {
323 chomp $line;
324 if ($line =~ m/^# --- (?:BEGIN|END) PVE ---/) {
325 # Include markers in the attribute section so
326 # remove_gateway_scripts() can find them.
327 push @{$section->{attr}}, $line if $section;
328 next;
329 }
330 if ($line =~ m/^#/) {
331 $interfaces .= "$line\n";
332 next;
333 }
334 if ($line =~ m/^\s*$/) {
335 if ($section) {
336 &$print_section();
337 } else {
338 $interfaces .= "$line\n";
339 }
340 next;
341 }
342 if ($line =~ m/^\s*iface\s+(\S+)\s+inet\s+(\S+)\s*$/) {
343 my $ifname = $1;
344 &$print_section(); # print previous section
345 if (!$networks->{$ifname}) {
346 $interfaces .= "$line\n";
347 next;
348 }
349 $section = { type => 'ipv4', ifname => $ifname, attr => []};
350 next;
351 }
352 if ($line =~ m/^\s*iface\s+(\S+)\s+inet6\s+(\S+)\s*$/) {
353 my $ifname = $1;
354 &$print_section(); # print previous section
355 if (!$networks->{$ifname}) {
356 $interfaces .= "$line\n";
357 next;
358 }
359 $section = { type => 'ipv6', ifname => $ifname, attr => []};
360 next;
361 }
362 # Handle 'auto'
363 if ($line =~ m/^\s*auto\s*(.*)$/) {
364 foreach my $iface (split(/\s+/, $1)) {
365 $done_auto->{$iface} = 1;
366 }
367 &$print_section();
368 $interfaces .= "$line\n";
369 next;
370 }
371 # Handle other section delimiters:
372 if ($line =~ m/^\s*(?:mapping\s
373 |allow-
374 |source\s
375 |source-directory\s
376 )/x) {
377 &$print_section();
378 $interfaces .= "$line\n";
379 next;
380 }
381 if ($section && $line =~ m/^\s*((\S+)\s(.*))$/) {
382 my ($adata, $aname) = ($1, $2);
383 if ($aname eq 'address' || $aname eq 'netmask' ||
384 $aname eq 'gateway' || $aname eq 'broadcast') {
385 # skip
386 } else {
387 push @{$section->{attr}}, $adata;
388 }
389 next;
390 }
391
392 $interfaces .= "$line\n";
393 }
394 &$print_section();
395 }
396
397 my $need_separator = length($interfaces) && ($interfaces !~ /\n\n$/);
398 foreach my $ifname (sort keys %$networks) {
399 my $net = $networks->{$ifname};
400
401 if (!$done_v4_hash->{$ifname} && defined($net->{address})) {
402 if ($need_separator) { $interfaces .= "\n"; $need_separator = 0; };
403 $section = { type => 'ipv4', ifname => $ifname, attr => []};
404 &$print_section();
405 }
406 if (!$done_v6_hash->{$ifname} && defined($net->{address6})) {
407 if ($need_separator) { $interfaces .= "\n"; $need_separator = 0; };
408 $section = { type => 'ipv6', ifname => $ifname, attr => []};
409 &$print_section();
410 }
411 }
412
413 # older templates (< Debian 8) do not configure the loopback interface
414 # if not explicitly told to do so
415 if (!$done_auto->{lo}) {
416 $interfaces = "auto lo\niface lo inet loopback\n" .
417 "iface lo inet6 loopback\n\n" .
418 $interfaces;
419 }
420
421 $self->ct_file_set_contents($filename, $interfaces);
422 }
423
424 1;