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