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