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