]> git.proxmox.com Git - pve-container.git/blame - src/PVE/LXC/Setup/Base.pm
Setup: order fqdn before hostname in /etc/hosts
[pve-container.git] / src / PVE / LXC / Setup / Base.pm
CommitLineData
7af97ad5 1package PVE::LXC::Setup::Base;
1c7f4f65
DM
2
3use strict;
4use warnings;
5
168d6b07
DM
6use File::stat;
7use Digest::SHA;
8use IO::File;
9use Encode;
f08b2779 10use Fcntl;
2063d380 11use File::Path;
f08b2779 12use File::Spec;
168d6b07 13
b9cd9975 14use PVE::INotify;
55fa4e09 15use PVE::Tools;
4401b7d4 16use PVE::Network;
55fa4e09 17
633a7bd8 18sub new {
5b4657d0 19 my ($class, $conf, $rootdir) = @_;
633a7bd8 20
5b4657d0 21 return bless { conf => $conf, rootdir => $rootdir }, $class;
633a7bd8 22}
b9cd9975 23
c0eae401 24sub lookup_dns_conf {
23d928a1 25 my ($self, $conf) = @_;
b9cd9975 26
27916659
DM
27 my $nameserver = $conf->{nameserver};
28 my $searchdomains = $conf->{searchdomain};
b9cd9975
DM
29
30 if (!($nameserver && $searchdomains)) {
31
27916659 32 if ($conf->{'testmode'}) {
b9cd9975
DM
33
34 $nameserver = "8.8.8.8 8.8.8.9";
e03c2cc7 35 $searchdomains = "proxmox.com";
b9cd9975
DM
36
37 } else {
38
23d928a1 39 my $host_resolv_conf = $self->{host_resolv_conf};
b9cd9975
DM
40
41 $searchdomains = $host_resolv_conf->{search};
42
43 my @list = ();
44 foreach my $k ("dns1", "dns2", "dns3") {
45 if (my $ns = $host_resolv_conf->{$k}) {
46 push @list, $ns;
47 }
48 }
49 $nameserver = join(' ', @list);
50 }
51 }
52
53 return ($searchdomains, $nameserver);
c0eae401 54}
b9cd9975 55
c0eae401 56sub update_etc_hosts {
e4929e97 57 my ($etc_hosts_data, $hostip, $oldname, $newname, $searchdomains) = @_;
1c7f4f65 58
1c7f4f65
DM
59 my $done = 0;
60
61 my @lines;
e4929e97 62
ce289e3c
WB
63 my $namepart = ($newname =~ s/\..*$//r);
64
005f91ad 65 my $all_names = '';
ce289e3c 66 if ($newname =~ /\./) {
005f91ad 67 $all_names .= "$newname $namepart";
ce289e3c
WB
68 } else {
69 foreach my $domain (PVE::Tools::split_list($searchdomains)) {
005f91ad
WB
70 $all_names .= ' ' if $all_names;
71 $all_names .= "$newname.$domain";
ce289e3c 72 }
005f91ad
WB
73 $all_names .= ' ' if $all_names;
74 $all_names .= $newname;
e4929e97 75 }
1c7f4f65
DM
76
77 foreach my $line (split(/\n/, $etc_hosts_data)) {
78 if ($line =~ m/^#/ || $line =~ m/^\s*$/) {
79 push @lines, $line;
80 next;
81 }
82
83 my ($ip, @names) = split(/\s+/, $line);
84 if (($ip eq '127.0.0.1') || ($ip eq '::1')) {
85 push @lines, $line;
86 next;
87 }
c325b32f 88
1c7f4f65
DM
89 my $found = 0;
90 foreach my $name (@names) {
91 if ($name eq $oldname || $name eq $newname) {
92 $found = 1;
93 } else {
94 # fixme: record extra names?
95 }
96 }
97 $found = 1 if defined($hostip) && ($ip eq $hostip);
98
99 if ($found) {
100 if (!$done) {
101 if (defined($hostip)) {
005f91ad 102 push @lines, "$hostip $all_names";
1c7f4f65 103 } else {
ce289e3c 104 push @lines, "127.0.1.1 $namepart";
1c7f4f65
DM
105 }
106 $done = 1;
107 }
108 next;
109 } else {
110 push @lines, $line;
111 }
112 }
113
114 if (!$done) {
115 if (defined($hostip)) {
005f91ad 116 push @lines, "$hostip $all_names";
1c7f4f65 117 } else {
ce289e3c 118 push @lines, "127.0.1.1 $namepart";
1c7f4f65
DM
119 }
120 }
121
1e180f97
DM
122 my $found_localhost = 0;
123 foreach my $line (@lines) {
124 if ($line =~ m/^127.0.0.1\s/) {
125 $found_localhost = 1;
126 last;
127 }
128 }
129
130 if (!$found_localhost) {
131 unshift @lines, "127.0.0.1 localhost.localnet localhost";
132 }
133
1c7f4f65
DM
134 $etc_hosts_data = join("\n", @lines) . "\n";
135
136 return $etc_hosts_data;
c0eae401 137}
1c7f4f65 138
142444d5
DM
139sub template_fixup {
140 my ($self, $conf) = @_;
141
142 # do nothing by default
143}
144
c325b32f 145sub set_dns {
633a7bd8 146 my ($self, $conf) = @_;
c325b32f 147
23d928a1 148 my ($searchdomains, $nameserver) = $self->lookup_dns_conf($conf);
c325b32f 149
c325b32f
DM
150 my $data = '';
151
152 $data .= "search " . join(' ', PVE::Tools::split_list($searchdomains)) . "\n"
153 if $searchdomains;
154
155 foreach my $ns ( PVE::Tools::split_list($nameserver)) {
156 $data .= "nameserver $ns\n";
157 }
158
f08b2779 159 $self->ct_file_set_contents("/etc/resolv.conf", $data);
c325b32f
DM
160}
161
1c7f4f65 162sub set_hostname {
633a7bd8 163 my ($self, $conf) = @_;
1c7f4f65 164
27916659 165 my $hostname = $conf->{hostname} || 'localhost';
1c7f4f65 166
ce289e3c 167 my $namepart = ($hostname =~ s/\..*$//r);
1c7f4f65 168
f08b2779 169 my $hostname_fn = "/etc/hostname";
1c7f4f65 170
f08b2779 171 my $oldname = $self->ct_file_read_firstline($hostname_fn) || 'localhost';
1c7f4f65 172
f08b2779 173 my $hosts_fn = "/etc/hosts";
1c7f4f65
DM
174 my $etc_hosts_data = '';
175
f08b2779
WB
176 if ($self->ct_file_exists($hosts_fn)) {
177 $etc_hosts_data = $self->ct_file_get_contents($hosts_fn);
1c7f4f65
DM
178 }
179
c325b32f
DM
180 my ($ipv4, $ipv6) = PVE::LXC::get_primary_ips($conf);
181 my $hostip = $ipv4 || $ipv6;
b9cd9975 182
23d928a1 183 my ($searchdomains) = $self->lookup_dns_conf($conf);
b9cd9975 184
c0eae401
DM
185 $etc_hosts_data = update_etc_hosts($etc_hosts_data, $hostip, $oldname,
186 $hostname, $searchdomains);
b9cd9975 187
ce289e3c 188 $self->ct_file_set_contents($hostname_fn, "$namepart\n");
f08b2779 189 $self->ct_file_set_contents($hosts_fn, $etc_hosts_data);
1c7f4f65
DM
190}
191
55fa4e09 192sub setup_network {
633a7bd8 193 my ($self, $conf) = @_;
55fa4e09
DM
194
195 die "please implement this inside subclass"
196}
197
d66768a2 198sub setup_init {
633a7bd8 199 my ($self, $conf) = @_;
1c7f4f65 200
d66768a2
DM
201 die "please implement this inside subclass"
202}
203
9143dec4
DM
204sub setup_systemd_console {
205 my ($self, $conf) = @_;
206
f08b2779 207 my $systemd_dir_rel = -x "/lib/systemd/systemd" ?
9143dec4
DM
208 "/lib/systemd/system" : "/usr/lib/systemd/system";
209
9143dec4
DM
210 my $systemd_getty_service_rel = "$systemd_dir_rel/getty\@.service";
211
f08b2779 212 return if !$self->ct_file_exists($systemd_getty_service_rel);
9143dec4 213
f08b2779 214 my $raw = $self->ct_file_get_contents($systemd_getty_service_rel);
9143dec4 215
c69ae0d0 216 my $systemd_container_getty_service_rel = "$systemd_dir_rel/container-getty\@.service";
c69ae0d0
DM
217
218 # systemd on CenoOS 7.1 is too old (version 205), so there is no
219 # container-getty service
f08b2779 220 if (!$self->ct_file_exists($systemd_container_getty_service_rel)) {
c69ae0d0 221 if ($raw =~ s!^ConditionPathExists=/dev/tty0$!ConditionPathExists=/dev/tty!m) {
f08b2779 222 $self->ct_file_set_contents($systemd_getty_service_rel, $raw);
c69ae0d0
DM
223 }
224 } else {
225 # undo above change (in case someone updated systemd)
226 if ($raw =~ s!^ConditionPathExists=/dev/tty$!ConditionPathExists=/dev/tty0!m) {
f08b2779 227 $self->ct_file_set_contents($systemd_getty_service_rel, $raw);
c69ae0d0 228 }
9143dec4
DM
229 }
230
0d0ca400 231 my $ttycount = PVE::LXC::get_tty_count($conf);
9143dec4
DM
232
233 for (my $i = 1; $i < 7; $i++) {
f08b2779 234 my $tty_service_lnk = "/etc/systemd/system/getty.target.wants/getty\@tty$i.service";
9143dec4 235 if ($i > $ttycount) {
f08b2779 236 $self->ct_unlink($tty_service_lnk);
9143dec4 237 } else {
f08b2779
WB
238 if (!$self->ct_is_symlink($tty_service_lnk)) {
239 $self->ct_unlink($tty_service_lnk);
240 $self->ct_symlink($systemd_getty_service_rel, $tty_service_lnk);
9143dec4
DM
241 }
242 }
243 }
244}
245
c1d32b55
WB
246sub setup_systemd_networkd {
247 my ($self, $conf) = @_;
248
c1d32b55
WB
249 foreach my $k (keys %$conf) {
250 next if $k !~ m/^net(\d+)$/;
251 my $d = PVE::LXC::parse_lxc_network($conf->{$k});
252 next if !$d->{name};
253
f08b2779 254 my $filename = "/etc/systemd/network/$d->{name}.network";
c1d32b55
WB
255
256 my $data = <<"DATA";
257[Match]
258Name = $d->{name}
259
260[Network]
261Description = Interface $d->{name} autoconfigured by PVE
262DATA
4401b7d4
WB
263
264 my $routes = '';
265 my ($has_ipv4, $has_ipv6);
266
c1d32b55
WB
267 # DHCP bitflags:
268 my @DHCPMODES = ('none', 'v4', 'v6', 'both');
269 my ($NONE, $DHCP4, $DHCP6, $BOTH) = (0, 1, 2, 3);
270 my $dhcp = $NONE;
271
272 if (defined(my $ip = $d->{ip})) {
273 if ($ip eq 'dhcp') {
274 $dhcp |= $DHCP4;
275 } elsif ($ip ne 'manual') {
4401b7d4 276 $has_ipv4 = 1;
c1d32b55
WB
277 $data .= "Address = $ip\n";
278 }
279 }
280 if (defined(my $gw = $d->{gw})) {
281 $data .= "Gateway = $gw\n";
4401b7d4
WB
282 if ($has_ipv4 && !PVE::Network::is_ip_in_cidr($gw, $d->{ip}, 4)) {
283 $routes .= "\n[Route]\nDestination = $gw/32\nScope = link\n";
284 }
c1d32b55
WB
285 }
286
287 if (defined(my $ip = $d->{ip6})) {
288 if ($ip eq 'dhcp') {
289 $dhcp |= $DHCP6;
290 } elsif ($ip ne 'manual') {
4401b7d4 291 $has_ipv6 = 1;
c1d32b55
WB
292 $data .= "Address = $ip\n";
293 }
294 }
295 if (defined(my $gw = $d->{gw6})) {
296 $data .= "Gateway = $gw\n";
4401b7d4
WB
297 if ($has_ipv6 && !PVE::Network::is_ip_in_cidr($gw, $d->{ip6}, 6)) {
298 $routes .= "\n[Route]\nDestination = $gw/128\nScope = link\n";
299 }
c1d32b55
WB
300 }
301
302 $data .= "DHCP = $DHCPMODES[$dhcp]\n";
4401b7d4 303 $data .= $routes if $routes;
c1d32b55 304
f08b2779 305 $self->ct_file_set_contents($filename, $data);
c1d32b55 306 }
b7cd927f
WB
307}
308
309sub setup_securetty {
310 my ($self, $conf, @add) = @_;
c1d32b55 311
f08b2779
WB
312 my $filename = "/etc/securetty";
313 my $data = $self->ct_file_get_contents($filename);
b7cd927f
WB
314 chomp $data; $data .= "\n";
315 foreach my $dev (@add) {
316 if ($data !~ m!^\Q$dev\E\s*$!m) {
317 $data .= "$dev\n";
318 }
319 }
f08b2779 320 $self->ct_file_set_contents($filename, $data);
c1d32b55
WB
321}
322
168d6b07 323my $replacepw = sub {
f08b2779 324 my ($self, $file, $user, $epw, $shadow) = @_;
168d6b07
DM
325
326 my $tmpfile = "$file.$$";
327
328 eval {
f08b2779 329 my $src = $self->ct_open_file_read($file) ||
168d6b07
DM
330 die "unable to open file '$file' - $!";
331
f08b2779 332 my $st = $self->ct_stat($src) ||
168d6b07
DM
333 die "unable to stat file - $!";
334
f08b2779 335 my $dst = $self->ct_open_file_write($tmpfile) ||
168d6b07
DM
336 die "unable to open file '$tmpfile' - $!";
337
338 # copy owner and permissions
339 chmod $st->mode, $dst;
340 chown $st->uid, $st->gid, $dst;
367a7c18
DM
341
342 my $last_change = int(time()/(60*60*24));
343
344 if ($epw =~ m/^\$TEST\$/) { # for regression tests
345 $last_change = 12345;
346 }
168d6b07
DM
347
348 while (defined (my $line = <$src>)) {
367a7c18
DM
349 if ($shadow) {
350 $line =~ s/^${user}:[^:]*:[^:]*:/${user}:${epw}:${last_change}:/;
351 } else {
352 $line =~ s/^${user}:[^:]*:/${user}:${epw}:/;
353 }
168d6b07
DM
354 print $dst $line;
355 }
356
357 $src->close() || die "close '$file' failed - $!\n";
358 $dst->close() || die "close '$tmpfile' failed - $!\n";
359 };
360 if (my $err = $@) {
f08b2779 361 $self->ct_unlink($tmpfile);
168d6b07 362 } else {
f08b2779
WB
363 $self->ct_rename($tmpfile, $file);
364 $self->ct_unlink($tmpfile); # in case rename fails
168d6b07
DM
365 }
366};
367
368sub set_user_password {
633a7bd8 369 my ($self, $conf, $user, $opt_password) = @_;
168d6b07 370
f08b2779 371 my $pwfile = "/etc/passwd";
168d6b07 372
f08b2779 373 return if !$self->ct_file_exists($pwfile);
168d6b07 374
f08b2779 375 my $shadow = "/etc/shadow";
168d6b07
DM
376
377 if (defined($opt_password)) {
378 if ($opt_password !~ m/^\$/) {
379 my $time = substr (Digest::SHA::sha1_base64 (time), 0, 8);
380 $opt_password = crypt(encode("utf8", $opt_password), "\$1\$$time\$");
381 };
382 } else {
383 $opt_password = '*';
384 }
385
f08b2779
WB
386 if ($self->ct_file_exists($shadow)) {
387 &$replacepw ($self, $shadow, $user, $opt_password, 1);
388 &$replacepw ($self, $pwfile, $user, 'x');
168d6b07 389 } else {
f08b2779 390 &$replacepw ($self, $pwfile, $user, $opt_password);
168d6b07
DM
391 }
392}
393
4727bd09
DM
394my $randomize_crontab = sub {
395 my ($self, $conf) = @_;
396
b5e62cd0
DM
397 my @files;
398 # Note: dir_glob_foreach() untaints filenames!
f08b2779 399 PVE::Tools::dir_glob_foreach("/etc/cron.d", qr/[A-Z\-\_a-z0-9]+/, sub {
b5e62cd0 400 my ($name) = @_;
f08b2779 401 push @files, "/etc/cron.d/$name";
b5e62cd0 402 });
4727bd09 403
f08b2779
WB
404 my $crontab_fn = "/etc/crontab";
405 unshift @files, $crontab_fn if $self->ct_file_exists($crontab_fn);
4727bd09
DM
406
407 foreach my $filename (@files) {
f08b2779 408 my $data = $self->ct_file_get_contents($filename);
4727bd09
DM
409 my $new = '';
410 foreach my $line (split(/\n/, $data)) {
411 # we only randomize minutes for root crontab entries
412 if ($line =~ m/^\d+(\s+\S+\s+\S+\s+\S+\s+\S+\s+root\s+\S.*)$/) {
413 my $rest = $1;
414 my $min = int(rand()*59);
415 $new .= "$min$rest\n";
416 } else {
417 $new .= "$line\n";
418 }
419 }
f08b2779 420 $self->ct_file_set_contents($filename, $new);
4727bd09
DM
421 }
422};
423
d66768a2 424sub pre_start_hook {
633a7bd8 425 my ($self, $conf) = @_;
d66768a2 426
633a7bd8
DM
427 $self->setup_init($conf);
428 $self->setup_network($conf);
429 $self->set_hostname($conf);
430 $self->set_dns($conf);
d66768a2
DM
431
432 # fixme: what else ?
433}
434
435sub post_create_hook {
633a7bd8 436 my ($self, $conf, $root_password) = @_;
d66768a2 437
142444d5 438 $self->template_fixup($conf);
4727bd09
DM
439
440 &$randomize_crontab($self, $conf);
441
633a7bd8
DM
442 $self->set_user_password($conf, 'root', $root_password);
443 $self->setup_init($conf);
444 $self->setup_network($conf);
445 $self->set_hostname($conf);
446 $self->set_dns($conf);
168d6b07 447
55fa4e09 448 # fixme: what else ?
1c7f4f65
DM
449}
450
f08b2779
WB
451# File access wrappers for container setup code.
452# For user-namespace support these might need to take uid and gid maps into account.
453
c6a605f9
WB
454sub ct_reset_ownership {
455 my ($self, @files) = @_;
456 my $conf = $self->{conf};
457 return if !$self->{id_map};
458 my $uid = $self->{rootuid};
459 my $gid = $self->{rootgid};
460 chown($uid, $gid, @files);
461}
462
2063d380
WB
463sub ct_mkdir {
464 my ($self, $file, $mask) = @_;
f08b2779 465 # mkdir goes by parameter count - an `undef' mode acts like a mode of 0000
c6a605f9
WB
466 if (defined($mask)) {
467 return CORE::mkdir($file, $mask) && $self->ct_reset_ownership($file);
468 } else {
469 return CORE::mkdir($file) && $self->ct_reset_ownership($file);
470 }
2063d380
WB
471}
472
473sub ct_unlink {
f08b2779
WB
474 my ($self, @files) = @_;
475 foreach my $file (@files) {
476 CORE::unlink($file);
477 }
478}
479
480sub ct_rename {
481 my ($self, $old, $new) = @_;
482 CORE::rename($old, $new);
2063d380
WB
483}
484
f08b2779 485sub ct_open_file_read {
2063d380 486 my $self = shift;
f08b2779
WB
487 my $file = shift;
488 return IO::File->new($file, O_RDONLY, @_);
2063d380
WB
489}
490
f08b2779 491sub ct_open_file_write {
2063d380 492 my $self = shift;
f08b2779 493 my $file = shift;
c6a605f9
WB
494 my $fh = IO::File->new($file, O_WRONLY | O_CREAT, @_);
495 $self->ct_reset_ownership($fh);
496 return $fh;
2063d380
WB
497}
498
f08b2779 499sub ct_make_path {
2063d380 500 my $self = shift;
c6a605f9
WB
501 if ($self->{id_map}) {
502 my $opts = pop;
503 if (ref($opts) eq 'HASH') {
504 $opts->{owner} = $self->{rootuid} if !defined($self->{owner});
505 $opts->{group} = $self->{rootgid} if !defined($self->{group});
506 }
507 File::Path::make_path(@_, $opts);
508 } else {
509 File::Path::make_path(@_);
510 }
2063d380
WB
511}
512
513sub ct_symlink {
514 my ($self, $old, $new) = @_;
f08b2779 515 return CORE::symlink($old, $new);
2063d380
WB
516}
517
518sub ct_file_exists {
519 my ($self, $file) = @_;
f08b2779
WB
520 return -f $file;
521}
522
523sub ct_is_directory {
524 my ($self, $file) = @_;
525 return -d $file;
526}
527
528sub ct_is_symlink {
529 my ($self, $file) = @_;
530 return -l $file;
531}
532
533sub ct_stat {
534 my ($self, $file) = @_;
535 return File::stat::stat($file);
2063d380
WB
536}
537
538sub ct_file_read_firstline {
539 my ($self, $file) = @_;
f08b2779 540 return PVE::Tools::file_read_firstline($file);
2063d380
WB
541}
542
543sub ct_file_get_contents {
544 my ($self, $file) = @_;
f08b2779 545 return PVE::Tools::file_get_contents($file);
2063d380
WB
546}
547
548sub ct_file_set_contents {
39243220 549 my ($self, $file, $data, $perms) = @_;
c6a605f9
WB
550 PVE::Tools::file_set_contents($file, $data, $perms);
551 $self->ct_reset_ownership($file);
2063d380
WB
552}
553
fa7cb12b
WB
554# Modify a marked portion of a file and move it to the beginning of the file.
555# If the file becomes empty it will be deleted.
556sub ct_modify_file_head_portion {
557 my ($self, $file, $head, $tail, $data) = @_;
558 if ($self->ct_file_exists($file)) {
559 my $old = $self->ct_file_get_contents($file);
560 # remove the portion between $head and $tail (all instances via /g)
561 $old =~ s/(?:^|(?<=\n))\Q$head\E.*\Q$tail\E//gs;
562 chomp $old;
563 if ($old) {
564 # old data existed, append and add the trailing newline
565 if ($data) {
566 $self->ct_file_set_contents($file, $head.$data.$tail . $old."\n");
567 } else {
568 $self->ct_file_set_contents($file, $old."\n");
569 }
570 } elsif ($data) {
571 # only our own data will be added
572 $self->ct_file_set_contents($file, $head.$data.$tail);
573 } else {
574 # empty => delete
575 $self->ct_unlink($file);
576 }
577 } else {
578 $self->ct_file_set_contents($file, $head.$data.$tail);
579 }
580}
581
1c7f4f65 5821;