]> git.proxmox.com Git - pve-container.git/blame - src/PVE/LXCSetup/Base.pm
mountpoint: add support for host /dev/xxx block device passthrough
[pve-container.git] / src / PVE / LXCSetup / Base.pm
CommitLineData
1c7f4f65
DM
1package PVE::LXCSetup::Base;
2
3use strict;
4use warnings;
5
168d6b07
DM
6use File::stat;
7use Digest::SHA;
8use IO::File;
9use Encode;
10
b9cd9975 11use PVE::INotify;
55fa4e09
DM
12use PVE::Tools;
13
633a7bd8 14sub new {
5b4657d0 15 my ($class, $conf, $rootdir) = @_;
633a7bd8 16
5b4657d0 17 return bless { conf => $conf, rootdir => $rootdir }, $class;
633a7bd8 18}
b9cd9975 19
c0eae401 20sub lookup_dns_conf {
b9cd9975
DM
21 my ($conf) = @_;
22
27916659
DM
23 my $nameserver = $conf->{nameserver};
24 my $searchdomains = $conf->{searchdomain};
b9cd9975
DM
25
26 if (!($nameserver && $searchdomains)) {
27
27916659 28 if ($conf->{'testmode'}) {
b9cd9975
DM
29
30 $nameserver = "8.8.8.8 8.8.8.9";
31 $searchdomains = "promxox.com";
32
33 } else {
34
35 my $host_resolv_conf = PVE::INotify::read_file('resolvconf');
36
37 $searchdomains = $host_resolv_conf->{search};
38
39 my @list = ();
40 foreach my $k ("dns1", "dns2", "dns3") {
41 if (my $ns = $host_resolv_conf->{$k}) {
42 push @list, $ns;
43 }
44 }
45 $nameserver = join(' ', @list);
46 }
47 }
48
49 return ($searchdomains, $nameserver);
c0eae401 50}
b9cd9975 51
c0eae401 52sub update_etc_hosts {
e4929e97 53 my ($etc_hosts_data, $hostip, $oldname, $newname, $searchdomains) = @_;
1c7f4f65 54
1c7f4f65
DM
55 my $done = 0;
56
57 my @lines;
e4929e97
DM
58
59 my $extra_names = '';
60 foreach my $domain (PVE::Tools::split_list($searchdomains)) {
61 $extra_names .= ' ' if $extra_names;
62 $extra_names .= "$newname.$domain";
63 }
1c7f4f65
DM
64
65 foreach my $line (split(/\n/, $etc_hosts_data)) {
66 if ($line =~ m/^#/ || $line =~ m/^\s*$/) {
67 push @lines, $line;
68 next;
69 }
70
71 my ($ip, @names) = split(/\s+/, $line);
72 if (($ip eq '127.0.0.1') || ($ip eq '::1')) {
73 push @lines, $line;
74 next;
75 }
c325b32f 76
1c7f4f65
DM
77 my $found = 0;
78 foreach my $name (@names) {
79 if ($name eq $oldname || $name eq $newname) {
80 $found = 1;
81 } else {
82 # fixme: record extra names?
83 }
84 }
85 $found = 1 if defined($hostip) && ($ip eq $hostip);
86
87 if ($found) {
88 if (!$done) {
89 if (defined($hostip)) {
c325b32f 90 push @lines, "$hostip $extra_names $newname";
1c7f4f65
DM
91 } else {
92 push @lines, "127.0.1.1 $newname";
93 }
94 $done = 1;
95 }
96 next;
97 } else {
98 push @lines, $line;
99 }
100 }
101
102 if (!$done) {
103 if (defined($hostip)) {
e4929e97 104 push @lines, "$hostip $extra_names $newname";
1c7f4f65
DM
105 } else {
106 push @lines, "127.0.1.1 $newname";
107 }
108 }
109
1e180f97
DM
110 my $found_localhost = 0;
111 foreach my $line (@lines) {
112 if ($line =~ m/^127.0.0.1\s/) {
113 $found_localhost = 1;
114 last;
115 }
116 }
117
118 if (!$found_localhost) {
119 unshift @lines, "127.0.0.1 localhost.localnet localhost";
120 }
121
1c7f4f65
DM
122 $etc_hosts_data = join("\n", @lines) . "\n";
123
124 return $etc_hosts_data;
c0eae401 125}
1c7f4f65 126
142444d5
DM
127sub template_fixup {
128 my ($self, $conf) = @_;
129
130 # do nothing by default
131}
132
c325b32f 133sub set_dns {
633a7bd8 134 my ($self, $conf) = @_;
c325b32f 135
c0eae401 136 my ($searchdomains, $nameserver) = lookup_dns_conf($conf);
c325b32f 137
5b4657d0 138 my $rootdir = $self->{rootdir};
c325b32f 139
5b4657d0 140 my $filename = "$rootdir/etc/resolv.conf";
c325b32f
DM
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 PVE::Tools::file_set_contents($filename, $data);
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
5b4657d0 161 my $rootdir = $self->{rootdir};
1c7f4f65 162
5b4657d0 163 my $hostname_fn = "$rootdir/etc/hostname";
1c7f4f65 164
e4929e97 165 my $oldname = PVE::Tools::file_read_firstline($hostname_fn) || 'localhost';
1c7f4f65 166
5b4657d0 167 my $hosts_fn = "$rootdir/etc/hosts";
1c7f4f65
DM
168 my $etc_hosts_data = '';
169
170 if (-f $hosts_fn) {
171 $etc_hosts_data = PVE::Tools::file_get_contents($hosts_fn);
172 }
173
c325b32f
DM
174 my ($ipv4, $ipv6) = PVE::LXC::get_primary_ips($conf);
175 my $hostip = $ipv4 || $ipv6;
b9cd9975 176
c0eae401 177 my ($searchdomains) = lookup_dns_conf($conf);
b9cd9975 178
c0eae401
DM
179 $etc_hosts_data = update_etc_hosts($etc_hosts_data, $hostip, $oldname,
180 $hostname, $searchdomains);
b9cd9975 181
1c7f4f65
DM
182 PVE::Tools::file_set_contents($hostname_fn, "$hostname\n");
183 PVE::Tools::file_set_contents($hosts_fn, $etc_hosts_data);
184}
185
55fa4e09 186sub setup_network {
633a7bd8 187 my ($self, $conf) = @_;
55fa4e09
DM
188
189 die "please implement this inside subclass"
190}
191
d66768a2 192sub setup_init {
633a7bd8 193 my ($self, $conf) = @_;
1c7f4f65 194
d66768a2
DM
195 die "please implement this inside subclass"
196}
197
9143dec4
DM
198sub setup_systemd_console {
199 my ($self, $conf) = @_;
200
201 my $rootdir = $self->{rootdir};
202
203 my $systemd_dir_rel = -x "$rootdir/lib/systemd/systemd" ?
204 "/lib/systemd/system" : "/usr/lib/systemd/system";
205
206 my $systemd_dir = "$rootdir/$systemd_dir_rel";
207
208 my $etc_systemd_dir = "$rootdir/etc/systemd/system";
209
210 my $systemd_getty_service_rel = "$systemd_dir_rel/getty\@.service";
211
212 my $systemd_getty_service = "$rootdir/$systemd_getty_service_rel";
213
214 return if ! -f $systemd_getty_service;
215
216 my $raw = PVE::Tools::file_get_contents($systemd_getty_service);
217
c69ae0d0
DM
218 my $systemd_container_getty_service_rel = "$systemd_dir_rel/container-getty\@.service";
219 my $systemd_container_getty_service = "$rootdir/$systemd_container_getty_service_rel";
220
221 # systemd on CenoOS 7.1 is too old (version 205), so there is no
222 # container-getty service
223 if (! -f $systemd_container_getty_service) {
224 if ($raw =~ s!^ConditionPathExists=/dev/tty0$!ConditionPathExists=/dev/tty!m) {
225 PVE::Tools::file_set_contents($systemd_getty_service, $raw);
226 }
227 } else {
228 # undo above change (in case someone updated systemd)
229 if ($raw =~ s!^ConditionPathExists=/dev/tty$!ConditionPathExists=/dev/tty0!m) {
230 PVE::Tools::file_set_contents($systemd_getty_service, $raw);
231 }
9143dec4
DM
232 }
233
0d0ca400 234 my $ttycount = PVE::LXC::get_tty_count($conf);
9143dec4
DM
235
236 for (my $i = 1; $i < 7; $i++) {
237 my $tty_service_lnk = "$etc_systemd_dir/getty.target.wants/getty\@tty$i.service";
238 if ($i > $ttycount) {
239 unlink $tty_service_lnk;
240 } else {
241 if (! -l $tty_service_lnk) {
242 unlink $tty_service_lnk;
243 symlink($systemd_getty_service_rel, $tty_service_lnk);
244 }
245 }
246 }
247}
248
c1d32b55
WB
249sub setup_systemd_networkd {
250 my ($self, $conf) = @_;
251
252 my $rootdir = $self->{rootdir};
253
254 foreach my $k (keys %$conf) {
255 next if $k !~ m/^net(\d+)$/;
256 my $d = PVE::LXC::parse_lxc_network($conf->{$k});
257 next if !$d->{name};
258
259 my $filename = "$rootdir/etc/systemd/network/$d->{name}.network";
260
261 my $data = <<"DATA";
262[Match]
263Name = $d->{name}
264
265[Network]
266Description = Interface $d->{name} autoconfigured by PVE
267DATA
268 # DHCP bitflags:
269 my @DHCPMODES = ('none', 'v4', 'v6', 'both');
270 my ($NONE, $DHCP4, $DHCP6, $BOTH) = (0, 1, 2, 3);
271 my $dhcp = $NONE;
272
273 if (defined(my $ip = $d->{ip})) {
274 if ($ip eq 'dhcp') {
275 $dhcp |= $DHCP4;
276 } elsif ($ip ne 'manual') {
277 $data .= "Address = $ip\n";
278 }
279 }
280 if (defined(my $gw = $d->{gw})) {
281 $data .= "Gateway = $gw\n";
282 }
283
284 if (defined(my $ip = $d->{ip6})) {
285 if ($ip eq 'dhcp') {
286 $dhcp |= $DHCP6;
287 } elsif ($ip ne 'manual') {
288 $data .= "Address = $ip\n";
289 }
290 }
291 if (defined(my $gw = $d->{gw6})) {
292 $data .= "Gateway = $gw\n";
293 }
294
295 $data .= "DHCP = $DHCPMODES[$dhcp]\n";
296
297 PVE::Tools::file_set_contents($filename, $data);
298 }
b7cd927f
WB
299}
300
301sub setup_securetty {
302 my ($self, $conf, @add) = @_;
c1d32b55 303
b7cd927f
WB
304 my $rootdir = $self->{rootdir};
305 my $filename = "$rootdir/etc/securetty";
306 my $data = PVE::Tools::file_get_contents($filename);
307 chomp $data; $data .= "\n";
308 foreach my $dev (@add) {
309 if ($data !~ m!^\Q$dev\E\s*$!m) {
310 $data .= "$dev\n";
311 }
312 }
313 PVE::Tools::file_set_contents($filename, $data);
c1d32b55
WB
314}
315
168d6b07 316my $replacepw = sub {
367a7c18 317 my ($file, $user, $epw, $shadow) = @_;
168d6b07
DM
318
319 my $tmpfile = "$file.$$";
320
321 eval {
322 my $src = IO::File->new("<$file") ||
323 die "unable to open file '$file' - $!";
324
325 my $st = File::stat::stat($src) ||
326 die "unable to stat file - $!";
327
328 my $dst = IO::File->new(">$tmpfile") ||
329 die "unable to open file '$tmpfile' - $!";
330
331 # copy owner and permissions
332 chmod $st->mode, $dst;
333 chown $st->uid, $st->gid, $dst;
367a7c18
DM
334
335 my $last_change = int(time()/(60*60*24));
336
337 if ($epw =~ m/^\$TEST\$/) { # for regression tests
338 $last_change = 12345;
339 }
168d6b07
DM
340
341 while (defined (my $line = <$src>)) {
367a7c18
DM
342 if ($shadow) {
343 $line =~ s/^${user}:[^:]*:[^:]*:/${user}:${epw}:${last_change}:/;
344 } else {
345 $line =~ s/^${user}:[^:]*:/${user}:${epw}:/;
346 }
168d6b07
DM
347 print $dst $line;
348 }
349
350 $src->close() || die "close '$file' failed - $!\n";
351 $dst->close() || die "close '$tmpfile' failed - $!\n";
352 };
353 if (my $err = $@) {
354 unlink $tmpfile;
355 } else {
356 rename $tmpfile, $file;
357 unlink $tmpfile; # in case rename fails
358 }
359};
360
361sub set_user_password {
633a7bd8 362 my ($self, $conf, $user, $opt_password) = @_;
168d6b07 363
5b4657d0 364 my $rootdir = $self->{rootdir};
168d6b07 365
5b4657d0 366 my $pwfile = "$rootdir/etc/passwd";
168d6b07
DM
367
368 return if ! -f $pwfile;
369
5b4657d0 370 my $shadow = "$rootdir/etc/shadow";
168d6b07
DM
371
372 if (defined($opt_password)) {
373 if ($opt_password !~ m/^\$/) {
374 my $time = substr (Digest::SHA::sha1_base64 (time), 0, 8);
375 $opt_password = crypt(encode("utf8", $opt_password), "\$1\$$time\$");
376 };
377 } else {
378 $opt_password = '*';
379 }
380
381 if (-f $shadow) {
367a7c18 382 &$replacepw ($shadow, $user, $opt_password, 1);
168d6b07
DM
383 &$replacepw ($pwfile, $user, 'x');
384 } else {
385 &$replacepw ($pwfile, $user, $opt_password);
386 }
387}
388
4727bd09
DM
389my $randomize_crontab = sub {
390 my ($self, $conf) = @_;
391
392 my $rootdir = $self->{rootdir};
393
b5e62cd0
DM
394 my @files;
395 # Note: dir_glob_foreach() untaints filenames!
396 my $cron_dir = "$rootdir/etc/cron.d";
397 PVE::Tools::dir_glob_foreach($cron_dir, qr/[A-Z\-\_a-z0-9]+/, sub {
398 my ($name) = @_;
399 push @files, "$cron_dir/$name";
400 });
4727bd09
DM
401
402 my $crontab_fn = "$rootdir/etc/crontab";
403 unshift @files, $crontab_fn if -f $crontab_fn;
404
405 foreach my $filename (@files) {
406 my $data = PVE::Tools::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 PVE::Tools::file_set_contents($filename, $new);
419 }
420};
421
7ee31468
DM
422sub rewrite_ssh_host_keys {
423 my ($self, $conf) = @_;
424
425 my $rootdir = $self->{rootdir};
426
427 my $etc_ssh_dir = "$rootdir/etc/ssh";
428
429 return if ! -d $etc_ssh_dir;
430
431 my $keynames = {
432 rsa1 => 'ssh_host_key',
433 rsa => 'ssh_host_rsa_key',
434 dsa => 'ssh_host_dsa_key',
435 ecdsa => 'ssh_host_ecdsa_key',
436 ed25519 => 'ssh_host_ed25519_key',
437 };
438
27916659 439 my $hostname = $conf->{hostname} || 'localhost';
7ee31468
DM
440 $hostname =~ s/\..*$//;
441
442 foreach my $keytype (keys %$keynames) {
443 my $basename = $keynames->{$keytype};
444 unlink "${etc_ssh_dir}/$basename";
445 unlink "${etc_ssh_dir}/$basename.pub";
446 print "Creating SSH host key '$basename' - this may take some time ...\n";
447 my $cmd = ['ssh-keygen', '-q', '-f', "${etc_ssh_dir}/$basename", '-t', $keytype,
448 '-N', '', '-C', "root\@$hostname"];
449 PVE::Tools::run_command($cmd);
450 }
451}
452
d66768a2 453sub pre_start_hook {
633a7bd8 454 my ($self, $conf) = @_;
d66768a2 455
633a7bd8
DM
456 $self->setup_init($conf);
457 $self->setup_network($conf);
458 $self->set_hostname($conf);
459 $self->set_dns($conf);
d66768a2
DM
460
461 # fixme: what else ?
462}
463
464sub post_create_hook {
633a7bd8 465 my ($self, $conf, $root_password) = @_;
d66768a2 466
142444d5 467 $self->template_fixup($conf);
4727bd09
DM
468
469 &$randomize_crontab($self, $conf);
470
633a7bd8
DM
471 $self->set_user_password($conf, 'root', $root_password);
472 $self->setup_init($conf);
473 $self->setup_network($conf);
474 $self->set_hostname($conf);
475 $self->set_dns($conf);
7ee31468 476 $self->rewrite_ssh_host_keys($conf);
168d6b07 477
55fa4e09 478 # fixme: what else ?
1c7f4f65
DM
479}
480
4811;