1 package PVE
::LXC
::Setup
::Base
;
19 my ($class, $conf, $rootdir) = @_;
21 return bless { conf
=> $conf, rootdir
=> $rootdir }, $class;
25 my ($self, $conf) = @_;
27 my $nameserver = $conf->{nameserver
};
28 my $searchdomains = $conf->{searchdomain
};
30 if (!($nameserver && $searchdomains)) {
32 if ($conf->{'testmode'}) {
34 $nameserver = "8.8.8.8 8.8.8.9";
35 $searchdomains = "proxmox.com";
39 my $host_resolv_conf = $self->{host_resolv_conf
};
41 $searchdomains = $host_resolv_conf->{search
};
44 foreach my $k ("dns1", "dns2", "dns3") {
45 if (my $ns = $host_resolv_conf->{$k}) {
49 $nameserver = join(' ', @list);
53 return ($searchdomains, $nameserver);
56 sub update_etc_hosts
{
57 my ($etc_hosts_data, $hostip, $oldname, $newname, $searchdomains) = @_;
63 my $namepart = ($newname =~ s/\..*$//r);
66 if ($newname =~ /\./) {
67 $all_names .= "$newname $namepart";
69 foreach my $domain (PVE
::Tools
::split_list
($searchdomains)) {
70 $all_names .= ' ' if $all_names;
71 $all_names .= "$newname.$domain";
73 $all_names .= ' ' if $all_names;
74 $all_names .= $newname;
77 foreach my $line (split(/\n/, $etc_hosts_data)) {
78 if ($line =~ m/^#/ || $line =~ m/^\s*$/) {
83 my ($ip, @names) = split(/\s+/, $line);
84 if (($ip eq '127.0.0.1') || ($ip eq '::1')) {
90 foreach my $name (@names) {
91 if ($name eq $oldname || $name eq $newname) {
94 # fixme: record extra names?
97 $found = 1 if defined($hostip) && ($ip eq $hostip);
101 if (defined($hostip)) {
102 push @lines, "$hostip $all_names";
104 push @lines, "127.0.1.1 $namepart";
115 if (defined($hostip)) {
116 push @lines, "$hostip $all_names";
118 push @lines, "127.0.1.1 $namepart";
122 my $found_localhost = 0;
123 foreach my $line (@lines) {
124 if ($line =~ m/^127.0.0.1\s/) {
125 $found_localhost = 1;
130 if (!$found_localhost) {
131 unshift @lines, "127.0.0.1 localhost.localnet localhost";
134 $etc_hosts_data = join("\n", @lines) . "\n";
136 return $etc_hosts_data;
140 my ($self, $conf) = @_;
142 # do nothing by default
146 my ($self, $conf) = @_;
148 my ($searchdomains, $nameserver) = $self->lookup_dns_conf($conf);
152 $data .= "search " . join(' ', PVE
::Tools
::split_list
($searchdomains)) . "\n"
155 foreach my $ns ( PVE
::Tools
::split_list
($nameserver)) {
156 $data .= "nameserver $ns\n";
159 $self->ct_file_set_contents("/etc/resolv.conf", $data);
163 my ($self, $conf) = @_;
165 my $hostname = $conf->{hostname
} || 'localhost';
167 my $namepart = ($hostname =~ s/\..*$//r);
169 my $hostname_fn = "/etc/hostname";
171 my $oldname = $self->ct_file_read_firstline($hostname_fn) || 'localhost';
173 my $hosts_fn = "/etc/hosts";
174 my $etc_hosts_data = '';
176 if ($self->ct_file_exists($hosts_fn)) {
177 $etc_hosts_data = $self->ct_file_get_contents($hosts_fn);
180 my ($ipv4, $ipv6) = PVE
::LXC
::get_primary_ips
($conf);
181 my $hostip = $ipv4 || $ipv6;
183 my ($searchdomains) = $self->lookup_dns_conf($conf);
185 $etc_hosts_data = update_etc_hosts
($etc_hosts_data, $hostip, $oldname,
186 $hostname, $searchdomains);
188 $self->ct_file_set_contents($hostname_fn, "$namepart\n");
189 $self->ct_file_set_contents($hosts_fn, $etc_hosts_data);
193 my ($self, $conf) = @_;
195 die "please implement this inside subclass"
199 my ($self, $conf) = @_;
201 die "please implement this inside subclass"
204 sub setup_systemd_console
{
205 my ($self, $conf) = @_;
207 my $systemd_dir_rel = -x
"/lib/systemd/systemd" ?
208 "/lib/systemd/system" : "/usr/lib/systemd/system";
210 my $systemd_getty_service_rel = "$systemd_dir_rel/getty\@.service";
212 return if !$self->ct_file_exists($systemd_getty_service_rel);
214 my $raw = $self->ct_file_get_contents($systemd_getty_service_rel);
216 my $systemd_container_getty_service_rel = "$systemd_dir_rel/container-getty\@.service";
218 # systemd on CenoOS 7.1 is too old (version 205), so there is no
219 # container-getty service
220 if (!$self->ct_file_exists($systemd_container_getty_service_rel)) {
221 if ($raw =~ s!^ConditionPathExists=/dev/tty0$!ConditionPathExists=/dev/tty!m) {
222 $self->ct_file_set_contents($systemd_getty_service_rel, $raw);
225 # undo above change (in case someone updated systemd)
226 if ($raw =~ s!^ConditionPathExists=/dev/tty$!ConditionPathExists=/dev/tty0!m) {
227 $self->ct_file_set_contents($systemd_getty_service_rel, $raw);
231 my $ttycount = PVE
::LXC
::get_tty_count
($conf);
233 for (my $i = 1; $i < 7; $i++) {
234 my $tty_service_lnk = "/etc/systemd/system/getty.target.wants/getty\@tty$i.service";
235 if ($i > $ttycount) {
236 $self->ct_unlink($tty_service_lnk);
238 if (!$self->ct_is_symlink($tty_service_lnk)) {
239 $self->ct_unlink($tty_service_lnk);
240 $self->ct_symlink($systemd_getty_service_rel, $tty_service_lnk);
246 sub setup_container_getty_service
{
248 my $servicefile = '/usr/lib/systemd/system/container-getty@.service';
249 my $raw = $self->ct_file_get_contents($servicefile);
250 if ($raw =~ s
@pts/%I@lxc/tty%I@g) {
251 $self->ct_file_set_contents($servicefile, $raw);
255 sub setup_systemd_networkd
{
256 my ($self, $conf) = @_;
258 foreach my $k (keys %$conf) {
259 next if $k !~ m/^net(\d+)$/;
260 my $d = PVE
::LXC
::parse_lxc_network
($conf->{$k});
263 my $filename = "/etc/systemd/network/$d->{name}.network";
270 Description = Interface $d->{name} autoconfigured by PVE
274 my ($has_ipv4, $has_ipv6);
277 my @DHCPMODES = ('none', 'v4', 'v6', 'both');
278 my ($NONE, $DHCP4, $DHCP6, $BOTH) = (0, 1, 2, 3);
281 if (defined(my $ip = $d->{ip
})) {
284 } elsif ($ip ne 'manual') {
286 $data .= "Address = $ip\n";
289 if (defined(my $gw = $d->{gw
})) {
290 $data .= "Gateway = $gw\n";
291 if ($has_ipv4 && !PVE
::Network
::is_ip_in_cidr
($gw, $d->{ip
}, 4)) {
292 $routes .= "\n[Route]\nDestination = $gw/32\nScope = link\n";
296 if (defined(my $ip = $d->{ip6
})) {
299 } elsif ($ip ne 'manual') {
301 $data .= "Address = $ip\n";
304 if (defined(my $gw = $d->{gw6
})) {
305 $data .= "Gateway = $gw\n";
306 if ($has_ipv6 && !PVE
::Network
::is_ip_in_cidr
($gw, $d->{ip6
}, 6)) {
307 $routes .= "\n[Route]\nDestination = $gw/128\nScope = link\n";
311 $data .= "DHCP = $DHCPMODES[$dhcp]\n";
312 $data .= $routes if $routes;
314 $self->ct_file_set_contents($filename, $data);
318 sub setup_securetty
{
319 my ($self, $conf, @add) = @_;
321 my $filename = "/etc/securetty";
322 my $data = $self->ct_file_get_contents($filename);
323 chomp $data; $data .= "\n";
324 foreach my $dev (@add) {
325 if ($data !~ m!^\Q$dev\E\s*$!m) {
329 $self->ct_file_set_contents($filename, $data);
332 my $replacepw = sub {
333 my ($self, $file, $user, $epw, $shadow) = @_;
335 my $tmpfile = "$file.$$";
338 my $src = $self->ct_open_file_read($file) ||
339 die "unable to open file '$file' - $!";
341 my $st = $self->ct_stat($src) ||
342 die "unable to stat file - $!";
344 my $dst = $self->ct_open_file_write($tmpfile) ||
345 die "unable to open file '$tmpfile' - $!";
347 # copy owner and permissions
348 chmod $st->mode, $dst;
349 chown $st->uid, $st->gid, $dst;
351 my $last_change = int(time()/(60*60*24));
353 if ($epw =~ m/^\$TEST\$/) { # for regression tests
354 $last_change = 12345;
357 while (defined (my $line = <$src>)) {
359 $line =~ s/^${user}:[^:]*:[^:]*:/${user}:${epw}:${last_change}:/;
361 $line =~ s/^${user}:[^:]*:/${user}:${epw}:/;
366 $src->close() || die "close '$file' failed - $!\n";
367 $dst->close() || die "close '$tmpfile' failed - $!\n";
370 $self->ct_unlink($tmpfile);
372 $self->ct_rename($tmpfile, $file);
373 $self->ct_unlink($tmpfile); # in case rename fails
377 sub set_user_password
{
378 my ($self, $conf, $user, $opt_password) = @_;
380 my $pwfile = "/etc/passwd";
382 return if !$self->ct_file_exists($pwfile);
384 my $shadow = "/etc/shadow";
386 if (defined($opt_password)) {
387 if ($opt_password !~ m/^\$/) {
388 my $time = substr (Digest
::SHA
::sha1_base64
(time), 0, 8);
389 $opt_password = crypt(encode
("utf8", $opt_password), "\$1\$$time\$");
395 if ($self->ct_file_exists($shadow)) {
396 &$replacepw ($self, $shadow, $user, $opt_password, 1);
397 &$replacepw ($self, $pwfile, $user, 'x');
399 &$replacepw ($self, $pwfile, $user, $opt_password);
403 my $randomize_crontab = sub {
404 my ($self, $conf) = @_;
407 # Note: dir_glob_foreach() untaints filenames!
408 PVE
::Tools
::dir_glob_foreach
("/etc/cron.d", qr/[A-Z\-\_a-z0-9]+/, sub {
410 push @files, "/etc/cron.d/$name";
413 my $crontab_fn = "/etc/crontab";
414 unshift @files, $crontab_fn if $self->ct_file_exists($crontab_fn);
416 foreach my $filename (@files) {
417 my $data = $self->ct_file_get_contents($filename);
419 foreach my $line (split(/\n/, $data)) {
420 # we only randomize minutes for root crontab entries
421 if ($line =~ m/^\d+(\s+\S+\s+\S+\s+\S+\s+\S+\s+root\s+\S.*)$/) {
423 my $min = int(rand()*59);
424 $new .= "$min$rest\n";
429 $self->ct_file_set_contents($filename, $new);
434 my ($self, $conf) = @_;
436 $self->setup_init($conf);
437 $self->setup_network($conf);
438 $self->set_hostname($conf);
439 $self->set_dns($conf);
444 sub post_create_hook
{
445 my ($self, $conf, $root_password) = @_;
447 $self->template_fixup($conf);
449 &$randomize_crontab($self, $conf);
451 $self->set_user_password($conf, 'root', $root_password);
452 $self->setup_init($conf);
453 $self->setup_network($conf);
454 $self->set_hostname($conf);
455 $self->set_dns($conf);
460 # File access wrappers for container setup code.
461 # For user-namespace support these might need to take uid and gid maps into account.
463 sub ct_reset_ownership
{
464 my ($self, @files) = @_;
465 my $conf = $self->{conf
};
466 return if !$self->{id_map
};
467 my $uid = $self->{rootuid
};
468 my $gid = $self->{rootgid
};
469 chown($uid, $gid, @files);
473 my ($self, $file, $mask) = @_;
474 # mkdir goes by parameter count - an `undef' mode acts like a mode of 0000
475 if (defined($mask)) {
476 return CORE
::mkdir($file, $mask) && $self->ct_reset_ownership($file);
478 return CORE
::mkdir($file) && $self->ct_reset_ownership($file);
483 my ($self, @files) = @_;
484 foreach my $file (@files) {
490 my ($self, $old, $new) = @_;
491 CORE
::rename($old, $new);
494 sub ct_open_file_read
{
497 return IO
::File-
>new($file, O_RDONLY
, @_);
500 sub ct_open_file_write
{
503 my $fh = IO
::File-
>new($file, O_WRONLY
| O_CREAT
, @_);
504 $self->ct_reset_ownership($fh);
510 if ($self->{id_map
}) {
512 if (ref($opts) eq 'HASH') {
513 $opts->{owner
} = $self->{rootuid
} if !defined($self->{owner
});
514 $opts->{group
} = $self->{rootgid
} if !defined($self->{group
});
516 File
::Path
::make_path
(@_, $opts);
518 File
::Path
::make_path
(@_);
523 my ($self, $old, $new) = @_;
524 return CORE
::symlink($old, $new);
528 my ($self, $file) = @_;
532 sub ct_is_directory
{
533 my ($self, $file) = @_;
538 my ($self, $file) = @_;
543 my ($self, $file) = @_;
544 return File
::stat::stat($file);
547 sub ct_file_read_firstline
{
548 my ($self, $file) = @_;
549 return PVE
::Tools
::file_read_firstline
($file);
552 sub ct_file_get_contents
{
553 my ($self, $file) = @_;
554 return PVE
::Tools
::file_get_contents
($file);
557 sub ct_file_set_contents
{
558 my ($self, $file, $data, $perms) = @_;
559 PVE
::Tools
::file_set_contents
($file, $data, $perms);
560 $self->ct_reset_ownership($file);
563 # Modify a marked portion of a file and move it to the beginning of the file.
564 # If the file becomes empty it will be deleted.
565 sub ct_modify_file_head_portion
{
566 my ($self, $file, $head, $tail, $data) = @_;
567 if ($self->ct_file_exists($file)) {
568 my $old = $self->ct_file_get_contents($file);
569 # remove the portion between $head and $tail (all instances via /g)
570 $old =~ s/(?:^|(?<=\n))\Q$head\E.*\Q$tail\E//gs;
573 # old data existed, append and add the trailing newline
575 $self->ct_file_set_contents($file, $head.$data.$tail . $old."\n");
577 $self->ct_file_set_contents($file, $old."\n");
580 # only our own data will be added
581 $self->ct_file_set_contents($file, $head.$data.$tail);
584 $self->ct_unlink($file);
587 $self->ct_file_set_contents($file, $head.$data.$tail);