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