]> git.proxmox.com Git - pve-container.git/blob - src/PVE/LXC/Setup/Base.pm
OpenSUSE support added
[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 Fcntl;
11 use File::Path;
12 use File::Spec;
13
14 use PVE::INotify;
15 use PVE::Tools;
16 use PVE::Network;
17
18 sub new {
19 my ($class, $conf, $rootdir) = @_;
20
21 return bless { conf => $conf, rootdir => $rootdir }, $class;
22 }
23
24 sub lookup_dns_conf {
25 my ($self, $conf) = @_;
26
27 my $nameserver = $conf->{nameserver};
28 my $searchdomains = $conf->{searchdomain};
29
30 if (!($nameserver && $searchdomains)) {
31
32 if ($conf->{'testmode'}) {
33
34 $nameserver = "8.8.8.8 8.8.8.9";
35 $searchdomains = "proxmox.com";
36
37 } else {
38
39 my $host_resolv_conf = $self->{host_resolv_conf};
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);
54 }
55
56 sub update_etc_hosts {
57 my ($etc_hosts_data, $hostip, $oldname, $newname, $searchdomains) = @_;
58
59 my $done = 0;
60
61 my @lines;
62
63 my $namepart = ($newname =~ s/\..*$//r);
64
65 my $extra_names = '';
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 }
73 }
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 }
86
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)) {
100 push @lines, "$hostip $extra_names $newname";
101 } else {
102 push @lines, "127.0.1.1 $namepart";
103 }
104 $done = 1;
105 }
106 next;
107 } else {
108 push @lines, $line;
109 }
110 }
111
112 if (!$done) {
113 if (defined($hostip)) {
114 push @lines, "$hostip $extra_names $newname";
115 } else {
116 push @lines, "127.0.1.1 $namepart";
117 }
118 }
119
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
132 $etc_hosts_data = join("\n", @lines) . "\n";
133
134 return $etc_hosts_data;
135 }
136
137 sub template_fixup {
138 my ($self, $conf) = @_;
139
140 # do nothing by default
141 }
142
143 sub set_dns {
144 my ($self, $conf) = @_;
145
146 my ($searchdomains, $nameserver) = $self->lookup_dns_conf($conf);
147
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
157 $self->ct_file_set_contents("/etc/resolv.conf", $data);
158 }
159
160 sub set_hostname {
161 my ($self, $conf) = @_;
162
163 my $hostname = $conf->{hostname} || 'localhost';
164
165 my $namepart = ($hostname =~ s/\..*$//r);
166
167 my $hostname_fn = "/etc/hostname";
168
169 my $oldname = $self->ct_file_read_firstline($hostname_fn) || 'localhost';
170
171 my $hosts_fn = "/etc/hosts";
172 my $etc_hosts_data = '';
173
174 if ($self->ct_file_exists($hosts_fn)) {
175 $etc_hosts_data = $self->ct_file_get_contents($hosts_fn);
176 }
177
178 my ($ipv4, $ipv6) = PVE::LXC::get_primary_ips($conf);
179 my $hostip = $ipv4 || $ipv6;
180
181 my ($searchdomains) = $self->lookup_dns_conf($conf);
182
183 $etc_hosts_data = update_etc_hosts($etc_hosts_data, $hostip, $oldname,
184 $hostname, $searchdomains);
185
186 $self->ct_file_set_contents($hostname_fn, "$namepart\n");
187 $self->ct_file_set_contents($hosts_fn, $etc_hosts_data);
188 }
189
190 sub setup_network {
191 my ($self, $conf) = @_;
192
193 die "please implement this inside subclass"
194 }
195
196 sub setup_init {
197 my ($self, $conf) = @_;
198
199 die "please implement this inside subclass"
200 }
201
202 sub setup_systemd_console {
203 my ($self, $conf) = @_;
204
205 my $systemd_dir_rel = -x "/lib/systemd/systemd" ?
206 "/lib/systemd/system" : "/usr/lib/systemd/system";
207
208 my $systemd_getty_service_rel = "$systemd_dir_rel/getty\@.service";
209
210 return if !$self->ct_file_exists($systemd_getty_service_rel);
211
212 my $raw = $self->ct_file_get_contents($systemd_getty_service_rel);
213
214 my $systemd_container_getty_service_rel = "$systemd_dir_rel/container-getty\@.service";
215
216 # systemd on CenoOS 7.1 is too old (version 205), so there is no
217 # container-getty service
218 if (!$self->ct_file_exists($systemd_container_getty_service_rel)) {
219 if ($raw =~ s!^ConditionPathExists=/dev/tty0$!ConditionPathExists=/dev/tty!m) {
220 $self->ct_file_set_contents($systemd_getty_service_rel, $raw);
221 }
222 } else {
223 # undo above change (in case someone updated systemd)
224 if ($raw =~ s!^ConditionPathExists=/dev/tty$!ConditionPathExists=/dev/tty0!m) {
225 $self->ct_file_set_contents($systemd_getty_service_rel, $raw);
226 }
227 }
228
229 my $ttycount = PVE::LXC::get_tty_count($conf);
230
231 for (my $i = 1; $i < 7; $i++) {
232 my $tty_service_lnk = "/etc/systemd/system/getty.target.wants/getty\@tty$i.service";
233 if ($i > $ttycount) {
234 $self->ct_unlink($tty_service_lnk);
235 } else {
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);
239 }
240 }
241 }
242 }
243
244 sub setup_systemd_networkd {
245 my ($self, $conf) = @_;
246
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
252 my $filename = "/etc/systemd/network/$d->{name}.network";
253
254 my $data = <<"DATA";
255 [Match]
256 Name = $d->{name}
257
258 [Network]
259 Description = Interface $d->{name} autoconfigured by PVE
260 DATA
261
262 my $routes = '';
263 my ($has_ipv4, $has_ipv6);
264
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') {
274 $has_ipv4 = 1;
275 $data .= "Address = $ip\n";
276 }
277 }
278 if (defined(my $gw = $d->{gw})) {
279 $data .= "Gateway = $gw\n";
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 }
283 }
284
285 if (defined(my $ip = $d->{ip6})) {
286 if ($ip eq 'dhcp') {
287 $dhcp |= $DHCP6;
288 } elsif ($ip ne 'manual') {
289 $has_ipv6 = 1;
290 $data .= "Address = $ip\n";
291 }
292 }
293 if (defined(my $gw = $d->{gw6})) {
294 $data .= "Gateway = $gw\n";
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 }
298 }
299
300 $data .= "DHCP = $DHCPMODES[$dhcp]\n";
301 $data .= $routes if $routes;
302
303 $self->ct_file_set_contents($filename, $data);
304 }
305 }
306
307 sub setup_securetty {
308 my ($self, $conf, @add) = @_;
309
310 my $filename = "/etc/securetty";
311 my $data = $self->ct_file_get_contents($filename);
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 }
318 $self->ct_file_set_contents($filename, $data);
319 }
320
321 my $replacepw = sub {
322 my ($self, $file, $user, $epw, $shadow) = @_;
323
324 my $tmpfile = "$file.$$";
325
326 eval {
327 my $src = $self->ct_open_file_read($file) ||
328 die "unable to open file '$file' - $!";
329
330 my $st = $self->ct_stat($src) ||
331 die "unable to stat file - $!";
332
333 my $dst = $self->ct_open_file_write($tmpfile) ||
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;
339
340 my $last_change = int(time()/(60*60*24));
341
342 if ($epw =~ m/^\$TEST\$/) { # for regression tests
343 $last_change = 12345;
344 }
345
346 while (defined (my $line = <$src>)) {
347 if ($shadow) {
348 $line =~ s/^${user}:[^:]*:[^:]*:/${user}:${epw}:${last_change}:/;
349 } else {
350 $line =~ s/^${user}:[^:]*:/${user}:${epw}:/;
351 }
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 = $@) {
359 $self->ct_unlink($tmpfile);
360 } else {
361 $self->ct_rename($tmpfile, $file);
362 $self->ct_unlink($tmpfile); # in case rename fails
363 }
364 };
365
366 sub set_user_password {
367 my ($self, $conf, $user, $opt_password) = @_;
368
369 my $pwfile = "/etc/passwd";
370
371 return if !$self->ct_file_exists($pwfile);
372
373 my $shadow = "/etc/shadow";
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
384 if ($self->ct_file_exists($shadow)) {
385 &$replacepw ($self, $shadow, $user, $opt_password, 1);
386 &$replacepw ($self, $pwfile, $user, 'x');
387 } else {
388 &$replacepw ($self, $pwfile, $user, $opt_password);
389 }
390 }
391
392 my $randomize_crontab = sub {
393 my ($self, $conf) = @_;
394
395 my @files;
396 # Note: dir_glob_foreach() untaints filenames!
397 PVE::Tools::dir_glob_foreach("/etc/cron.d", qr/[A-Z\-\_a-z0-9]+/, sub {
398 my ($name) = @_;
399 push @files, "/etc/cron.d/$name";
400 });
401
402 my $crontab_fn = "/etc/crontab";
403 unshift @files, $crontab_fn if $self->ct_file_exists($crontab_fn);
404
405 foreach my $filename (@files) {
406 my $data = $self->ct_file_get_contents($filename);
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 }
418 $self->ct_file_set_contents($filename, $new);
419 }
420 };
421
422 sub pre_start_hook {
423 my ($self, $conf) = @_;
424
425 $self->setup_init($conf);
426 $self->setup_network($conf);
427 $self->set_hostname($conf);
428 $self->set_dns($conf);
429
430 # fixme: what else ?
431 }
432
433 sub post_create_hook {
434 my ($self, $conf, $root_password) = @_;
435
436 $self->template_fixup($conf);
437
438 &$randomize_crontab($self, $conf);
439
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);
445
446 # fixme: what else ?
447 }
448
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
452 sub 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
461 sub ct_mkdir {
462 my ($self, $file, $mask) = @_;
463 # mkdir goes by parameter count - an `undef' mode acts like a mode of 0000
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 }
469 }
470
471 sub ct_unlink {
472 my ($self, @files) = @_;
473 foreach my $file (@files) {
474 CORE::unlink($file);
475 }
476 }
477
478 sub ct_rename {
479 my ($self, $old, $new) = @_;
480 CORE::rename($old, $new);
481 }
482
483 sub ct_open_file_read {
484 my $self = shift;
485 my $file = shift;
486 return IO::File->new($file, O_RDONLY, @_);
487 }
488
489 sub ct_open_file_write {
490 my $self = shift;
491 my $file = shift;
492 my $fh = IO::File->new($file, O_WRONLY | O_CREAT, @_);
493 $self->ct_reset_ownership($fh);
494 return $fh;
495 }
496
497 sub ct_make_path {
498 my $self = shift;
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 }
509 }
510
511 sub ct_symlink {
512 my ($self, $old, $new) = @_;
513 return CORE::symlink($old, $new);
514 }
515
516 sub ct_file_exists {
517 my ($self, $file) = @_;
518 return -f $file;
519 }
520
521 sub ct_is_directory {
522 my ($self, $file) = @_;
523 return -d $file;
524 }
525
526 sub ct_is_symlink {
527 my ($self, $file) = @_;
528 return -l $file;
529 }
530
531 sub ct_stat {
532 my ($self, $file) = @_;
533 return File::stat::stat($file);
534 }
535
536 sub ct_file_read_firstline {
537 my ($self, $file) = @_;
538 return PVE::Tools::file_read_firstline($file);
539 }
540
541 sub ct_file_get_contents {
542 my ($self, $file) = @_;
543 return PVE::Tools::file_get_contents($file);
544 }
545
546 sub ct_file_set_contents {
547 my ($self, $file, $data, $perms) = @_;
548 PVE::Tools::file_set_contents($file, $data, $perms);
549 $self->ct_reset_ownership($file);
550 }
551
552 # Modify a marked portion of a file and move it to the beginning of the file.
553 # If the file becomes empty it will be deleted.
554 sub ct_modify_file_head_portion {
555 my ($self, $file, $head, $tail, $data) = @_;
556 if ($self->ct_file_exists($file)) {
557 my $old = $self->ct_file_get_contents($file);
558 # remove the portion between $head and $tail (all instances via /g)
559 $old =~ s/(?:^|(?<=\n))\Q$head\E.*\Q$tail\E//gs;
560 chomp $old;
561 if ($old) {
562 # old data existed, append and add the trailing newline
563 if ($data) {
564 $self->ct_file_set_contents($file, $head.$data.$tail . $old."\n");
565 } else {
566 $self->ct_file_set_contents($file, $old."\n");
567 }
568 } elsif ($data) {
569 # only our own data will be added
570 $self->ct_file_set_contents($file, $head.$data.$tail);
571 } else {
572 # empty => delete
573 $self->ct_unlink($file);
574 }
575 } else {
576 $self->ct_file_set_contents($file, $head.$data.$tail);
577 }
578 }
579
580 1;