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