]> git.proxmox.com Git - pve-container.git/blob - src/PVE/LXC/Setup/Base.pm
Create: safer rewrite_ssh_host_keys
[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 $extra_names = '';
64 foreach my $domain (PVE::Tools::split_list($searchdomains)) {
65 $extra_names .= ' ' if $extra_names;
66 $extra_names .= "$newname.$domain";
67 }
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 }
80
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)) {
94 push @lines, "$hostip $extra_names $newname";
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)) {
108 push @lines, "$hostip $extra_names $newname";
109 } else {
110 push @lines, "127.0.1.1 $newname";
111 }
112 }
113
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
126 $etc_hosts_data = join("\n", @lines) . "\n";
127
128 return $etc_hosts_data;
129 }
130
131 sub template_fixup {
132 my ($self, $conf) = @_;
133
134 # do nothing by default
135 }
136
137 sub set_dns {
138 my ($self, $conf) = @_;
139
140 my ($searchdomains, $nameserver) = $self->lookup_dns_conf($conf);
141
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
151 $self->ct_file_set_contents("/etc/resolv.conf", $data);
152 }
153
154 sub set_hostname {
155 my ($self, $conf) = @_;
156
157 my $hostname = $conf->{hostname} || 'localhost';
158
159 $hostname =~ s/\..*$//;
160
161 my $hostname_fn = "/etc/hostname";
162
163 my $oldname = $self->ct_file_read_firstline($hostname_fn) || 'localhost';
164
165 my $hosts_fn = "/etc/hosts";
166 my $etc_hosts_data = '';
167
168 if ($self->ct_file_exists($hosts_fn)) {
169 $etc_hosts_data = $self->ct_file_get_contents($hosts_fn);
170 }
171
172 my ($ipv4, $ipv6) = PVE::LXC::get_primary_ips($conf);
173 my $hostip = $ipv4 || $ipv6;
174
175 my ($searchdomains) = $self->lookup_dns_conf($conf);
176
177 $etc_hosts_data = update_etc_hosts($etc_hosts_data, $hostip, $oldname,
178 $hostname, $searchdomains);
179
180 $self->ct_file_set_contents($hostname_fn, "$hostname\n");
181 $self->ct_file_set_contents($hosts_fn, $etc_hosts_data);
182 }
183
184 sub setup_network {
185 my ($self, $conf) = @_;
186
187 die "please implement this inside subclass"
188 }
189
190 sub setup_init {
191 my ($self, $conf) = @_;
192
193 die "please implement this inside subclass"
194 }
195
196 sub setup_systemd_console {
197 my ($self, $conf) = @_;
198
199 my $systemd_dir_rel = -x "/lib/systemd/systemd" ?
200 "/lib/systemd/system" : "/usr/lib/systemd/system";
201
202 my $systemd_getty_service_rel = "$systemd_dir_rel/getty\@.service";
203
204 return if !$self->ct_file_exists($systemd_getty_service_rel);
205
206 my $raw = $self->ct_file_get_contents($systemd_getty_service_rel);
207
208 my $systemd_container_getty_service_rel = "$systemd_dir_rel/container-getty\@.service";
209
210 # systemd on CenoOS 7.1 is too old (version 205), so there is no
211 # container-getty service
212 if (!$self->ct_file_exists($systemd_container_getty_service_rel)) {
213 if ($raw =~ s!^ConditionPathExists=/dev/tty0$!ConditionPathExists=/dev/tty!m) {
214 $self->ct_file_set_contents($systemd_getty_service_rel, $raw);
215 }
216 } else {
217 # undo above change (in case someone updated systemd)
218 if ($raw =~ s!^ConditionPathExists=/dev/tty$!ConditionPathExists=/dev/tty0!m) {
219 $self->ct_file_set_contents($systemd_getty_service_rel, $raw);
220 }
221 }
222
223 my $ttycount = PVE::LXC::get_tty_count($conf);
224
225 for (my $i = 1; $i < 7; $i++) {
226 my $tty_service_lnk = "/etc/systemd/system/getty.target.wants/getty\@tty$i.service";
227 if ($i > $ttycount) {
228 $self->ct_unlink($tty_service_lnk);
229 } else {
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);
233 }
234 }
235 }
236 }
237
238 sub setup_systemd_networkd {
239 my ($self, $conf) = @_;
240
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
246 my $filename = "/etc/systemd/network/$d->{name}.network";
247
248 my $data = <<"DATA";
249 [Match]
250 Name = $d->{name}
251
252 [Network]
253 Description = Interface $d->{name} autoconfigured by PVE
254 DATA
255
256 my $routes = '';
257 my ($has_ipv4, $has_ipv6);
258
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') {
268 $has_ipv4 = 1;
269 $data .= "Address = $ip\n";
270 }
271 }
272 if (defined(my $gw = $d->{gw})) {
273 $data .= "Gateway = $gw\n";
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 }
277 }
278
279 if (defined(my $ip = $d->{ip6})) {
280 if ($ip eq 'dhcp') {
281 $dhcp |= $DHCP6;
282 } elsif ($ip ne 'manual') {
283 $has_ipv6 = 1;
284 $data .= "Address = $ip\n";
285 }
286 }
287 if (defined(my $gw = $d->{gw6})) {
288 $data .= "Gateway = $gw\n";
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 }
292 }
293
294 $data .= "DHCP = $DHCPMODES[$dhcp]\n";
295 $data .= $routes if $routes;
296
297 $self->ct_file_set_contents($filename, $data);
298 }
299 }
300
301 sub setup_securetty {
302 my ($self, $conf, @add) = @_;
303
304 my $filename = "/etc/securetty";
305 my $data = $self->ct_file_get_contents($filename);
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 }
312 $self->ct_file_set_contents($filename, $data);
313 }
314
315 my $replacepw = sub {
316 my ($self, $file, $user, $epw, $shadow) = @_;
317
318 my $tmpfile = "$file.$$";
319
320 eval {
321 my $src = $self->ct_open_file_read($file) ||
322 die "unable to open file '$file' - $!";
323
324 my $st = $self->ct_stat($src) ||
325 die "unable to stat file - $!";
326
327 my $dst = $self->ct_open_file_write($tmpfile) ||
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;
333
334 my $last_change = int(time()/(60*60*24));
335
336 if ($epw =~ m/^\$TEST\$/) { # for regression tests
337 $last_change = 12345;
338 }
339
340 while (defined (my $line = <$src>)) {
341 if ($shadow) {
342 $line =~ s/^${user}:[^:]*:[^:]*:/${user}:${epw}:${last_change}:/;
343 } else {
344 $line =~ s/^${user}:[^:]*:/${user}:${epw}:/;
345 }
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 = $@) {
353 $self->ct_unlink($tmpfile);
354 } else {
355 $self->ct_rename($tmpfile, $file);
356 $self->ct_unlink($tmpfile); # in case rename fails
357 }
358 };
359
360 sub set_user_password {
361 my ($self, $conf, $user, $opt_password) = @_;
362
363 my $pwfile = "/etc/passwd";
364
365 return if !$self->ct_file_exists($pwfile);
366
367 my $shadow = "/etc/shadow";
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
378 if ($self->ct_file_exists($shadow)) {
379 &$replacepw ($self, $shadow, $user, $opt_password, 1);
380 &$replacepw ($self, $pwfile, $user, 'x');
381 } else {
382 &$replacepw ($self, $pwfile, $user, $opt_password);
383 }
384 }
385
386 my $randomize_crontab = sub {
387 my ($self, $conf) = @_;
388
389 my @files;
390 # Note: dir_glob_foreach() untaints filenames!
391 PVE::Tools::dir_glob_foreach("/etc/cron.d", qr/[A-Z\-\_a-z0-9]+/, sub {
392 my ($name) = @_;
393 push @files, "/etc/cron.d/$name";
394 });
395
396 my $crontab_fn = "/etc/crontab";
397 unshift @files, $crontab_fn if $self->ct_file_exists($crontab_fn);
398
399 foreach my $filename (@files) {
400 my $data = $self->ct_file_get_contents($filename);
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 }
412 $self->ct_file_set_contents($filename, $new);
413 }
414 };
415
416 sub pre_start_hook {
417 my ($self, $conf) = @_;
418
419 $self->setup_init($conf);
420 $self->setup_network($conf);
421 $self->set_hostname($conf);
422 $self->set_dns($conf);
423
424 # fixme: what else ?
425 }
426
427 sub post_create_hook {
428 my ($self, $conf, $root_password) = @_;
429
430 $self->template_fixup($conf);
431
432 &$randomize_crontab($self, $conf);
433
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);
439
440 # fixme: what else ?
441 }
442
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
446 sub ct_mkdir {
447 my ($self, $file, $mask) = @_;
448 # mkdir goes by parameter count - an `undef' mode acts like a mode of 0000
449 return CORE::mkdir($file, $mask) if defined ($mask);
450 return CORE::mkdir($file);
451 }
452
453 sub ct_unlink {
454 my ($self, @files) = @_;
455 foreach my $file (@files) {
456 CORE::unlink($file);
457 }
458 }
459
460 sub ct_rename {
461 my ($self, $old, $new) = @_;
462 CORE::rename($old, $new);
463 }
464
465 sub ct_open_file_read {
466 my $self = shift;
467 my $file = shift;
468 return IO::File->new($file, O_RDONLY, @_);
469 }
470
471 sub ct_open_file_write {
472 my $self = shift;
473 my $file = shift;
474 return IO::File->new($file, O_WRONLY | O_CREAT, @_);
475 }
476
477 sub ct_make_path {
478 my $self = shift;
479 File::Path::make_path(@_);
480 }
481
482 sub ct_symlink {
483 my ($self, $old, $new) = @_;
484 return CORE::symlink($old, $new);
485 }
486
487 sub ct_file_exists {
488 my ($self, $file) = @_;
489 return -f $file;
490 }
491
492 sub ct_is_directory {
493 my ($self, $file) = @_;
494 return -d $file;
495 }
496
497 sub ct_is_symlink {
498 my ($self, $file) = @_;
499 return -l $file;
500 }
501
502 sub ct_stat {
503 my ($self, $file) = @_;
504 return File::stat::stat($file);
505 }
506
507 sub ct_file_read_firstline {
508 my ($self, $file) = @_;
509 return PVE::Tools::file_read_firstline($file);
510 }
511
512 sub ct_file_get_contents {
513 my ($self, $file) = @_;
514 return PVE::Tools::file_get_contents($file);
515 }
516
517 sub ct_file_set_contents {
518 my ($self, $file, $data) = @_;
519 return PVE::Tools::file_set_contents($file, $data);
520 }
521
522 1;