]> git.proxmox.com Git - pve-container.git/blob - src/PVE/LXC/Tools.pm
f96756dc3b12b92943c7e3f454efd4ad424efb00
[pve-container.git] / src / PVE / LXC / Tools.pm
1 # Module for lxc related functionality used mostly by our hooks.
2 package PVE::LXC::Tools;
3
4 use strict;
5 use warnings;
6
7 use PVE::SafeSyslog;
8
9 # LXC introduced an `lxc.hook.version` property which allows hooks to be executed in different
10 # manners. The old way passes a lot of stuff as command line parameter, the new way passes
11 # environment variables.
12 #
13 # This is supposed to be a common entry point for hooks, consuming the parameters passed by lxc and
14 # passing them on to a subroutine in a defined way.
15 sub lxc_hook($$&) {
16 my ($expected_type, $expected_section, $code) = @_;
17
18 my ($ct_name, $section, $type);
19 my $namespaces = {};
20 my $args;
21
22 my $version = $ENV{LXC_HOOK_VERSION} // '0';
23 if ($version eq '0') {
24 # Old style hook:
25 $ct_name = shift @ARGV;
26 $section = shift @ARGV;
27 $type = shift @ARGV;
28
29 if (!defined($ct_name) || !defined($section) || !defined($type)) {
30 die "missing hook parameters, expected to be called by lxc as hook\n";
31 }
32
33 if ($ct_name !~ /^\d+$/ || $section ne $expected_section || $type ne $expected_type) {
34 return;
35 }
36
37 if ($type eq 'stop') {
38 foreach my $ns (@ARGV) {
39 if ($ns =~ /^([^:]+):(.+)$/) {
40 $namespaces->{$1} = $2;
41 } else {
42 die "unrecognized 'stop' hook parameter: $ns\n";
43 }
44 }
45 } elsif ($type eq 'clone') {
46 $args = [@ARGV];
47 }
48 } elsif ($version eq '1') {
49 $ct_name = $ENV{LXC_NAME}
50 or die "missing LXC_NAME environment variable\n";
51 $section = $ENV{LXC_HOOK_SECTION}
52 or die "missing LXC_HOOK_SECTION environment variable\n";
53 $type = $ENV{LXC_HOOK_TYPE}
54 or die "missing LXC_HOOK_TYPE environment variable\n";
55
56 if ($ct_name !~ /^\d+$/ || $section ne $expected_section || $type ne $expected_type) {
57 return;
58 }
59
60 foreach my $var (keys %$ENV) {
61 if ($var =~ /^LXC_([A-Z]+)_NS$/) {
62 $namespaces->{lc($1)} = $ENV{$1};
63 }
64 }
65 } else {
66 die "lxc.hook.version $version not supported!\n";
67 }
68
69 my $logid = $ENV{PVE_LOG_ID} || "pve-lxc-hook-$section-$type";
70 initlog($logid);
71
72 my $common_vars = {
73 ROOTFS_MOUNT => ($ENV{LXC_ROOTFS_MOUNT} or die "missing LXC_ROOTFS_MOUNT env var\n"),
74 ROOTFS_PATH => ($ENV{LXC_ROOTFS_PATH} or die "missing LXC_ROOTFS_PATH env var\n"),
75 CONFIG_FILE => ($ENV{LXC_CONFIG_FILE} or die "missing LXC_CONFIG_FILE env var\n"),
76 };
77 if (defined(my $target = $ENV{LXC_TARGET})) {
78 $common_vars->{TARGET} = $target;
79 }
80
81 $code->($ct_name, $common_vars, $namespaces, $args);
82 }
83
84 sub for_devices {
85 my ($devlist_file, $vmid, $code) = @_;
86
87 my $fd;
88
89 if (! open $fd, '<', $devlist_file) {
90 exit 0 if $!{ENOENT}; # If the list is empty the file might not exist.
91 die "failed to open device list: $!\n";
92 }
93
94 while (defined(my $line = <$fd>)) {
95 if ($line !~ m@^(b|c):(\d+):(\d+):/dev/(\S+)\s*$@) {
96 warn "invalid $devlist_file entry: $line\n";
97 next;
98 }
99
100 my ($type, $major, $minor, $dev) = ($1, $2, $3, $4);
101
102 # Don't break out of $root/dev/
103 if ($dev =~ /\.\./) {
104 warn "skipping illegal device node entry: $dev\n";
105 next;
106 }
107
108 # Never expose /dev/loop-control
109 if ($major == 10 && $minor == 237) {
110 warn "skipping illegal device entry (loop-control) for: $dev\n";
111 next;
112 }
113
114 $code->($type, $major, $minor, $dev);
115 }
116 close $fd;
117 }
118
119 sub for_current_passthrough_mounts($&) {
120 my ($vmid, $code) = @_;
121
122 my $devlist_file = "/var/lib/lxc/$vmid/passthrough/mounts";
123
124 if (-e $devlist_file) {
125 for_devices($devlist_file, $vmid, $code);
126 } else {
127 # Fallback to the old device list file in case a package upgrade
128 # occurs between lxc-pve-prestart-hook and now.
129 for_devices("/var/lib/lxc/$vmid/devices", $vmid, $code);
130 }
131 }
132
133 sub for_current_passthrough_devices($&) {
134 my ($vmid, $code) = @_;
135
136 my $passthrough_devlist_file = "/var/lib/lxc/$vmid/passthrough/devices";
137 for_devices($passthrough_devlist_file, $vmid, $code);
138 }
139
140 sub cgroup_do_write($$) {
141 my ($path, $value) = @_;
142 my $fd;
143 if (!open($fd, '>', $path)) {
144 warn "failed to open cgroup file $path: $!\n";
145 return 0;
146 }
147 if (!defined syswrite($fd, $value)) {
148 warn "failed to write value $value to cgroup file $path: $!\n";
149 return 0;
150 }
151 close($fd);
152 return 1;
153 }
154
155 # Tries to the architecture of an executable file based on its ELF header.
156 sub detect_elf_architecture {
157 my ($elf_fn) = @_;
158
159 # see https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
160
161 my $supported_elf_machine = {
162 0x03 => 'i386',
163 0x3e => 'amd64',
164 0x28 => 'armhf',
165 0xb7 => 'arm64',
166 0xf3 => 'riscv',
167 };
168
169 my $detect_arch = sub {
170 open(my $fh, "<", $elf_fn) or die "open '$elf_fn' failed: $!\n";
171 binmode($fh);
172
173 my $length = read($fh, my $data, 20) or die "read failed: $!\n";
174
175 # 4 bytes ELF magic number and 1 byte ELF class, padding, machine
176 my ($magic, $class, undef, $machine) = unpack("A4CA12n", $data);
177
178 die "'$elf_fn' does not resolve to an ELF!\n"
179 if (!defined($class) || !defined($magic) || $magic ne "\177ELF");
180
181 my $arch = $supported_elf_machine->{$machine};
182 die "'$elf_fn' has unknown ELF machine '$machine'!\n"
183 if !defined($arch);
184
185 if ($arch eq 'riscv') {
186 if ($class eq 1) {
187 $arch = 'riscv32';
188 } elsif ($class eq 2) {
189 $arch = 'riscv64';
190 } else {
191 die "'$elf_fn' has invalid class '$class'!\n";
192 }
193 }
194
195 return $arch;
196 };
197
198 my $arch = eval { PVE::Tools::run_fork_with_timeout(10, $detect_arch); };
199 my $err = $@ // "timeout\n";
200 die $err if !defined($arch);
201
202 return $arch;
203 }
204
205 1;