]>
Commit | Line | Data |
---|---|---|
7af97ad5 | 1 | package PVE::LXC::Setup::Base; |
1c7f4f65 DM |
2 | |
3 | use strict; | |
4 | use warnings; | |
5 | ||
168d6b07 DM |
6 | use File::stat; |
7 | use Digest::SHA; | |
8 | use IO::File; | |
9 | use Encode; | |
f08b2779 | 10 | use Fcntl; |
2063d380 | 11 | use File::Path; |
f08b2779 | 12 | use File::Spec; |
bd1dc8d1 | 13 | use File::Basename; |
168d6b07 | 14 | |
b9cd9975 | 15 | use PVE::INotify; |
55fa4e09 | 16 | use PVE::Tools; |
4401b7d4 | 17 | use PVE::Network; |
55fa4e09 | 18 | |
633a7bd8 | 19 | sub new { |
153747ff | 20 | my ($class, $conf, $rootdir, $os_release) = @_; |
633a7bd8 | 21 | |
153747ff | 22 | return bless { conf => $conf, rootdir => $rootdir, os_release => $os_release }, $class; |
633a7bd8 | 23 | } |
b9cd9975 | 24 | |
c0eae401 | 25 | sub lookup_dns_conf { |
23d928a1 | 26 | my ($self, $conf) = @_; |
b9cd9975 | 27 | |
27916659 DM |
28 | my $nameserver = $conf->{nameserver}; |
29 | my $searchdomains = $conf->{searchdomain}; | |
b9cd9975 | 30 | |
ffc468ee DC |
31 | if ($conf->{'testmode'}) { |
32 | return ('proxmox.com', '8.8.8.8 8.8.8.9'); | |
33 | } | |
b9cd9975 | 34 | |
ffc468ee | 35 | my $host_resolv_conf = $self->{host_resolv_conf}; |
b9cd9975 | 36 | |
ffc468ee DC |
37 | if (!defined($nameserver)) { |
38 | my @list = (); | |
39 | foreach my $k ("dns1", "dns2", "dns3") { | |
40 | if (my $ns = $host_resolv_conf->{$k}) { | |
41 | push @list, $ns; | |
b9cd9975 | 42 | } |
b9cd9975 | 43 | } |
ffc468ee DC |
44 | $nameserver = join(' ', @list); |
45 | } | |
46 | ||
47 | if (!defined($searchdomains)) { | |
48 | $searchdomains = $host_resolv_conf->{search}; | |
b9cd9975 DM |
49 | } |
50 | ||
51 | return ($searchdomains, $nameserver); | |
c0eae401 | 52 | } |
b9cd9975 | 53 | |
c0eae401 | 54 | sub update_etc_hosts { |
9096a91d | 55 | my ($self, $hostip, $oldname, $newname, $searchdomains) = @_; |
1c7f4f65 | 56 | |
1b3cffda WB |
57 | my $hosts_fn = '/etc/hosts'; |
58 | return if $self->ct_is_file_ignored($hosts_fn); | |
59 | ||
ce289e3c WB |
60 | my $namepart = ($newname =~ s/\..*$//r); |
61 | ||
005f91ad | 62 | my $all_names = ''; |
ce289e3c | 63 | if ($newname =~ /\./) { |
005f91ad | 64 | $all_names .= "$newname $namepart"; |
ce289e3c WB |
65 | } else { |
66 | foreach my $domain (PVE::Tools::split_list($searchdomains)) { | |
005f91ad WB |
67 | $all_names .= ' ' if $all_names; |
68 | $all_names .= "$newname.$domain"; | |
ce289e3c | 69 | } |
005f91ad WB |
70 | $all_names .= ' ' if $all_names; |
71 | $all_names .= $newname; | |
e4929e97 | 72 | } |
1c7f4f65 | 73 | |
9096a91d WB |
74 | # Prepare section: |
75 | my $section = ''; | |
c325b32f | 76 | |
9096a91d WB |
77 | my $lo4 = "127.0.0.1 localhost.localnet localhost\n"; |
78 | my $lo6 = "::1 localhost.localnet localhost\n"; | |
79 | if ($self->ct_file_exists($hosts_fn)) { | |
80 | my $data = $self->ct_file_get_contents($hosts_fn); | |
81 | # don't take localhost entries within our hosts sections into account | |
82 | $data = remove_pve_sections($data); | |
1c7f4f65 | 83 | |
9096a91d WB |
84 | # check for existing localhost entries |
85 | $section .= $lo4 if $data !~ /^\h*127\.0\.0\.1\h+/m; | |
86 | $section .= $lo6 if $data !~ /^\h*::1\h+/m; | |
87 | } else { | |
88 | $section .= $lo4 . $lo6; | |
1c7f4f65 DM |
89 | } |
90 | ||
9096a91d WB |
91 | if (defined($hostip)) { |
92 | $section .= "$hostip $all_names\n"; | |
63ec4b3b DC |
93 | } elsif ($namepart ne 'localhost') { |
94 | $section .= "127.0.1.1 $all_names\n"; | |
9096a91d WB |
95 | } else { |
96 | $section .= "127.0.1.1 $namepart\n"; | |
1e180f97 DM |
97 | } |
98 | ||
9096a91d | 99 | $self->ct_modify_file($hosts_fn, $section); |
c0eae401 | 100 | } |
1c7f4f65 | 101 | |
142444d5 DM |
102 | sub template_fixup { |
103 | my ($self, $conf) = @_; | |
104 | ||
105 | # do nothing by default | |
106 | } | |
107 | ||
c325b32f | 108 | sub set_dns { |
633a7bd8 | 109 | my ($self, $conf) = @_; |
c325b32f | 110 | |
23d928a1 | 111 | my ($searchdomains, $nameserver) = $self->lookup_dns_conf($conf); |
c325b32f | 112 | |
c325b32f DM |
113 | my $data = ''; |
114 | ||
115 | $data .= "search " . join(' ', PVE::Tools::split_list($searchdomains)) . "\n" | |
116 | if $searchdomains; | |
117 | ||
118 | foreach my $ns ( PVE::Tools::split_list($nameserver)) { | |
119 | $data .= "nameserver $ns\n"; | |
120 | } | |
121 | ||
2edb50e5 | 122 | $self->ct_modify_file("/etc/resolv.conf", $data, replace => 1); |
c325b32f DM |
123 | } |
124 | ||
1c7f4f65 | 125 | sub set_hostname { |
633a7bd8 | 126 | my ($self, $conf) = @_; |
1c7f4f65 | 127 | |
27916659 | 128 | my $hostname = $conf->{hostname} || 'localhost'; |
1c7f4f65 | 129 | |
ce289e3c | 130 | my $namepart = ($hostname =~ s/\..*$//r); |
1c7f4f65 | 131 | |
f08b2779 | 132 | my $hostname_fn = "/etc/hostname"; |
1c7f4f65 | 133 | |
f08b2779 | 134 | my $oldname = $self->ct_file_read_firstline($hostname_fn) || 'localhost'; |
1c7f4f65 | 135 | |
c325b32f DM |
136 | my ($ipv4, $ipv6) = PVE::LXC::get_primary_ips($conf); |
137 | my $hostip = $ipv4 || $ipv6; | |
b9cd9975 | 138 | |
23d928a1 | 139 | my ($searchdomains) = $self->lookup_dns_conf($conf); |
b9cd9975 | 140 | |
9096a91d | 141 | $self->update_etc_hosts($hostip, $oldname, $hostname, $searchdomains); |
b9cd9975 | 142 | |
ce289e3c | 143 | $self->ct_file_set_contents($hostname_fn, "$namepart\n"); |
1c7f4f65 DM |
144 | } |
145 | ||
55fa4e09 | 146 | sub setup_network { |
633a7bd8 | 147 | my ($self, $conf) = @_; |
55fa4e09 DM |
148 | |
149 | die "please implement this inside subclass" | |
150 | } | |
151 | ||
d66768a2 | 152 | sub setup_init { |
633a7bd8 | 153 | my ($self, $conf) = @_; |
1c7f4f65 | 154 | |
d66768a2 DM |
155 | die "please implement this inside subclass" |
156 | } | |
157 | ||
9143dec4 DM |
158 | sub setup_systemd_console { |
159 | my ($self, $conf) = @_; | |
160 | ||
e7deac01 | 161 | my $systemd_dir_rel = $self->ct_is_executable("/lib/systemd/systemd") ? |
9143dec4 DM |
162 | "/lib/systemd/system" : "/usr/lib/systemd/system"; |
163 | ||
9143dec4 DM |
164 | my $systemd_getty_service_rel = "$systemd_dir_rel/getty\@.service"; |
165 | ||
f08b2779 | 166 | return if !$self->ct_file_exists($systemd_getty_service_rel); |
9143dec4 | 167 | |
1b4cf758 | 168 | my $ttycount = PVE::LXC::Config->get_tty_count($conf); |
9143dec4 DM |
169 | |
170 | for (my $i = 1; $i < 7; $i++) { | |
f08b2779 | 171 | my $tty_service_lnk = "/etc/systemd/system/getty.target.wants/getty\@tty$i.service"; |
9143dec4 | 172 | if ($i > $ttycount) { |
f08b2779 | 173 | $self->ct_unlink($tty_service_lnk); |
9143dec4 | 174 | } else { |
f08b2779 WB |
175 | if (!$self->ct_is_symlink($tty_service_lnk)) { |
176 | $self->ct_unlink($tty_service_lnk); | |
177 | $self->ct_symlink($systemd_getty_service_rel, $tty_service_lnk); | |
9143dec4 DM |
178 | } |
179 | } | |
180 | } | |
181 | } | |
182 | ||
5e84bdc8 DM |
183 | # A few distros as well as unprivileged containers cannot deal with the |
184 | # /dev/lxc/ tty subdirectory. | |
185 | sub devttydir { | |
186 | my ($self, $conf) = @_; | |
187 | return $conf->{unprivileged} ? '' : 'lxc/'; | |
188 | } | |
189 | ||
dd7a436b TL |
190 | sub fixup_old_getty { |
191 | my ($self) = @_; | |
192 | ||
193 | my $sd_dir_rel = $self->ct_is_executable("/lib/systemd/systemd") ? | |
194 | "/lib/systemd/system" : "/usr/lib/systemd/system"; | |
195 | ||
196 | my $sd_getty_service_rel = "$sd_dir_rel/getty\@.service"; | |
197 | return if !$self->ct_file_exists($sd_getty_service_rel); | |
198 | ||
199 | my $raw = $self->ct_file_get_contents($sd_getty_service_rel); | |
200 | ||
201 | my $sd_container_getty_service_rel = "$sd_dir_rel/container-getty\@.service"; | |
202 | # systemd on CenoOS 7.1 is too old (version 205), so there is no | |
203 | # container-getty service | |
204 | if (!$self->ct_file_exists($sd_container_getty_service_rel)) { | |
205 | if ($raw =~ s!^ConditionPathExists=/dev/tty0$!ConditionPathExists=/dev/tty!m) { | |
206 | $self->ct_file_set_contents($sd_getty_service_rel, $raw); | |
207 | } | |
208 | } else { | |
209 | # undo above change (in case someone updated systemd) | |
210 | if ($raw =~ s!^ConditionPathExists=/dev/tty$!ConditionPathExists=/dev/tty0!m) { | |
211 | $self->ct_file_set_contents($sd_getty_service_rel, $raw); | |
212 | } | |
213 | } | |
214 | } | |
215 | ||
90b21cdc | 216 | sub setup_container_getty_service { |
5e84bdc8 DM |
217 | my ($self, $conf) = @_; |
218 | ||
e7deac01 | 219 | my $systemd_dir_rel = $self->ct_is_executable("/lib/systemd/systemd") ? |
8f115f7c WB |
220 | "/lib/systemd/system" : "/usr/lib/systemd/system"; |
221 | my $servicefile = "$systemd_dir_rel/container-getty\@.service"; | |
90b21cdc | 222 | my $raw = $self->ct_file_get_contents($servicefile); |
5e84bdc8 | 223 | my $ttyname = $self->devttydir($conf) . 'tty%I'; |
418ec240 | 224 | if ($raw =~ s@pts/%I|lxc/tty%I@$ttyname@g) { |
90b21cdc WB |
225 | $self->ct_file_set_contents($servicefile, $raw); |
226 | } | |
227 | } | |
228 | ||
c1d32b55 WB |
229 | sub setup_systemd_networkd { |
230 | my ($self, $conf) = @_; | |
231 | ||
c1d32b55 WB |
232 | foreach my $k (keys %$conf) { |
233 | next if $k !~ m/^net(\d+)$/; | |
1b4cf758 | 234 | my $d = PVE::LXC::Config->parse_lxc_network($conf->{$k}); |
c1d32b55 WB |
235 | next if !$d->{name}; |
236 | ||
f08b2779 | 237 | my $filename = "/etc/systemd/network/$d->{name}.network"; |
c1d32b55 WB |
238 | |
239 | my $data = <<"DATA"; | |
240 | [Match] | |
241 | Name = $d->{name} | |
242 | ||
243 | [Network] | |
244 | Description = Interface $d->{name} autoconfigured by PVE | |
245 | DATA | |
4401b7d4 WB |
246 | |
247 | my $routes = ''; | |
248 | my ($has_ipv4, $has_ipv6); | |
249 | ||
c1d32b55 WB |
250 | # DHCP bitflags: |
251 | my @DHCPMODES = ('none', 'v4', 'v6', 'both'); | |
252 | my ($NONE, $DHCP4, $DHCP6, $BOTH) = (0, 1, 2, 3); | |
253 | my $dhcp = $NONE; | |
bb7f06ef | 254 | my $accept_ra = 'false'; |
c1d32b55 WB |
255 | |
256 | if (defined(my $ip = $d->{ip})) { | |
257 | if ($ip eq 'dhcp') { | |
258 | $dhcp |= $DHCP4; | |
259 | } elsif ($ip ne 'manual') { | |
4401b7d4 | 260 | $has_ipv4 = 1; |
c1d32b55 WB |
261 | $data .= "Address = $ip\n"; |
262 | } | |
263 | } | |
264 | if (defined(my $gw = $d->{gw})) { | |
265 | $data .= "Gateway = $gw\n"; | |
4401b7d4 WB |
266 | if ($has_ipv4 && !PVE::Network::is_ip_in_cidr($gw, $d->{ip}, 4)) { |
267 | $routes .= "\n[Route]\nDestination = $gw/32\nScope = link\n"; | |
268 | } | |
c1d32b55 WB |
269 | } |
270 | ||
271 | if (defined(my $ip = $d->{ip6})) { | |
272 | if ($ip eq 'dhcp') { | |
273 | $dhcp |= $DHCP6; | |
bb7f06ef WB |
274 | } elsif ($ip eq 'auto') { |
275 | $accept_ra = 'true'; | |
c1d32b55 | 276 | } elsif ($ip ne 'manual') { |
4401b7d4 | 277 | $has_ipv6 = 1; |
c1d32b55 WB |
278 | $data .= "Address = $ip\n"; |
279 | } | |
280 | } | |
281 | if (defined(my $gw = $d->{gw6})) { | |
bb7f06ef | 282 | $accept_ra = 'false'; |
c1d32b55 | 283 | $data .= "Gateway = $gw\n"; |
d13fd23a WB |
284 | if ($has_ipv6 && !PVE::Network::is_ip_in_cidr($gw, $d->{ip6}, 6) && |
285 | !PVE::Network::is_ip_in_cidr($gw, 'fe80::/10', 6)) { | |
4401b7d4 WB |
286 | $routes .= "\n[Route]\nDestination = $gw/128\nScope = link\n"; |
287 | } | |
c1d32b55 WB |
288 | } |
289 | ||
290 | $data .= "DHCP = $DHCPMODES[$dhcp]\n"; | |
bb7f06ef | 291 | $data .= "IPv6AcceptRA = $accept_ra\n"; |
4401b7d4 | 292 | $data .= $routes if $routes; |
c1d32b55 | 293 | |
f08b2779 | 294 | $self->ct_file_set_contents($filename, $data); |
c1d32b55 | 295 | } |
b7cd927f WB |
296 | } |
297 | ||
298 | sub setup_securetty { | |
299 | my ($self, $conf, @add) = @_; | |
c1d32b55 | 300 | |
f08b2779 | 301 | my $filename = "/etc/securetty"; |
bd3093ef TL |
302 | # root login is already allowed on every device if no securetty present |
303 | return if !$self->ct_file_exists($filename); | |
304 | ||
5e84bdc8 DM |
305 | if (!scalar(@add)) { |
306 | @add = qw(console tty1 tty2 tty3 tty4); | |
307 | if (my $dir = $self->devttydir($conf)) { | |
308 | @add = map { "${dir}$_" } @add; | |
309 | } | |
310 | } | |
311 | ||
f08b2779 | 312 | my $data = $self->ct_file_get_contents($filename); |
b7cd927f WB |
313 | chomp $data; $data .= "\n"; |
314 | foreach my $dev (@add) { | |
315 | if ($data !~ m!^\Q$dev\E\s*$!m) { | |
316 | $data .= "$dev\n"; | |
317 | } | |
318 | } | |
f08b2779 | 319 | $self->ct_file_set_contents($filename, $data); |
c1d32b55 WB |
320 | } |
321 | ||
168d6b07 | 322 | my $replacepw = sub { |
f08b2779 | 323 | my ($self, $file, $user, $epw, $shadow) = @_; |
168d6b07 DM |
324 | |
325 | my $tmpfile = "$file.$$"; | |
326 | ||
327 | eval { | |
f08b2779 | 328 | my $src = $self->ct_open_file_read($file) || |
168d6b07 DM |
329 | die "unable to open file '$file' - $!"; |
330 | ||
f08b2779 | 331 | my $st = $self->ct_stat($src) || |
168d6b07 DM |
332 | die "unable to stat file - $!"; |
333 | ||
f08b2779 | 334 | my $dst = $self->ct_open_file_write($tmpfile) || |
168d6b07 DM |
335 | die "unable to open file '$tmpfile' - $!"; |
336 | ||
337 | # copy owner and permissions | |
338 | chmod $st->mode, $dst; | |
339 | chown $st->uid, $st->gid, $dst; | |
367a7c18 DM |
340 | |
341 | my $last_change = int(time()/(60*60*24)); | |
342 | ||
168d6b07 | 343 | while (defined (my $line = <$src>)) { |
367a7c18 DM |
344 | if ($shadow) { |
345 | $line =~ s/^${user}:[^:]*:[^:]*:/${user}:${epw}:${last_change}:/; | |
346 | } else { | |
347 | $line =~ s/^${user}:[^:]*:/${user}:${epw}:/; | |
348 | } | |
168d6b07 DM |
349 | print $dst $line; |
350 | } | |
351 | ||
352 | $src->close() || die "close '$file' failed - $!\n"; | |
353 | $dst->close() || die "close '$tmpfile' failed - $!\n"; | |
354 | }; | |
355 | if (my $err = $@) { | |
f08b2779 | 356 | $self->ct_unlink($tmpfile); |
168d6b07 | 357 | } else { |
f08b2779 WB |
358 | $self->ct_rename($tmpfile, $file); |
359 | $self->ct_unlink($tmpfile); # in case rename fails | |
168d6b07 DM |
360 | } |
361 | }; | |
362 | ||
363 | sub set_user_password { | |
633a7bd8 | 364 | my ($self, $conf, $user, $opt_password) = @_; |
168d6b07 | 365 | |
f08b2779 | 366 | my $pwfile = "/etc/passwd"; |
168d6b07 | 367 | |
f08b2779 | 368 | return if !$self->ct_file_exists($pwfile); |
168d6b07 | 369 | |
f08b2779 | 370 | my $shadow = "/etc/shadow"; |
168d6b07 DM |
371 | |
372 | if (defined($opt_password)) { | |
a46b2fb3 | 373 | if ($opt_password !~ m/^\$(?:1|2[axy]?|5|6)\$[a-zA-Z0-9.\/]{1,16}\$[a-zA-Z0-9.\/]+$/) { |
168d6b07 | 374 | my $time = substr (Digest::SHA::sha1_base64 (time), 0, 8); |
f55589da | 375 | $opt_password = crypt(encode("utf8", $opt_password), "\$6\$$time\$"); |
168d6b07 DM |
376 | }; |
377 | } else { | |
378 | $opt_password = '*'; | |
379 | } | |
380 | ||
f08b2779 WB |
381 | if ($self->ct_file_exists($shadow)) { |
382 | &$replacepw ($self, $shadow, $user, $opt_password, 1); | |
383 | &$replacepw ($self, $pwfile, $user, 'x'); | |
168d6b07 | 384 | } else { |
f08b2779 | 385 | &$replacepw ($self, $pwfile, $user, $opt_password); |
168d6b07 DM |
386 | } |
387 | } | |
388 | ||
f36ce482 FG |
389 | my $parse_home_dir = sub { |
390 | my ($self, $passwdfile, $user) = @_; | |
391 | ||
392 | my $fh = $self->ct_open_file_read($passwdfile); | |
393 | while (defined (my $line = <$fh>)) { | |
394 | return $2 | |
395 | if $line =~ m/^${user}:([^:]*:){4}([^:]*):/; | |
396 | } | |
397 | }; | |
398 | ||
399 | sub set_user_authorized_ssh_keys { | |
400 | my ($self, $conf, $user, $ssh_keys) = @_; | |
401 | ||
402 | my $passwd = "/etc/passwd"; | |
403 | my $home = $user eq "root" ? "/root/" : "/home/$user/"; | |
404 | ||
405 | $home = &$parse_home_dir($self, $passwd, $user) | |
406 | if $self->ct_file_exists($passwd); | |
407 | ||
408 | die "home directory '$home' of $user does not exist!" | |
409 | if ! ($self->ct_is_directory($home) || $self->ct_is_symlink($home)); | |
410 | ||
411 | $self->ct_mkdir("$home/.ssh", 0700) | |
412 | if ! $self->ct_is_directory("$home/.ssh"); | |
413 | ||
414 | $self->ct_modify_file("$home/.ssh/authorized_keys", $ssh_keys, perms => 0700); | |
415 | } | |
416 | ||
4727bd09 DM |
417 | my $randomize_crontab = sub { |
418 | my ($self, $conf) = @_; | |
419 | ||
b5e62cd0 DM |
420 | my @files; |
421 | # Note: dir_glob_foreach() untaints filenames! | |
f08b2779 | 422 | PVE::Tools::dir_glob_foreach("/etc/cron.d", qr/[A-Z\-\_a-z0-9]+/, sub { |
b5e62cd0 | 423 | my ($name) = @_; |
f08b2779 | 424 | push @files, "/etc/cron.d/$name"; |
b5e62cd0 | 425 | }); |
4727bd09 | 426 | |
f08b2779 WB |
427 | my $crontab_fn = "/etc/crontab"; |
428 | unshift @files, $crontab_fn if $self->ct_file_exists($crontab_fn); | |
4727bd09 DM |
429 | |
430 | foreach my $filename (@files) { | |
f08b2779 | 431 | my $data = $self->ct_file_get_contents($filename); |
4727bd09 DM |
432 | my $new = ''; |
433 | foreach my $line (split(/\n/, $data)) { | |
434 | # we only randomize minutes for root crontab entries | |
435 | if ($line =~ m/^\d+(\s+\S+\s+\S+\s+\S+\s+\S+\s+root\s+\S.*)$/) { | |
436 | my $rest = $1; | |
437 | my $min = int(rand()*59); | |
438 | $new .= "$min$rest\n"; | |
439 | } else { | |
440 | $new .= "$line\n"; | |
441 | } | |
442 | } | |
f08b2779 | 443 | $self->ct_file_set_contents($filename, $new); |
4727bd09 DM |
444 | } |
445 | }; | |
446 | ||
d66768a2 | 447 | sub pre_start_hook { |
633a7bd8 | 448 | my ($self, $conf) = @_; |
d66768a2 | 449 | |
633a7bd8 DM |
450 | $self->setup_init($conf); |
451 | $self->setup_network($conf); | |
452 | $self->set_hostname($conf); | |
453 | $self->set_dns($conf); | |
d66768a2 DM |
454 | |
455 | # fixme: what else ? | |
456 | } | |
457 | ||
458 | sub post_create_hook { | |
f36ce482 | 459 | my ($self, $conf, $root_password, $ssh_keys) = @_; |
d66768a2 | 460 | |
142444d5 | 461 | $self->template_fixup($conf); |
4727bd09 DM |
462 | |
463 | &$randomize_crontab($self, $conf); | |
464 | ||
633a7bd8 | 465 | $self->set_user_password($conf, 'root', $root_password); |
f36ce482 | 466 | $self->set_user_authorized_ssh_keys($conf, 'root', $ssh_keys) if $ssh_keys; |
633a7bd8 DM |
467 | $self->setup_init($conf); |
468 | $self->setup_network($conf); | |
469 | $self->set_hostname($conf); | |
470 | $self->set_dns($conf); | |
168d6b07 | 471 | |
55fa4e09 | 472 | # fixme: what else ? |
1c7f4f65 DM |
473 | } |
474 | ||
f08b2779 WB |
475 | # File access wrappers for container setup code. |
476 | # For user-namespace support these might need to take uid and gid maps into account. | |
477 | ||
bd1dc8d1 WB |
478 | sub ct_is_file_ignored { |
479 | my ($self, $file) = @_; | |
480 | my ($name, $path) = fileparse($file); | |
481 | return -f "$path/.pve-ignore.$name"; | |
482 | } | |
483 | ||
c6a605f9 WB |
484 | sub ct_reset_ownership { |
485 | my ($self, @files) = @_; | |
486 | my $conf = $self->{conf}; | |
487 | return if !$self->{id_map}; | |
bd1dc8d1 WB |
488 | |
489 | @files = grep { !$self->ct_is_file_ignored($_) } @files; | |
490 | return if !@files; | |
491 | ||
c6a605f9 WB |
492 | my $uid = $self->{rootuid}; |
493 | my $gid = $self->{rootgid}; | |
494 | chown($uid, $gid, @files); | |
495 | } | |
496 | ||
2063d380 WB |
497 | sub ct_mkdir { |
498 | my ($self, $file, $mask) = @_; | |
f08b2779 | 499 | # mkdir goes by parameter count - an `undef' mode acts like a mode of 0000 |
c6a605f9 WB |
500 | if (defined($mask)) { |
501 | return CORE::mkdir($file, $mask) && $self->ct_reset_ownership($file); | |
502 | } else { | |
503 | return CORE::mkdir($file) && $self->ct_reset_ownership($file); | |
504 | } | |
2063d380 WB |
505 | } |
506 | ||
507 | sub ct_unlink { | |
f08b2779 WB |
508 | my ($self, @files) = @_; |
509 | foreach my $file (@files) { | |
bd1dc8d1 | 510 | next if $self->ct_is_file_ignored($file); |
f08b2779 WB |
511 | CORE::unlink($file); |
512 | } | |
513 | } | |
514 | ||
515 | sub ct_rename { | |
516 | my ($self, $old, $new) = @_; | |
bd1dc8d1 | 517 | return if $self->ct_is_file_ignored($new); |
f08b2779 | 518 | CORE::rename($old, $new); |
2063d380 WB |
519 | } |
520 | ||
f08b2779 | 521 | sub ct_open_file_read { |
2063d380 | 522 | my $self = shift; |
f08b2779 WB |
523 | my $file = shift; |
524 | return IO::File->new($file, O_RDONLY, @_); | |
2063d380 WB |
525 | } |
526 | ||
f08b2779 | 527 | sub ct_open_file_write { |
2063d380 | 528 | my $self = shift; |
f08b2779 | 529 | my $file = shift; |
bd1dc8d1 | 530 | $file = '/dev/null' if $self->ct_is_file_ignored($file); |
c6a605f9 WB |
531 | my $fh = IO::File->new($file, O_WRONLY | O_CREAT, @_); |
532 | $self->ct_reset_ownership($fh); | |
533 | return $fh; | |
2063d380 WB |
534 | } |
535 | ||
f08b2779 | 536 | sub ct_make_path { |
2063d380 | 537 | my $self = shift; |
c6a605f9 WB |
538 | if ($self->{id_map}) { |
539 | my $opts = pop; | |
540 | if (ref($opts) eq 'HASH') { | |
541 | $opts->{owner} = $self->{rootuid} if !defined($self->{owner}); | |
542 | $opts->{group} = $self->{rootgid} if !defined($self->{group}); | |
543 | } | |
544 | File::Path::make_path(@_, $opts); | |
545 | } else { | |
546 | File::Path::make_path(@_); | |
547 | } | |
2063d380 WB |
548 | } |
549 | ||
550 | sub ct_symlink { | |
551 | my ($self, $old, $new) = @_; | |
bd1dc8d1 | 552 | return if $self->ct_is_file_ignored($new); |
f08b2779 | 553 | return CORE::symlink($old, $new); |
2063d380 WB |
554 | } |
555 | ||
8f115f7c WB |
556 | sub ct_readlink { |
557 | my ($self, $name) = @_; | |
558 | return CORE::readlink($name); | |
559 | } | |
560 | ||
2063d380 WB |
561 | sub ct_file_exists { |
562 | my ($self, $file) = @_; | |
f08b2779 WB |
563 | return -f $file; |
564 | } | |
565 | ||
566 | sub ct_is_directory { | |
567 | my ($self, $file) = @_; | |
568 | return -d $file; | |
569 | } | |
570 | ||
571 | sub ct_is_symlink { | |
572 | my ($self, $file) = @_; | |
573 | return -l $file; | |
574 | } | |
575 | ||
e7deac01 WB |
576 | sub ct_is_executable { |
577 | my ($self, $file) = @_; | |
578 | return -x $file | |
579 | } | |
580 | ||
f08b2779 WB |
581 | sub ct_stat { |
582 | my ($self, $file) = @_; | |
583 | return File::stat::stat($file); | |
2063d380 WB |
584 | } |
585 | ||
586 | sub ct_file_read_firstline { | |
587 | my ($self, $file) = @_; | |
f08b2779 | 588 | return PVE::Tools::file_read_firstline($file); |
2063d380 WB |
589 | } |
590 | ||
591 | sub ct_file_get_contents { | |
592 | my ($self, $file) = @_; | |
f08b2779 | 593 | return PVE::Tools::file_get_contents($file); |
2063d380 WB |
594 | } |
595 | ||
596 | sub ct_file_set_contents { | |
39243220 | 597 | my ($self, $file, $data, $perms) = @_; |
bd1dc8d1 | 598 | return if $self->ct_is_file_ignored($file); |
c6a605f9 WB |
599 | PVE::Tools::file_set_contents($file, $data, $perms); |
600 | $self->ct_reset_ownership($file); | |
2063d380 WB |
601 | } |
602 | ||
2edb50e5 WB |
603 | # Modify a marked portion of a file. |
604 | # Optionally if the file becomes empty it will be deleted. | |
605 | sub ct_modify_file { | |
606 | my ($self, $file, $data, %options) = @_; | |
bd1dc8d1 | 607 | return if $self->ct_is_file_ignored($file); |
2edb50e5 WB |
608 | |
609 | my $head = "# --- BEGIN PVE ---\n"; | |
610 | my $tail = "# --- END PVE ---\n"; | |
be7ee97a | 611 | my $perms = $options{perms}; |
a5a4b5aa | 612 | $data .= "\n" if $data && $data !~ /\n$/; |
2edb50e5 WB |
613 | |
614 | if (!$self->ct_file_exists($file)) { | |
be7ee97a | 615 | $self->ct_file_set_contents($file, $head.$data.$tail, $perms) if $data; |
2edb50e5 WB |
616 | return; |
617 | } | |
618 | ||
619 | my $old = $self->ct_file_get_contents($file); | |
620 | my @lines = split(/\n/, $old); | |
621 | ||
622 | my ($beg, $end); | |
623 | foreach my $i (0..(@lines-1)) { | |
624 | my $line = $lines[$i]; | |
625 | $beg = $i if !defined($beg) && | |
626 | $line =~ /^#\s*---\s*BEGIN\s*PVE\s*/; | |
627 | $end = $i if !defined($end) && defined($beg) && | |
628 | $line =~ /^#\s*---\s*END\s*PVE\s*/i; | |
629 | last if defined($beg) && defined($end); | |
630 | } | |
631 | ||
632 | if (defined($beg) && defined($end)) { | |
633 | # Found a section | |
634 | if ($data) { | |
635 | chomp $tail; | |
636 | splice @lines, $beg, $end-$beg+1, $head.$data.$tail; | |
fa7cb12b | 637 | } else { |
2edb50e5 WB |
638 | if ($beg == 0 && $end == (@lines-1)) { |
639 | $self->ct_unlink($file) if $options{delete}; | |
640 | return; | |
641 | } | |
642 | splice @lines, $beg, $end-$beg+1, $head.$data.$tail; | |
643 | } | |
644 | $self->ct_file_set_contents($file, join("\n", @lines) . "\n"); | |
645 | } elsif ($data) { | |
646 | # No section found | |
647 | my $content = join("\n", @lines); | |
648 | chomp $content; | |
649 | if (!$content && !$data && $options{delete}) { | |
fa7cb12b | 650 | $self->ct_unlink($file); |
2edb50e5 WB |
651 | return; |
652 | } | |
653 | $content .= "\n"; | |
654 | $data = $head.$data.$tail; | |
655 | if ($options{replace}) { | |
be7ee97a | 656 | $self->ct_file_set_contents($file, $data, $perms); |
2edb50e5 | 657 | } elsif ($options{prepend}) { |
be7ee97a | 658 | $self->ct_file_set_contents($file, $data . $content, $perms); |
2edb50e5 | 659 | } else { # append |
be7ee97a | 660 | $self->ct_file_set_contents($file, $content . $data, $perms); |
fa7cb12b | 661 | } |
fa7cb12b WB |
662 | } |
663 | } | |
664 | ||
9096a91d WB |
665 | sub remove_pve_sections { |
666 | my ($data) = @_; | |
667 | ||
668 | my $head = "# --- BEGIN PVE ---"; | |
669 | my $tail = "# --- END PVE ---"; | |
670 | ||
671 | # Remove the sections enclosed with the above headers and footers. | |
672 | # from a line (^) starting with '\h*$head' | |
673 | # to a line (the other ^) starting with '\h*$tail' up to including that | |
674 | # line's end (.*?$). | |
675 | return $data =~ s/^\h*\Q$head\E.*^\h*\Q$tail\E.*?$//rgms; | |
676 | } | |
677 | ||
1c7f4f65 | 678 | 1; |