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