]> git.proxmox.com Git - pve-container.git/blob - src/PVE/LXC/Setup/Base.pm
cleanup directory structure
[pve-container.git] / src / PVE / LXC / Setup / Base.pm
1 package PVE::LXC::Setup::Base;
2
3 use strict;
4 use warnings;
5
6 use File::stat;
7 use Digest::SHA;
8 use IO::File;
9 use Encode;
10 use File::Path;
11
12 use PVE::INotify;
13 use PVE::Tools;
14
15 sub new {
16 my ($class, $conf, $rootdir) = @_;
17
18 return bless { conf => $conf, rootdir => $rootdir }, $class;
19 }
20
21 sub lookup_dns_conf {
22 my ($conf) = @_;
23
24 my $nameserver = $conf->{nameserver};
25 my $searchdomains = $conf->{searchdomain};
26
27 if (!($nameserver && $searchdomains)) {
28
29 if ($conf->{'testmode'}) {
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);
51 }
52
53 sub update_etc_hosts {
54 my ($etc_hosts_data, $hostip, $oldname, $newname, $searchdomains) = @_;
55
56 my $done = 0;
57
58 my @lines;
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 }
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 }
77
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)) {
91 push @lines, "$hostip $extra_names $newname";
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)) {
105 push @lines, "$hostip $extra_names $newname";
106 } else {
107 push @lines, "127.0.1.1 $newname";
108 }
109 }
110
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
123 $etc_hosts_data = join("\n", @lines) . "\n";
124
125 return $etc_hosts_data;
126 }
127
128 sub template_fixup {
129 my ($self, $conf) = @_;
130
131 # do nothing by default
132 }
133
134 sub set_dns {
135 my ($self, $conf) = @_;
136
137 my ($searchdomains, $nameserver) = lookup_dns_conf($conf);
138
139 my $rootdir = $self->{rootdir};
140
141 my $filename = "$rootdir/etc/resolv.conf";
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
155 sub set_hostname {
156 my ($self, $conf) = @_;
157
158 my $hostname = $conf->{hostname} || 'localhost';
159
160 $hostname =~ s/\..*$//;
161
162 my $rootdir = $self->{rootdir};
163
164 my $hostname_fn = "$rootdir/etc/hostname";
165
166 my $oldname = PVE::Tools::file_read_firstline($hostname_fn) || 'localhost';
167
168 my $hosts_fn = "$rootdir/etc/hosts";
169 my $etc_hosts_data = '';
170
171 if (-f $hosts_fn) {
172 $etc_hosts_data = PVE::Tools::file_get_contents($hosts_fn);
173 }
174
175 my ($ipv4, $ipv6) = PVE::LXC::get_primary_ips($conf);
176 my $hostip = $ipv4 || $ipv6;
177
178 my ($searchdomains) = lookup_dns_conf($conf);
179
180 $etc_hosts_data = update_etc_hosts($etc_hosts_data, $hostip, $oldname,
181 $hostname, $searchdomains);
182
183 PVE::Tools::file_set_contents($hostname_fn, "$hostname\n");
184 PVE::Tools::file_set_contents($hosts_fn, $etc_hosts_data);
185 }
186
187 sub setup_network {
188 my ($self, $conf) = @_;
189
190 die "please implement this inside subclass"
191 }
192
193 sub setup_init {
194 my ($self, $conf) = @_;
195
196 die "please implement this inside subclass"
197 }
198
199 sub 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
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 }
233 }
234
235 my $ttycount = PVE::LXC::get_tty_count($conf);
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
250 sub 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]
264 Name = $d->{name}
265
266 [Network]
267 Description = Interface $d->{name} autoconfigured by PVE
268 DATA
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 }
300 }
301
302 sub setup_securetty {
303 my ($self, $conf, @add) = @_;
304
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);
315 }
316
317 my $replacepw = sub {
318 my ($file, $user, $epw, $shadow) = @_;
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;
335
336 my $last_change = int(time()/(60*60*24));
337
338 if ($epw =~ m/^\$TEST\$/) { # for regression tests
339 $last_change = 12345;
340 }
341
342 while (defined (my $line = <$src>)) {
343 if ($shadow) {
344 $line =~ s/^${user}:[^:]*:[^:]*:/${user}:${epw}:${last_change}:/;
345 } else {
346 $line =~ s/^${user}:[^:]*:/${user}:${epw}:/;
347 }
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
362 sub set_user_password {
363 my ($self, $conf, $user, $opt_password) = @_;
364
365 my $rootdir = $self->{rootdir};
366
367 my $pwfile = "$rootdir/etc/passwd";
368
369 return if ! -f $pwfile;
370
371 my $shadow = "$rootdir/etc/shadow";
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) {
383 &$replacepw ($shadow, $user, $opt_password, 1);
384 &$replacepw ($pwfile, $user, 'x');
385 } else {
386 &$replacepw ($pwfile, $user, $opt_password);
387 }
388 }
389
390 my $randomize_crontab = sub {
391 my ($self, $conf) = @_;
392
393 my $rootdir = $self->{rootdir};
394
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 });
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
423 sub 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
440 my $hostname = $conf->{hostname} || 'localhost';
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
454 sub pre_start_hook {
455 my ($self, $conf) = @_;
456
457 $self->setup_init($conf);
458 $self->setup_network($conf);
459 $self->set_hostname($conf);
460 $self->set_dns($conf);
461
462 # fixme: what else ?
463 }
464
465 sub post_create_hook {
466 my ($self, $conf, $root_password) = @_;
467
468 $self->template_fixup($conf);
469
470 &$randomize_crontab($self, $conf);
471
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);
477 $self->rewrite_ssh_host_keys($conf);
478
479 # fixme: what else ?
480 }
481
482 sub 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
490 sub 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
497 sub ct_open_file {
498 my $self = shift;
499 my $file = $self->{rootdir} . '/' . shift;
500 return IO::File->new($file, @_);
501 }
502
503 sub 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
511 sub 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
523 sub ct_symlink {
524 my ($self, $old, $new) = @_;
525 my $root = $self->{rootdir};
526 return CORE::symlink($old, "$root/$new");
527 }
528
529 sub ct_file_exists {
530 my ($self, $file) = @_;
531 my $root = $self->{rootdir};
532 return -f "$root/$file";
533 }
534
535 sub ct_file_read_firstline {
536 my ($self, $file) = @_;
537 my $root = $self->{rootdir};
538 return PVE::Tools::file_read_firstline("$root/$file");
539 }
540
541 sub ct_file_get_contents {
542 my ($self, $file) = @_;
543 my $root = $self->{rootdir};
544 return PVE::Tools::file_get_contents("$root/$file");
545 }
546
547 sub 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
553 1;