]>
Commit | Line | Data |
---|---|---|
7af97ad5 | 1 | package PVE::LXC::Setup; |
1c7f4f65 DM |
2 | |
3 | use strict; | |
4 | use warnings; | |
5 | ||
21ec7304 | 6 | use POSIX; |
e6e308ae OB |
7 | use Cwd 'abs_path'; |
8 | ||
21ec7304 TL |
9 | use PVE::Tools; |
10 | ||
11 | use PVE::LXC::Setup::Alpine; | |
12 | use PVE::LXC::Setup::ArchLinux; | |
81a6ec3f | 13 | use PVE::LXC::Setup::CentOS; |
21ec7304 TL |
14 | use PVE::LXC::Setup::Debian; |
15 | use PVE::LXC::Setup::Devuan; | |
b0143ab1 | 16 | use PVE::LXC::Setup::Fedora; |
ed027b58 | 17 | use PVE::LXC::Setup::Gentoo; |
21ec7304 TL |
18 | use PVE::LXC::Setup::SUSE; |
19 | use PVE::LXC::Setup::Ubuntu; | |
1c7f4f65 DM |
20 | |
21 | my $plugins = { | |
21ec7304 TL |
22 | alpine => 'PVE::LXC::Setup::Alpine', |
23 | archlinux => 'PVE::LXC::Setup::ArchLinux', | |
24 | centos => 'PVE::LXC::Setup::CentOS', | |
7af97ad5 | 25 | debian => 'PVE::LXC::Setup::Debian', |
151c5b73 | 26 | devuan => 'PVE::LXC::Setup::Devuan', |
b0143ab1 | 27 | fedora => 'PVE::LXC::Setup::Fedora', |
ed027b58 | 28 | gentoo => 'PVE::LXC::Setup::Gentoo', |
21ec7304 TL |
29 | opensuse => 'PVE::LXC::Setup::SUSE', |
30 | ubuntu => 'PVE::LXC::Setup::Ubuntu', | |
1c7f4f65 DM |
31 | }; |
32 | ||
934a30b0 TL |
33 | # a map to allow supporting related distro flavours |
34 | my $plugin_alias = { | |
47447841 | 35 | 'opensuse-leap' => 'opensuse', |
005e72bd | 36 | 'opensuse-tumbleweed' => 'opensuse', |
21ec7304 TL |
37 | arch => 'archlinux', |
38 | sles => 'opensuse', | |
934a30b0 TL |
39 | }; |
40 | ||
bdd4194c | 41 | my $autodetect_type = sub { |
153747ff WB |
42 | my ($self, $rootdir, $os_release) = @_; |
43 | ||
44 | if (my $id = $os_release->{ID}) { | |
934a30b0 TL |
45 | return $id if $plugins->{$id}; |
46 | return $plugin_alias->{$id} if $plugin_alias->{$id}; | |
5c81c821 | 47 | warn "unknown ID '$id' in /etc/os-release file, trying fallback detection\n"; |
153747ff WB |
48 | } |
49 | ||
50 | # fallback compatibility checks | |
a8e58e9c DM |
51 | |
52 | my $lsb_fn = "$rootdir/etc/lsb-release"; | |
53 | if (-f $lsb_fn) { | |
54 | my $data = PVE::Tools::file_get_contents($lsb_fn); | |
55 | if ($data =~ m/^DISTRIB_ID=Ubuntu$/im) { | |
56 | return 'ubuntu'; | |
57 | } | |
75c7928e DM |
58 | } |
59 | ||
60 | if (-f "$rootdir/etc/debian_version") { | |
bdd4194c | 61 | return "debian"; |
151c5b73 TL |
62 | } elsif (-f "$rootdir/etc/devuan_version") { |
63 | return "devuan"; | |
fa7cb12b WB |
64 | } elsif (-f "$rootdir/etc/SuSE-brand" || -f "$rootdir/etc/SuSE-release") { |
65 | return "opensuse"; | |
b0143ab1 WB |
66 | } elsif (-f "$rootdir/etc/fedora-release") { |
67 | return "fedora"; | |
81a6ec3f WB |
68 | } elsif (-f "$rootdir/etc/centos-release" || -f "$rootdir/etc/redhat-release") { |
69 | return "centos"; | |
c1d32b55 WB |
70 | } elsif (-f "$rootdir/etc/arch-release") { |
71 | return "archlinux"; | |
a8700492 JV |
72 | } elsif (-f "$rootdir/etc/alpine-release") { |
73 | return "alpine"; | |
ed027b58 WB |
74 | } elsif (-f "$rootdir/etc/gentoo-release") { |
75 | return "gentoo"; | |
5f94d5e5 AE |
76 | } elsif (-f "$rootdir/etc/os-release") { |
77 | die "unable to detect OS distribution\n"; | |
78 | } else { | |
79 | warn "/etc/os-release file not found and autodetection failed, falling back to 'unmanaged'\n"; | |
80 | return "unmanaged"; | |
bdd4194c | 81 | } |
bdd4194c DM |
82 | }; |
83 | ||
1c7f4f65 | 84 | sub new { |
5b4657d0 | 85 | my ($class, $conf, $rootdir, $type) = @_; |
1c7f4f65 | 86 | |
5b4657d0 DM |
87 | die "no root directory\n" if !$rootdir || $rootdir eq '/'; |
88 | ||
250b158c | 89 | my $self = bless { conf => $conf, rootdir => $rootdir}, $class; |
1c7f4f65 | 90 | |
153747ff WB |
91 | my $os_release = $self->get_ct_os_release(); |
92 | ||
238b7e3e DM |
93 | if ($conf->{ostype} && $conf->{ostype} eq 'unmanaged') { |
94 | return $self; | |
95 | } elsif (!defined($type)) { | |
bdd4194c | 96 | # try to autodetect type |
153747ff | 97 | $type = &$autodetect_type($self, $rootdir, $os_release); |
238b7e3e DM |
98 | my $expected_type = $conf->{ostype} || $type; |
99 | ||
f9955be7 TL |
100 | if ($type ne $expected_type) { |
101 | warn "WARNING: /etc not present in CT, is the rootfs mounted?\n" | |
102 | if ! -e "$rootdir/etc"; | |
103 | warn "got unexpected ostype ($type != $expected_type)\n" | |
104 | } | |
bdd4194c | 105 | } |
238b7e3e | 106 | |
5f94d5e5 AE |
107 | if ($type eq 'unmanaged') { |
108 | $conf->{ostype} = $type; | |
109 | return $self; | |
110 | } | |
111 | ||
1db602d0 | 112 | my $plugin_class = $plugins->{$type} || die "no such OS type '$type'\n"; |
1c7f4f65 | 113 | |
153747ff | 114 | my $plugin = $plugin_class->new($conf, $rootdir, $os_release); |
23d928a1 | 115 | $self->{plugin} = $plugin; |
f08b2779 | 116 | $self->{in_chroot} = 0; |
23d928a1 WB |
117 | |
118 | # Cache some host files we need access to: | |
119 | $plugin->{host_resolv_conf} = PVE::INotify::read_file('resolvconf'); | |
e6e308ae | 120 | $plugin->{host_localtime} = abs_path('/etc/localtime'); |
c6a605f9 WB |
121 | |
122 | # pass on user namespace information: | |
123 | my ($id_map, $rootuid, $rootgid) = PVE::LXC::parse_id_maps($conf); | |
124 | if (@$id_map) { | |
125 | $plugin->{id_map} = $id_map; | |
126 | $plugin->{rootuid} = $rootuid; | |
127 | $plugin->{rootgid} = $rootgid; | |
128 | } | |
1db602d0 | 129 | |
1c7f4f65 DM |
130 | return $self; |
131 | } | |
132 | ||
9ba0e0b5 | 133 | # Forks into a chroot and executes $sub |
f08b2779 WB |
134 | sub protected_call { |
135 | my ($self, $sub) = @_; | |
136 | ||
137 | # avoid recursion: | |
138 | return $sub->() if $self->{in_chroot}; | |
139 | ||
9ba0e0b5 WB |
140 | pipe(my $res_in, my $res_out) or die "pipe failed: $!\n"; |
141 | ||
f08b2779 WB |
142 | my $child = fork(); |
143 | die "fork failed: $!\n" if !defined($child); | |
144 | ||
145 | if (!$child) { | |
9ba0e0b5 | 146 | close($res_in); |
f08b2779 WB |
147 | # avoid recursive forks |
148 | $self->{in_chroot} = 1; | |
f08b2779 | 149 | eval { |
229ab7fa | 150 | my $rootdir = $self->{rootdir}; |
f08b2779 WB |
151 | chroot($rootdir) or die "failed to change root to: $rootdir: $!\n"; |
152 | chdir('/') or die "failed to change to root directory\n"; | |
9ba0e0b5 WB |
153 | my $res = $sub->(); |
154 | if (defined($res)) { | |
155 | print {$res_out} "$res"; | |
156 | $res_out->flush(); | |
157 | } | |
f08b2779 WB |
158 | }; |
159 | if (my $err = $@) { | |
797e12e8 | 160 | warn $err; |
f08b2779 WB |
161 | POSIX::_exit(1); |
162 | } | |
163 | POSIX::_exit(0); | |
164 | } | |
9ba0e0b5 WB |
165 | close($res_out); |
166 | my $result = do { local $/ = undef; <$res_in> }; | |
f08b2779 | 167 | while (waitpid($child, 0) != $child) {} |
952de558 WB |
168 | if ($? != 0) { |
169 | my $method = (caller(1))[3]; | |
170 | die "error in setup task $method\n"; | |
171 | } | |
9ba0e0b5 | 172 | return $result; |
f08b2779 WB |
173 | } |
174 | ||
142444d5 DM |
175 | sub template_fixup { |
176 | my ($self) = @_; | |
177 | ||
238b7e3e DM |
178 | return if !$self->{plugin}; # unmanaged |
179 | ||
1db602d0 | 180 | $self->protected_call(sub { $self->{plugin}->template_fixup($self->{conf}) }); |
142444d5 DM |
181 | } |
182 | ||
1c7f4f65 | 183 | sub setup_network { |
55fa4e09 | 184 | my ($self) = @_; |
1c7f4f65 | 185 | |
238b7e3e DM |
186 | return if !$self->{plugin}; # unmanaged |
187 | ||
1db602d0 | 188 | $self->protected_call(sub { $self->{plugin}->setup_network($self->{conf}) }); |
1c7f4f65 DM |
189 | } |
190 | ||
191 | sub set_hostname { | |
192 | my ($self) = @_; | |
193 | ||
238b7e3e DM |
194 | return if !$self->{plugin}; # unmanaged |
195 | ||
1db602d0 | 196 | $self->protected_call(sub { $self->{plugin}->set_hostname($self->{conf}) }); |
1c7f4f65 DM |
197 | } |
198 | ||
c325b32f DM |
199 | sub set_dns { |
200 | my ($self) = @_; | |
201 | ||
238b7e3e DM |
202 | return if !$self->{plugin}; # unmanaged |
203 | ||
1db602d0 | 204 | $self->protected_call(sub { $self->{plugin}->set_dns($self->{conf}) }); |
c325b32f DM |
205 | } |
206 | ||
e6e308ae OB |
207 | sub set_timezone { |
208 | my ($self) = @_; | |
209 | ||
210 | return if !$self->{plugin}; # unmanaged | |
1db602d0 TL |
211 | |
212 | $self->protected_call(sub { $self->{plugin}->set_timezone($self->{conf}) }); | |
e6e308ae OB |
213 | } |
214 | ||
d66768a2 DM |
215 | sub setup_init { |
216 | my ($self) = @_; | |
217 | ||
238b7e3e DM |
218 | return if !$self->{plugin}; # unmanaged |
219 | ||
1db602d0 | 220 | $self->protected_call(sub { $self->{plugin}->setup_init($self->{conf}) }); |
d66768a2 DM |
221 | } |
222 | ||
168d6b07 DM |
223 | sub set_user_password { |
224 | my ($self, $user, $pw) = @_; | |
238b7e3e DM |
225 | |
226 | return if !$self->{plugin}; # unmanaged | |
227 | ||
1db602d0 | 228 | $self->protected_call(sub { $self->{plugin}->set_user_password($self->{conf}, $user, $pw) }); |
1c7f4f65 DM |
229 | } |
230 | ||
6d9173c3 TL |
231 | my sub generate_ssh_key { # create temporary key in hosts' /run, then read and unlink |
232 | my ($type, $comment) = @_; | |
233 | ||
234 | my $key_id = ''; | |
235 | my $keygen_outfunc = sub { | |
236 | if ($_[0] =~ m/^((?:[0-9a-f]{2}:)+[0-9a-f]{2}|SHA256:[0-9a-z+\/]{43})\s+\Q$comment\E$/i) { | |
237 | $key_id = $_[0]; | |
238 | } | |
239 | }; | |
240 | my $file = "/run/pve/.tmp$$.$type"; | |
241 | PVE::Tools::run_command( | |
242 | ['ssh-keygen', '-f', $file, '-t', $type, '-N', '', '-E', 'sha256', '-C', $comment], | |
243 | outfunc => $keygen_outfunc, | |
244 | ); | |
245 | my ($private) = (PVE::Tools::file_get_contents($file) =~ /^(.*)$/sg); # untaint | |
246 | my ($public) = (PVE::Tools::file_get_contents("$file.pub") =~ /^(.*)$/sg); # untaint | |
247 | unlink $file, "$file.pub"; | |
248 | ||
249 | return ($key_id, $private, $public); | |
250 | } | |
251 | ||
7ee31468 DM |
252 | sub rewrite_ssh_host_keys { |
253 | my ($self) = @_; | |
254 | ||
238b7e3e DM |
255 | return if !$self->{plugin}; # unmanaged |
256 | ||
797e12e8 | 257 | my $plugin = $self->{plugin}; |
797e12e8 | 258 | |
9cc77d55 | 259 | return if ! -d "$self->{rootdir}/etc/ssh"; |
797e12e8 WB |
260 | |
261 | my $keynames = { | |
797e12e8 WB |
262 | rsa => 'ssh_host_rsa_key', |
263 | dsa => 'ssh_host_dsa_key', | |
30e7205e | 264 | ecdsa => 'ssh_host_ecdsa_key', |
797e12e8 WB |
265 | ed25519 => 'ssh_host_ed25519_key', |
266 | }; | |
267 | ||
9cc77d55 | 268 | my $hostname = $self->{conf}->{hostname} || 'localhost'; |
797e12e8 WB |
269 | $hostname =~ s/\..*$//; |
270 | ||
6d9173c3 TL |
271 | my $keyfiles = []; |
272 | for my $keytype (keys $keynames->%*) { | |
797e12e8 | 273 | my $basename = $keynames->{$keytype}; |
797e12e8 | 274 | print "Creating SSH host key '$basename' - this may take some time ...\n"; |
6d9173c3 TL |
275 | my ($id, $private, $public) = generate_ssh_key($keytype, "root\@$hostname"); |
276 | print "done: $id\n"; | |
277 | ||
278 | push $keyfiles->@*, ["/etc/ssh/$basename", $private, 0600], ["/etc/ssh/$basename.pub", $public, 0644]; | |
797e12e8 WB |
279 | } |
280 | ||
6d9173c3 TL |
281 | $self->protected_call(sub { # write them now all to the CTs rootfs at once |
282 | for my $file ($keyfiles->@*) { | |
283 | $plugin->ct_file_set_contents($file->@*); | |
797e12e8 | 284 | } |
30e7205e | 285 | }); |
1db602d0 | 286 | } |
7ee31468 | 287 | |
24f37c3d TL |
288 | my sub get_emulator { |
289 | my ($container_arch) = @_; | |
d66768a2 | 290 | |
24f37c3d TL |
291 | my $container_emulator_path = { |
292 | 'amd64' => '/usr/bin/qemu-x86_64-static', | |
293 | 'arm64' => '/usr/bin/qemu-aarch64-static', | |
294 | }; | |
238b7e3e | 295 | |
a0745b47 | 296 | my $host_arch = PVE::Tools::get_host_arch(); |
24f37c3d | 297 | if ($host_arch eq 'x86_64') { # containers use different architecture names |
11e1612f SR |
298 | $host_arch = 'amd64'; |
299 | } elsif ($host_arch eq 'aarch64') { | |
300 | $host_arch = 'arm64'; | |
301 | } else { | |
302 | die "unsupported host architecture '$host_arch'\n"; | |
303 | } | |
304 | ||
0c59b609 DM |
305 | $container_arch = 'amd64' if $container_arch eq 'i386'; # always use 64 bit version |
306 | $container_arch = 'arm64' if $container_arch eq 'armhf'; # always use 64 bit version | |
307 | ||
0c59b609 | 308 | if ($host_arch ne $container_arch) { |
24f37c3d TL |
309 | if (my $emul = $container_emulator_path->{$container_arch}) { |
310 | return ($emul, PVE::Tools::file_get_contents($emul, 10*1024*1024)) if -f $emul; | |
0c59b609 DM |
311 | } |
312 | } | |
24f37c3d TL |
313 | # none required |
314 | } | |
315 | ||
316 | sub pre_start_hook { | |
317 | my ($self) = @_; | |
318 | ||
319 | return if !$self->{plugin}; # unmanaged | |
320 | ||
321 | my ($emul, $emul_data) = get_emulator($self->{conf}->{arch}); | |
0c59b609 | 322 | |
f08b2779 | 323 | my $code = sub { |
0c59b609 DM |
324 | if ($emul && $emul_data) { |
325 | $self->{plugin}->ct_file_set_contents($emul, $emul_data, 0755); | |
326 | } | |
2988dbbc | 327 | |
f08b2779 WB |
328 | $self->{plugin}->pre_start_hook($self->{conf}); |
329 | }; | |
330 | $self->protected_call($code); | |
d66768a2 DM |
331 | } |
332 | ||
64d4c144 OB |
333 | sub post_clone_hook { |
334 | my ($self, $conf) = @_; | |
335 | ||
1db602d0 | 336 | $self->protected_call(sub { $self->{plugin}->post_clone_hook($conf) }); |
64d4c144 OB |
337 | } |
338 | ||
d66768a2 | 339 | sub post_create_hook { |
f36ce482 | 340 | my ($self, $root_password, $ssh_keys) = @_; |
1c7f4f65 | 341 | |
238b7e3e DM |
342 | return if !$self->{plugin}; # unmanaged |
343 | ||
1db602d0 | 344 | $self->protected_call(sub { |
f36ce482 | 345 | $self->{plugin}->post_create_hook($self->{conf}, $root_password, $ssh_keys); |
1db602d0 | 346 | }); |
797e12e8 | 347 | $self->rewrite_ssh_host_keys(); |
1c7f4f65 DM |
348 | } |
349 | ||
42216b6b TL |
350 | sub unified_cgroupv2_support { |
351 | my ($self) = @_; | |
352 | ||
353 | $self->protected_call(sub { $self->{plugin}->unified_cgroupv2_support() }); | |
354 | } | |
355 | ||
153747ff WB |
356 | # os-release(5): |
357 | # (...) a newline-separated list of environment-like shell-compatible | |
358 | # variable assignments. (...) beyond mere variable assignments, no shell | |
359 | # features are supported (this means variable expansion is explicitly not | |
360 | # supported) (...). Variable assignment values must be enclosed in double or | |
361 | # single quotes *if* they include spaces, semicolons or other special | |
362 | # characters outside of A-Z, a-z, 0-9. Shell special characters ("$", quotes, | |
363 | # backslash, backtick) must be escaped with backslashes (...). All strings | |
364 | # should be in UTF-8 format, and non-printable characters should not be used. | |
365 | # It is not supported to concatenate multiple individually quoted strings. | |
366 | # Lines beginning with "#" shall be ignored as comments. | |
367 | my $parse_os_release = sub { | |
368 | my ($data) = @_; | |
369 | my $variables = {}; | |
370 | while (defined($data) && $data =~ /^(.+)$/gm) { | |
371 | next if $1 !~ /^\s*([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$/; | |
372 | my ($var, $content) = ($1, $2); | |
373 | chomp $content; | |
374 | ||
375 | if ($content =~ /^'([^']*)'/) { | |
376 | $variables->{$var} = $1; | |
377 | } elsif ($content =~ /^"((?:[^"\\]|\\.)*)"/) { | |
378 | my $s = $1; | |
379 | $s =~ s/(\\["'`nt\$\\])/"\"$1\""/eeg; | |
380 | $variables->{$var} = $s; | |
381 | } elsif ($content =~ /^([A-Za-z0-9]*)/) { | |
382 | $variables->{$var} = $1; | |
383 | } | |
384 | } | |
385 | return $variables; | |
386 | }; | |
387 | ||
388 | sub get_ct_os_release { | |
389 | my ($self) = @_; | |
390 | ||
1db602d0 | 391 | my $data = $self->protected_call(sub { |
153747ff WB |
392 | if (-f '/etc/os-release') { |
393 | return PVE::Tools::file_get_contents('/etc/os-release'); | |
394 | } elsif (-f '/usr/lib/os-release') { | |
395 | return PVE::Tools::file_get_contents('/usr/lib/os-release'); | |
396 | } | |
397 | return undef; | |
1db602d0 | 398 | }); |
153747ff WB |
399 | |
400 | return &$parse_os_release($data); | |
401 | } | |
402 | ||
1c7f4f65 | 403 | 1; |