]>
Commit | Line | Data |
---|---|---|
1 | package PVE::LXC::Setup::Base; | |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use Cwd 'abs_path'; | |
7 | use File::stat; | |
8 | use Digest::SHA; | |
9 | use IO::File; | |
10 | use Encode; | |
11 | use Fcntl; | |
12 | use File::Path; | |
13 | use File::Spec; | |
14 | use File::Basename; | |
15 | use POSIX (); | |
16 | ||
17 | use PVE::INotify; | |
18 | use PVE::Tools; | |
19 | use PVE::Network; | |
20 | ||
21 | use PVE::LXC::Setup::Plugin; | |
22 | use PVE::LXC::Tools; | |
23 | ||
24 | use base qw(PVE::LXC::Setup::Plugin); | |
25 | ||
26 | sub new { | |
27 | my ($class, $conf, $rootdir, $os_release) = @_; | |
28 | ||
29 | return bless { conf => $conf, rootdir => $rootdir, os_release => $os_release }, $class; | |
30 | } | |
31 | ||
32 | sub lookup_dns_conf { | |
33 | my ($self, $conf) = @_; | |
34 | ||
35 | my $nameserver = $conf->{nameserver}; | |
36 | my $searchdomains = $conf->{searchdomain}; | |
37 | ||
38 | if ($conf->{'testmode'}) { | |
39 | $nameserver //= '8.8.8.8 8.8.8.9'; | |
40 | $searchdomains //= 'proxmox.com'; | |
41 | } | |
42 | ||
43 | my $host_resolv_conf = $self->{host_resolv_conf}; | |
44 | ||
45 | if (!defined($nameserver)) { | |
46 | my @list = (); | |
47 | foreach my $k ("dns1", "dns2", "dns3") { | |
48 | if (my $ns = $host_resolv_conf->{$k}) { | |
49 | push @list, $ns; | |
50 | } | |
51 | } | |
52 | $nameserver = join(' ', @list); | |
53 | } | |
54 | ||
55 | if (!defined($searchdomains)) { | |
56 | $searchdomains = $host_resolv_conf->{search}; | |
57 | } | |
58 | ||
59 | return ($searchdomains, $nameserver); | |
60 | } | |
61 | ||
62 | sub update_etc_hosts { | |
63 | my ($self, $hostip, $oldname, $newname, $searchdomains) = @_; | |
64 | ||
65 | my $hosts_fn = '/etc/hosts'; | |
66 | return if $self->ct_is_file_ignored($hosts_fn); | |
67 | ||
68 | my $namepart = ($newname =~ s/\..*$//r); | |
69 | ||
70 | my $all_names = ''; | |
71 | if ($newname =~ /\./) { | |
72 | $all_names .= "$newname $namepart"; | |
73 | } else { | |
74 | foreach my $domain (PVE::Tools::split_list($searchdomains)) { | |
75 | $all_names .= ' ' if $all_names; | |
76 | $all_names .= "$newname.$domain"; | |
77 | } | |
78 | $all_names .= ' ' if $all_names; | |
79 | $all_names .= $newname; | |
80 | } | |
81 | ||
82 | # Prepare section: | |
83 | my $section = ''; | |
84 | ||
85 | my $lo4 = "127.0.0.1 localhost.localnet localhost\n"; | |
86 | my $lo6 = "::1 localhost.localnet localhost\n"; | |
87 | if ($self->ct_file_exists($hosts_fn)) { | |
88 | my $data = $self->ct_file_get_contents($hosts_fn); | |
89 | # don't take localhost entries within our hosts sections into account | |
90 | $data = remove_pve_sections($data); | |
91 | ||
92 | # check for existing localhost entries | |
93 | $section .= $lo4 if $data !~ /^\h*127\.0\.0\.1\h+/m; | |
94 | $section .= $lo6 if $data !~ /^\h*::1\h+/m; | |
95 | } else { | |
96 | $section .= $lo4 . $lo6; | |
97 | } | |
98 | ||
99 | if (defined($hostip)) { | |
100 | $section .= "$hostip $all_names\n"; | |
101 | } elsif ($namepart ne 'localhost') { | |
102 | $section .= "127.0.1.1 $all_names\n"; | |
103 | } else { | |
104 | $section .= "127.0.1.1 $namepart\n"; | |
105 | } | |
106 | ||
107 | $self->ct_modify_file($hosts_fn, $section); | |
108 | } | |
109 | ||
110 | sub template_fixup { | |
111 | my ($self, $conf) = @_; | |
112 | ||
113 | # do nothing by default | |
114 | } | |
115 | ||
116 | sub set_dns { | |
117 | my ($self, $conf) = @_; | |
118 | ||
119 | my ($searchdomains, $nameserver) = $self->lookup_dns_conf($conf); | |
120 | ||
121 | my $data = ''; | |
122 | ||
123 | $data .= "search " . join(' ', PVE::Tools::split_list($searchdomains)) . "\n" | |
124 | if $searchdomains; | |
125 | ||
126 | foreach my $ns ( PVE::Tools::split_list($nameserver)) { | |
127 | $data .= "nameserver $ns\n"; | |
128 | } | |
129 | ||
130 | $self->ct_modify_file("/etc/resolv.conf", $data, replace => 1); | |
131 | } | |
132 | ||
133 | sub set_hostname { | |
134 | my ($self, $conf) = @_; | |
135 | ||
136 | my $hostname = $conf->{hostname} || 'localhost'; | |
137 | ||
138 | my $namepart = ($hostname =~ s/\..*$//r); | |
139 | ||
140 | my $hostname_fn = "/etc/hostname"; | |
141 | ||
142 | my $oldname = $self->ct_file_read_firstline($hostname_fn) || 'localhost'; | |
143 | ||
144 | my ($ipv4, $ipv6) = PVE::LXC::get_primary_ips($conf); | |
145 | my $hostip = $ipv4 || $ipv6; | |
146 | ||
147 | my ($searchdomains) = $self->lookup_dns_conf($conf); | |
148 | ||
149 | $self->update_etc_hosts($hostip, $oldname, $hostname, $searchdomains); | |
150 | ||
151 | $self->ct_file_set_contents($hostname_fn, "$namepart\n"); | |
152 | } | |
153 | ||
154 | sub setup_network { | |
155 | my ($self, $conf) = @_; | |
156 | ||
157 | die "please implement this inside subclass" | |
158 | } | |
159 | ||
160 | sub setup_init { | |
161 | my ($self, $conf) = @_; | |
162 | ||
163 | die "please implement this inside subclass" | |
164 | } | |
165 | ||
166 | # A few distros as well as unprivileged containers cannot deal with the | |
167 | # /dev/lxc/ tty subdirectory. | |
168 | sub devttydir { | |
169 | my ($self, $conf) = @_; | |
170 | return $conf->{unprivileged} ? '' : 'lxc/'; | |
171 | } | |
172 | ||
173 | sub fixup_old_getty { | |
174 | my ($self) = @_; | |
175 | ||
176 | my $sd_dir_rel = $self->ct_is_executable("/lib/systemd/systemd") ? | |
177 | "/lib/systemd/system" : "/usr/lib/systemd/system"; | |
178 | ||
179 | my $sd_getty_service_rel = "$sd_dir_rel/getty\@.service"; | |
180 | return if !$self->ct_file_exists($sd_getty_service_rel); | |
181 | ||
182 | my $raw = $self->ct_file_get_contents($sd_getty_service_rel); | |
183 | ||
184 | my $sd_container_getty_service_rel = "$sd_dir_rel/container-getty\@.service"; | |
185 | # systemd on CenoOS 7.1 is too old (version 205), so there is no | |
186 | # container-getty service | |
187 | if (!$self->ct_file_exists($sd_container_getty_service_rel)) { | |
188 | if ($raw =~ s!^ConditionPathExists=/dev/tty0$!ConditionPathExists=/dev/tty!m) { | |
189 | $self->ct_file_set_contents($sd_getty_service_rel, $raw); | |
190 | } | |
191 | } else { | |
192 | # undo above change (in case someone updated systemd) | |
193 | if ($raw =~ s!^ConditionPathExists=/dev/tty$!ConditionPathExists=/dev/tty0!m) { | |
194 | $self->ct_file_set_contents($sd_getty_service_rel, $raw); | |
195 | } | |
196 | } | |
197 | } | |
198 | ||
199 | sub setup_container_getty_service { | |
200 | my ($self, $conf) = @_; | |
201 | ||
202 | my $sd_dir = $self->ct_is_executable("/lib/systemd/systemd") ? | |
203 | "/lib/systemd/system" : "/usr/lib/systemd/system"; | |
204 | ||
205 | # prefer container-getty.service shipped by newer systemd versions | |
206 | # fallback to getty.service and just return if that doesn't exists either.. | |
207 | my $template_base = "container-getty\@"; | |
208 | my $template_path = "${sd_dir}/${template_base}.service"; | |
209 | my $instance_base = $template_base; | |
210 | ||
211 | if (!$self->ct_file_exists($template_path)) { | |
212 | $template_base = "getty\@"; | |
213 | $template_path = "${template_base}.service"; | |
214 | $instance_base = "{$template_base}tty"; | |
215 | return if !$self->ct_file_exists($template_path); | |
216 | } | |
217 | ||
218 | my $raw = $self->ct_file_get_contents($template_path); | |
219 | my $ttyname = $self->devttydir($conf) . 'tty%I'; | |
220 | if ($raw =~ s@pts/%I|lxc/tty%I@$ttyname@g) { | |
221 | $self->ct_file_set_contents($template_path, $raw); | |
222 | } | |
223 | ||
224 | my $getty_target_fn = "/etc/systemd/system/getty.target.wants/"; | |
225 | my $ttycount = PVE::LXC::Config->get_tty_count($conf); | |
226 | ||
227 | for (my $i = 1; $i < 7; $i++) { | |
228 | # ensure that not two gettys are using the same tty! | |
229 | $self->ct_unlink("$getty_target_fn/getty\@tty$i.service"); | |
230 | $self->ct_unlink("$getty_target_fn/container-getty\@$i.service"); | |
231 | ||
232 | # re-enable only those requested | |
233 | if ($i <= $ttycount) { | |
234 | my $tty_service = "${instance_base}${i}.service"; | |
235 | ||
236 | $self->ct_symlink($template_path, "$getty_target_fn/$tty_service"); | |
237 | } | |
238 | } | |
239 | ||
240 | # ensure getty.target is not masked | |
241 | $self->ct_unlink("/etc/systemd/system/getty.target"); | |
242 | } | |
243 | ||
244 | sub setup_systemd_networkd { | |
245 | my ($self, $conf) = @_; | |
246 | ||
247 | foreach my $k (keys %$conf) { | |
248 | next if $k !~ m/^net(\d+)$/; | |
249 | my $d = PVE::LXC::Config->parse_lxc_network($conf->{$k}); | |
250 | next if !$d->{name}; | |
251 | ||
252 | my $filename = "/etc/systemd/network/$d->{name}.network"; | |
253 | ||
254 | my $data = <<"DATA"; | |
255 | [Match] | |
256 | Name = $d->{name} | |
257 | ||
258 | [Network] | |
259 | Description = Interface $d->{name} autoconfigured by PVE | |
260 | DATA | |
261 | ||
262 | my $routes = ''; | |
263 | my ($has_ipv4, $has_ipv6); | |
264 | ||
265 | # DHCP bitflags: | |
266 | my @DHCPMODES = ('no', 'ipv4', 'ipv6', 'yes'); | |
267 | my ($NONE, $DHCP4, $DHCP6, $BOTH) = (0, 1, 2, 3); | |
268 | my $dhcp = $NONE; | |
269 | my $accept_ra = 'false'; | |
270 | ||
271 | if (defined(my $ip = $d->{ip})) { | |
272 | if ($ip eq 'dhcp') { | |
273 | $dhcp |= $DHCP4; | |
274 | } elsif ($ip ne 'manual') { | |
275 | $has_ipv4 = 1; | |
276 | $data .= "Address = $ip\n"; | |
277 | } | |
278 | } | |
279 | if (defined(my $gw = $d->{gw})) { | |
280 | $data .= "Gateway = $gw\n"; | |
281 | if ($has_ipv4 && !PVE::Network::is_ip_in_cidr($gw, $d->{ip}, 4)) { | |
282 | $routes .= "\n[Route]\nDestination = $gw/32\nScope = link\n"; | |
283 | } | |
284 | } | |
285 | ||
286 | if (defined(my $ip = $d->{ip6})) { | |
287 | if ($ip eq 'dhcp') { | |
288 | $dhcp |= $DHCP6; | |
289 | } elsif ($ip eq 'auto') { | |
290 | $accept_ra = 'true'; | |
291 | } elsif ($ip ne 'manual') { | |
292 | $has_ipv6 = 1; | |
293 | $data .= "Address = $ip\n"; | |
294 | } | |
295 | } | |
296 | if (defined(my $gw = $d->{gw6})) { | |
297 | $accept_ra = 'false'; | |
298 | $data .= "Gateway = $gw\n"; | |
299 | if ($has_ipv6 && !PVE::Network::is_ip_in_cidr($gw, $d->{ip6}, 6) && | |
300 | !PVE::Network::is_ip_in_cidr($gw, 'fe80::/10', 6)) { | |
301 | $routes .= "\n[Route]\nDestination = $gw/128\nScope = link\n"; | |
302 | } | |
303 | } | |
304 | ||
305 | $data .= "DHCP = $DHCPMODES[$dhcp]\n"; | |
306 | $data .= "IPv6AcceptRA = $accept_ra\n"; | |
307 | $data .= $routes if $routes; | |
308 | ||
309 | $self->ct_file_set_contents($filename, $data); | |
310 | } | |
311 | } | |
312 | ||
313 | # At first boot (/etc/machine-id not existing), systemd goes through a set of `presets` to bring the | |
314 | # enable/disable state of services to a default. | |
315 | # Meaning for newer templates we should use this instead of manually creating enable/disable | |
316 | # symlinks. | |
317 | # | |
318 | # Disables some common problematic and/or useless units by default. | |
319 | # | |
320 | # `$extra_preset` should map service names to a bool-ish. It can also hold overrides for the default | |
321 | # presets. | |
322 | sub setup_systemd_preset { | |
323 | my ($self, $extra_preset) = @_; | |
324 | ||
325 | # some don't make sense in CTs, child-plugins can still override through extra_presets | |
326 | my $preset = { | |
327 | 'sys-kernel-config.mount' => 0, | |
328 | 'sys-kernel-debug.mount' => 0, | |
329 | # NOTE: some older distro releases (e.g., centos 7.0) had no container-getty, which itself | |
330 | # is not an issue for presets, but disabling getty@ then could cause issues. | |
331 | 'getty@.service' => 0, | |
332 | 'container-getty@.service' => 1, | |
333 | }; | |
334 | ||
335 | if (defined($extra_preset)) { | |
336 | $preset->{$_} = $extra_preset->{$_} for keys $extra_preset->%*; | |
337 | } | |
338 | ||
339 | my $preset_data = "# Added by PVE at create-time for first-boot configuration.\n"; | |
340 | for my $service (sort keys %$preset) { | |
341 | if ($preset->{$service}) { | |
342 | $preset_data .= "enable $service\n"; | |
343 | } else { | |
344 | $preset_data .= "disable $service\n"; | |
345 | } | |
346 | } | |
347 | ||
348 | $self->ct_mkdir('/etc/systemd/system-preset', 0755); | |
349 | $self->ct_file_set_contents( | |
350 | '/etc/systemd/system-preset/00-pve.preset', | |
351 | $preset_data, | |
352 | ); | |
353 | } | |
354 | ||
355 | sub setup_securetty { | |
356 | my ($self, $conf, @add) = @_; | |
357 | ||
358 | my $filename = "/etc/securetty"; | |
359 | # root login is already allowed on every device if no securetty present | |
360 | return if !$self->ct_file_exists($filename); | |
361 | ||
362 | if (!scalar(@add)) { | |
363 | @add = qw(console tty1 tty2 tty3 tty4); | |
364 | if (my $dir = $self->devttydir($conf)) { | |
365 | @add = map { "${dir}$_" } @add; | |
366 | } | |
367 | } | |
368 | ||
369 | my $data = $self->ct_file_get_contents($filename); | |
370 | chomp $data; $data .= "\n"; | |
371 | foreach my $dev (@add) { | |
372 | if ($data !~ m!^\Q$dev\E\s*$!m) { | |
373 | $data .= "$dev\n"; | |
374 | } | |
375 | } | |
376 | $self->ct_file_set_contents($filename, $data); | |
377 | } | |
378 | ||
379 | my $replacepw = sub { | |
380 | my ($self, $file, $user, $epw, $shadow) = @_; | |
381 | ||
382 | my $tmpfile = "$file.$$"; | |
383 | ||
384 | eval { | |
385 | my $src = $self->ct_open_file_read($file) || | |
386 | die "unable to open file '$file' - $!"; | |
387 | ||
388 | my $st = $self->ct_stat($src) || | |
389 | die "unable to stat file - $!"; | |
390 | ||
391 | my $dst = $self->ct_open_file_write($tmpfile) || | |
392 | die "unable to open file '$tmpfile' - $!"; | |
393 | ||
394 | # copy owner and permissions | |
395 | chmod $st->mode, $dst; | |
396 | chown $st->uid, $st->gid, $dst; | |
397 | ||
398 | my $last_change = int(time()/(60*60*24)); | |
399 | ||
400 | while (defined (my $line = <$src>)) { | |
401 | if ($shadow) { | |
402 | $line =~ s/^${user}:[^:]*:[^:]*:/${user}:${epw}:${last_change}:/; | |
403 | } else { | |
404 | $line =~ s/^${user}:[^:]*:/${user}:${epw}:/; | |
405 | } | |
406 | print $dst $line; | |
407 | } | |
408 | ||
409 | $src->close() || die "close '$file' failed - $!\n"; | |
410 | $dst->close() || die "close '$tmpfile' failed - $!\n"; | |
411 | }; | |
412 | if (my $err = $@) { | |
413 | $self->ct_unlink($tmpfile); | |
414 | } else { | |
415 | $self->ct_rename($tmpfile, $file); | |
416 | $self->ct_unlink($tmpfile); # in case rename fails | |
417 | } | |
418 | }; | |
419 | ||
420 | sub set_user_password { | |
421 | my ($self, $conf, $user, $opt_password) = @_; | |
422 | ||
423 | my $pwfile = "/etc/passwd"; | |
424 | ||
425 | return if !$self->ct_file_exists($pwfile); | |
426 | ||
427 | my $shadow = "/etc/shadow"; | |
428 | ||
429 | if (defined($opt_password)) { | |
430 | if ($opt_password !~ m/^\$(?:1|2[axy]?|5|6)\$[a-zA-Z0-9.\/]{1,16}\$[a-zA-Z0-9.\/]+$/) { | |
431 | my $time = substr (Digest::SHA::sha1_base64 (time), 0, 8); | |
432 | $opt_password = crypt(encode("utf8", $opt_password), "\$6\$$time\$"); | |
433 | }; | |
434 | } else { | |
435 | $opt_password = '*'; | |
436 | } | |
437 | ||
438 | if ($self->ct_file_exists($shadow)) { | |
439 | &$replacepw ($self, $shadow, $user, $opt_password, 1); | |
440 | &$replacepw ($self, $pwfile, $user, 'x'); | |
441 | } else { | |
442 | &$replacepw ($self, $pwfile, $user, $opt_password); | |
443 | } | |
444 | } | |
445 | ||
446 | my $parse_home_dir = sub { | |
447 | my ($self, $passwdfile, $user) = @_; | |
448 | ||
449 | my $fh = $self->ct_open_file_read($passwdfile); | |
450 | while (defined (my $line = <$fh>)) { | |
451 | return $2 | |
452 | if $line =~ m/^${user}:([^:]*:){4}([^:]*):/; | |
453 | } | |
454 | }; | |
455 | ||
456 | sub set_user_authorized_ssh_keys { | |
457 | my ($self, $conf, $user, $ssh_keys) = @_; | |
458 | ||
459 | my $passwd = "/etc/passwd"; | |
460 | my $home = $user eq "root" ? "/root/" : "/home/$user/"; | |
461 | ||
462 | $home = &$parse_home_dir($self, $passwd, $user) | |
463 | if $self->ct_file_exists($passwd); | |
464 | ||
465 | die "home directory '$home' of $user does not exist!" | |
466 | if ! ($self->ct_is_directory($home) || $self->ct_is_symlink($home)); | |
467 | ||
468 | $self->ct_mkdir("$home/.ssh", 0700) | |
469 | if ! $self->ct_is_directory("$home/.ssh"); | |
470 | ||
471 | $self->ct_modify_file("$home/.ssh/authorized_keys", $ssh_keys, perms => 0700); | |
472 | } | |
473 | ||
474 | my $randomize_crontab = sub { | |
475 | my ($self, $conf) = @_; | |
476 | ||
477 | my @files; | |
478 | # Note: dir_glob_foreach() untaints filenames! | |
479 | PVE::Tools::dir_glob_foreach("/etc/cron.d", qr/[A-Z\-\_a-z0-9]+/, sub { | |
480 | my ($name) = @_; | |
481 | push @files, "/etc/cron.d/$name"; | |
482 | }); | |
483 | ||
484 | my $crontab_fn = "/etc/crontab"; | |
485 | unshift @files, $crontab_fn if $self->ct_file_exists($crontab_fn); | |
486 | ||
487 | foreach my $filename (@files) { | |
488 | my $data = $self->ct_file_get_contents($filename); | |
489 | my $new = ''; | |
490 | foreach my $line (split(/\n/, $data)) { | |
491 | # we only randomize minutes for root crontab entries | |
492 | if ($line =~ m/^\d+(\s+\S+\s+\S+\s+\S+\s+\S+\s+root\s+\S.*)$/) { | |
493 | my $rest = $1; | |
494 | my $min = int(rand()*59); | |
495 | $new .= "$min$rest\n"; | |
496 | } else { | |
497 | $new .= "$line\n"; | |
498 | } | |
499 | } | |
500 | $self->ct_file_set_contents($filename, $new); | |
501 | } | |
502 | }; | |
503 | ||
504 | sub set_timezone { | |
505 | my ($self, $conf) = @_; | |
506 | ||
507 | my $zoneinfo = $conf->{timezone}; | |
508 | ||
509 | return if !defined($zoneinfo); | |
510 | ||
511 | my $tz_path = "/usr/share/zoneinfo/$zoneinfo"; | |
512 | ||
513 | if ($zoneinfo eq 'host') { | |
514 | $tz_path = $self->{host_localtime}; | |
515 | } | |
516 | ||
517 | if ($self->ct_file_exists($tz_path)) { | |
518 | if (abs_path('/etc/localtime') ne $tz_path) { | |
519 | my $tmpfile = "localtime.$$.new.tmpfile"; | |
520 | $self->ct_symlink($tz_path, $tmpfile); | |
521 | $self->ct_rename($tmpfile, "/etc/localtime"); | |
522 | } | |
523 | ||
524 | # not all distributions have /etc/timezone | |
525 | if ($self->ct_file_exists('/etc/timezone')) { | |
526 | my $contents = $zoneinfo eq 'host' ? $self->{host_timezone} : $zoneinfo; | |
527 | $self->ct_file_set_contents('/etc/timezone', "$contents\n"); | |
528 | } | |
529 | } else { | |
530 | warn "container does not have $tz_path, timezone can not be modified\n"; | |
531 | } | |
532 | } | |
533 | ||
534 | sub clear_machine_id { | |
535 | my ($self, $conf, $clone) = @_; | |
536 | ||
537 | my $uses_systemd = $self->ct_is_executable("/lib/systemd/systemd") | |
538 | || $self->ct_is_executable("/usr/lib/systemd/systemd"); | |
539 | ||
540 | my $dbus_machine_id_path = "/var/lib/dbus/machine-id"; | |
541 | my $machine_id_path = "/etc/machine-id"; | |
542 | ||
543 | my $machine_id_existed = $self->ct_file_exists($machine_id_path); | |
544 | ||
545 | if ( | |
546 | $self->ct_file_exists($dbus_machine_id_path) | |
547 | && !$self->ct_is_symlink($dbus_machine_id_path) | |
548 | && $uses_systemd | |
549 | ) { | |
550 | $self->ct_unlink($dbus_machine_id_path); | |
551 | } | |
552 | ||
553 | if ($machine_id_existed) { | |
554 | # truncate exiting ones on clone to avoid FirstBoot condition. admins can override this by | |
555 | # removing the machine-id file or setting it to uninitialized before creating a template, or | |
556 | # cloning a guest - as per machine-id(5) man page. TODO: add explicit switch to API? | |
557 | if ($clone) { | |
558 | my $old_machine_id = $self->ct_file_read_firstline($machine_id_path) // ''; | |
559 | if ($uses_systemd && $old_machine_id ne 'uninitialized') { | |
560 | $self->ct_file_set_contents($machine_id_path, "\n") if $uses_systemd; | |
561 | } | |
562 | } else { | |
563 | $self->ct_unlink($machine_id_path); | |
564 | } | |
565 | } | |
566 | } | |
567 | ||
568 | # tries to guess the systemd (major) version based on the | |
569 | # libsystemd-shared<version>.so linked with /sbin/init | |
570 | sub get_systemd_version { | |
571 | my ($self, $init) = @_; | |
572 | ||
573 | my $version = undef; | |
574 | PVE::Tools::run_command( | |
575 | ['objdump', '-p', $self->{rootdir}.$init], | |
576 | outfunc => sub { | |
577 | my $line = shift; | |
578 | if ($line =~ /libsystemd-shared-(\d+)(?:[-.][a-zA-Z0-9]+)*\.so:?$/) { | |
579 | $version = $1; | |
580 | } | |
581 | }, | |
582 | errmsg => "objdump on $init failed", | |
583 | ); | |
584 | ||
585 | return $version; | |
586 | } | |
587 | ||
588 | sub unified_cgroupv2_support { | |
589 | my ($self, $init) = @_; | |
590 | ||
591 | # https://www.freedesktop.org/software/systemd/man/systemd.html | |
592 | # systemd is installed as symlink to /sbin/init | |
593 | # assume non-systemd init will run with unified cgroupv2 | |
594 | if (!defined($init) || $init !~ m@/systemd$@) { | |
595 | return 1; | |
596 | } | |
597 | ||
598 | # systemd version 232 (e.g. debian stretch) supports the unified hierarchy | |
599 | my $sdver = $self->get_systemd_version($init); | |
600 | if (!defined($sdver) || $sdver < 232) { | |
601 | return 0; | |
602 | } | |
603 | ||
604 | return 1; | |
605 | } | |
606 | ||
607 | sub get_ct_init_path { | |
608 | my ($self) = @_; | |
609 | ||
610 | my $init_path = "/sbin/init"; | |
611 | if ($self->ct_is_symlink($init_path)) { | |
612 | $init_path = $self->ct_readlink_recursive($init_path); | |
613 | } | |
614 | return $init_path; | |
615 | } | |
616 | ||
617 | sub ssh_host_key_types_to_generate { | |
618 | my ($self) = @_; | |
619 | ||
620 | return { | |
621 | rsa => 'ssh_host_rsa_key', | |
622 | dsa => 'ssh_host_dsa_key', | |
623 | ecdsa => 'ssh_host_ecdsa_key', | |
624 | ed25519 => 'ssh_host_ed25519_key', | |
625 | }; | |
626 | } | |
627 | ||
628 | sub detect_architecture { | |
629 | my ($self) = @_; | |
630 | ||
631 | # '/bin/sh' is POSIX mandatory | |
632 | return PVE::LXC::Tools::detect_elf_architecture('/bin/sh'); | |
633 | } | |
634 | ||
635 | sub pre_start_hook { | |
636 | my ($self, $conf) = @_; | |
637 | ||
638 | $self->ct_file_set_contents('/fastboot', ''); # skips fsck, among other things | |
639 | ||
640 | $self->setup_init($conf); | |
641 | $self->setup_network($conf); | |
642 | $self->set_hostname($conf); | |
643 | $self->set_dns($conf); | |
644 | $self->set_timezone($conf); | |
645 | ||
646 | # fixme: what else ? | |
647 | } | |
648 | ||
649 | sub post_clone_hook { | |
650 | my ($self, $conf) = @_; | |
651 | ||
652 | $self->clear_machine_id($conf, 1); | |
653 | } | |
654 | ||
655 | sub post_create_hook { | |
656 | my ($self, $conf, $root_password, $ssh_keys) = @_; | |
657 | ||
658 | $self->clear_machine_id($conf); | |
659 | $self->template_fixup($conf); | |
660 | ||
661 | &$randomize_crontab($self, $conf); | |
662 | ||
663 | $self->set_user_password($conf, 'root', $root_password); | |
664 | $self->set_user_authorized_ssh_keys($conf, 'root', $ssh_keys) if $ssh_keys; | |
665 | $self->setup_init($conf); | |
666 | $self->setup_network($conf); | |
667 | $self->set_hostname($conf); | |
668 | $self->set_dns($conf); | |
669 | $self->set_timezone($conf); | |
670 | ||
671 | # fixme: what else ? | |
672 | } | |
673 | ||
674 | # File access wrappers for container setup code. | |
675 | # NOTE: those are not direct part of the Plugin API (yet), avoid using them outside the child plugins | |
676 | # For user-namespace support these might need to take uid and gid maps into account. | |
677 | ||
678 | sub ct_is_file_ignored { | |
679 | my ($self, $file) = @_; | |
680 | my ($name, $path) = fileparse($file); | |
681 | return -f "$path/.pve-ignore.$name"; | |
682 | } | |
683 | ||
684 | sub ct_reset_ownership { | |
685 | my ($self, @files) = @_; | |
686 | my $conf = $self->{conf}; | |
687 | return if !$self->{id_map}; | |
688 | ||
689 | @files = grep { !$self->ct_is_file_ignored($_) } @files; | |
690 | return if !@files; | |
691 | ||
692 | my $uid = $self->{root_uid}; | |
693 | my $gid = $self->{root_gid}; | |
694 | chown($uid, $gid, @files); | |
695 | } | |
696 | ||
697 | sub ct_mkdir { | |
698 | my ($self, $file, $mask) = @_; | |
699 | # mkdir goes by parameter count - an `undef' mode acts like a mode of 0000 | |
700 | if (defined($mask)) { | |
701 | return CORE::mkdir($file, $mask) && $self->ct_reset_ownership($file); | |
702 | } else { | |
703 | return CORE::mkdir($file) && $self->ct_reset_ownership($file); | |
704 | } | |
705 | } | |
706 | ||
707 | sub ct_unlink { | |
708 | my ($self, @files) = @_; | |
709 | foreach my $file (@files) { | |
710 | next if $self->ct_is_file_ignored($file); | |
711 | CORE::unlink($file); | |
712 | } | |
713 | } | |
714 | ||
715 | sub ct_rename { | |
716 | my ($self, $old, $new) = @_; | |
717 | return if $self->ct_is_file_ignored($new); | |
718 | CORE::rename($old, $new); | |
719 | } | |
720 | ||
721 | sub ct_open_file_read { | |
722 | my $self = shift; | |
723 | my $file = shift; | |
724 | return IO::File->new($file, O_RDONLY, @_); | |
725 | } | |
726 | ||
727 | sub ct_open_file_write { | |
728 | my $self = shift; | |
729 | my $file = shift; | |
730 | $file = '/dev/null' if $self->ct_is_file_ignored($file); | |
731 | my $fh = IO::File->new($file, O_WRONLY | O_CREAT, @_); | |
732 | $self->ct_reset_ownership($fh); | |
733 | return $fh; | |
734 | } | |
735 | ||
736 | sub ct_make_path { | |
737 | my $self = shift; | |
738 | ||
739 | my $opts = {}; | |
740 | if (defined($self->{id_map})) { | |
741 | $opts->{owner} = $self->{root_uid}; | |
742 | $opts->{group} = $self->{root_gid}; | |
743 | } | |
744 | File::Path::make_path(@_, $opts); | |
745 | } | |
746 | ||
747 | sub ct_symlink { | |
748 | my ($self, $old, $new) = @_; | |
749 | return if $self->ct_is_file_ignored($new); | |
750 | if (CORE::symlink($old, $new)) { | |
751 | if (defined($self->{id_map})) { | |
752 | POSIX::lchown($self->{root_uid}, $self->{root_gid}, $new); | |
753 | } | |
754 | return 1; | |
755 | } else { | |
756 | return 0; | |
757 | } | |
758 | } | |
759 | ||
760 | sub ct_readlink { | |
761 | my ($self, $name) = @_; | |
762 | return CORE::readlink($name); | |
763 | } | |
764 | ||
765 | sub ct_readlink_recursive { | |
766 | my ($self, $name) = @_; | |
767 | ||
768 | my $res = $name; | |
769 | for (my $i = 0; $self->ct_is_symlink($res); $i++) { | |
770 | # arbitrary limit, but should be enough for all for our management relevant things | |
771 | die "maximal link depth of 10 for resolving '$name' reached, abort\n" if $i >= 10; | |
772 | $res = $self->ct_readlink($res); | |
773 | $res = abs_path($res); | |
774 | } | |
775 | return $res; | |
776 | } | |
777 | ||
778 | sub ct_file_exists { | |
779 | my ($self, $file) = @_; | |
780 | return -f $file; | |
781 | } | |
782 | ||
783 | sub ct_is_directory { | |
784 | my ($self, $file) = @_; | |
785 | return -d $file; | |
786 | } | |
787 | ||
788 | sub ct_is_symlink { | |
789 | my ($self, $file) = @_; | |
790 | return -l $file; | |
791 | } | |
792 | ||
793 | sub ct_is_executable { | |
794 | my ($self, $file) = @_; | |
795 | return -x $file | |
796 | } | |
797 | ||
798 | sub ct_stat { | |
799 | my ($self, $file) = @_; | |
800 | return File::stat::stat($file); | |
801 | } | |
802 | ||
803 | sub ct_file_read_firstline { | |
804 | my ($self, $file) = @_; | |
805 | return PVE::Tools::file_read_firstline($file); | |
806 | } | |
807 | ||
808 | sub ct_file_get_contents { | |
809 | my ($self, $file) = @_; | |
810 | return PVE::Tools::file_get_contents($file); | |
811 | } | |
812 | ||
813 | sub ct_file_set_contents { | |
814 | my ($self, $file, $data, $perms) = @_; | |
815 | return if $self->ct_is_file_ignored($file); | |
816 | PVE::Tools::file_set_contents($file, $data, $perms); | |
817 | $self->ct_reset_ownership($file); | |
818 | } | |
819 | ||
820 | # Modify a marked portion of a file. | |
821 | # Optionally if the file becomes empty it will be deleted. | |
822 | sub ct_modify_file { | |
823 | my ($self, $file, $data, %options) = @_; | |
824 | return if $self->ct_is_file_ignored($file); | |
825 | ||
826 | my $head = "# --- BEGIN PVE ---\n"; | |
827 | my $tail = "# --- END PVE ---\n"; | |
828 | my $perms = $options{perms}; | |
829 | $data .= "\n" if $data && $data !~ /\n$/; | |
830 | ||
831 | if (!$self->ct_file_exists($file)) { | |
832 | $self->ct_file_set_contents($file, $head.$data.$tail, $perms) if $data; | |
833 | return; | |
834 | } | |
835 | ||
836 | my $old = $self->ct_file_get_contents($file); | |
837 | my @lines = split(/\n/, $old); | |
838 | ||
839 | my ($beg, $end); | |
840 | foreach my $i (0..(@lines-1)) { | |
841 | my $line = $lines[$i]; | |
842 | $beg = $i if !defined($beg) && | |
843 | $line =~ /^#\s*---\s*BEGIN\s*PVE\s*/; | |
844 | $end = $i if !defined($end) && defined($beg) && | |
845 | $line =~ /^#\s*---\s*END\s*PVE\s*/i; | |
846 | last if defined($beg) && defined($end); | |
847 | } | |
848 | ||
849 | if (defined($beg) && defined($end)) { | |
850 | # Found a section | |
851 | if ($data) { | |
852 | chomp $tail; | |
853 | splice @lines, $beg, $end-$beg+1, $head.$data.$tail; | |
854 | } else { | |
855 | if ($beg == 0 && $end == (@lines-1)) { | |
856 | $self->ct_unlink($file) if $options{delete}; | |
857 | return; | |
858 | } | |
859 | splice @lines, $beg, $end-$beg+1, $head.$data.$tail; | |
860 | } | |
861 | $self->ct_file_set_contents($file, join("\n", @lines) . "\n"); | |
862 | } elsif ($data) { | |
863 | # No section found | |
864 | my $content = join("\n", @lines); | |
865 | chomp $content; | |
866 | if (!$content && !$data && $options{delete}) { | |
867 | $self->ct_unlink($file); | |
868 | return; | |
869 | } | |
870 | $content .= "\n"; | |
871 | $data = $head.$data.$tail; | |
872 | if ($options{replace}) { | |
873 | $self->ct_file_set_contents($file, $data, $perms); | |
874 | } elsif ($options{prepend}) { | |
875 | $self->ct_file_set_contents($file, $data . $content, $perms); | |
876 | } else { # append | |
877 | $self->ct_file_set_contents($file, $content . $data, $perms); | |
878 | } | |
879 | } | |
880 | } | |
881 | ||
882 | sub remove_pve_sections { | |
883 | my ($data) = @_; | |
884 | ||
885 | my $head = "# --- BEGIN PVE ---"; | |
886 | my $tail = "# --- END PVE ---"; | |
887 | ||
888 | # Remove the sections enclosed with the above headers and footers. | |
889 | # from a line (^) starting with '\h*$head' | |
890 | # to a line (the other ^) starting with '\h*$tail' up to including that | |
891 | # line's end (.*?$). | |
892 | return $data =~ s/^\h*\Q$head\E.*^\h*\Q$tail\E.*?$//rgms; | |
893 | } | |
894 | ||
895 | # templates from images.linuxcontainers.org have a bogus LXC_NAME line in /etc/hosts | |
896 | sub remove_lxc_name_from_etc_hosts { | |
897 | my ($self) = @_; | |
898 | ||
899 | return if ! -e '/etc/hosts'; | |
900 | ||
901 | my $hosts = $self->ct_file_get_contents('/etc/hosts'); | |
902 | my @lines = grep { !/^127.0.1.1\s+LXC_NAME$/ } split(/\n/, $hosts); | |
903 | ||
904 | $hosts = join("\n", @lines). "\n"; | |
905 | ||
906 | $self->ct_file_set_contents('/etc/hosts', $hosts); | |
907 | } | |
908 | ||
909 | 1; |