]>
Commit | Line | Data |
---|---|---|
fe000966 DM |
1 | package PVE::Cluster; |
2 | ||
3 | use strict; | |
7181f622 | 4 | use warnings; |
57d8355e | 5 | |
57d8355e | 6 | use Encode; |
fe000966 | 7 | use File::stat qw(); |
462c16b7 | 8 | use File::Path qw(make_path); |
57d8355e | 9 | use JSON; |
26784563 | 10 | use Net::SSLeay; |
c5204e14 | 11 | use POSIX qw(ENOENT); |
57d8355e TL |
12 | use Socket; |
13 | use Storable qw(dclone); | |
57d8355e | 14 | |
c92b7716 | 15 | use PVE::Certificate; |
fe000966 DM |
16 | use PVE::INotify; |
17 | use PVE::IPCC; | |
d0ad18e8 | 18 | use PVE::JSONSchema; |
54d487bf | 19 | use PVE::Network; |
57d8355e TL |
20 | use PVE::SafeSyslog; |
21 | use PVE::Tools qw(run_command); | |
22 | ||
95c44445 | 23 | use PVE::Cluster::IPCConst; |
57d8355e | 24 | |
fe000966 DM |
25 | use base 'Exporter'; |
26 | ||
27 | our @EXPORT_OK = qw( | |
28 | cfs_read_file | |
29 | cfs_write_file | |
30 | cfs_register_file | |
31 | cfs_lock_file); | |
32 | ||
fe000966 DM |
33 | # x509 certificate utils |
34 | ||
35 | my $basedir = "/etc/pve"; | |
36 | my $authdir = "$basedir/priv"; | |
37 | my $lockdir = "/etc/pve/priv/lock"; | |
38 | ||
68491de3 | 39 | # cfs and corosync files |
e02bdaae TL |
40 | my $dbfile = "/var/lib/pve-cluster/config.db"; |
41 | my $dbbackupdir = "/var/lib/pve-cluster/backup"; | |
fe000966 | 42 | |
34180e03 TL |
43 | # this is just a readonly copy, the relevant one is in status.c from pmxcfs |
44 | # observed files are the one we can get directly through IPCC, they are cached | |
45 | # using a computed version and only those can be used by the cfs_*_file methods | |
fe000966 | 46 | my $observed = { |
e1735a61 | 47 | 'vzdump.cron' => 1, |
581e22f4 | 48 | 'vzdump.conf' => 1, |
1fdf150e | 49 | 'jobs.cfg' => 1, |
fe000966 DM |
50 | 'storage.cfg' => 1, |
51 | 'datacenter.cfg' => 1, | |
f6de131a | 52 | 'replication.cfg' => 1, |
cafc7309 DM |
53 | 'corosync.conf' => 1, |
54 | 'corosync.conf.new' => 1, | |
cabbc27e | 55 | 'firewall/cluster.fw' => 1, |
fe000966 DM |
56 | 'user.cfg' => 1, |
57 | 'domains.cfg' => 1, | |
41ceede7 LW |
58 | 'notifications.cfg' => 1, |
59 | 'priv/notifications.cfg' => 1, | |
fe000966 | 60 | 'priv/shadow.cfg' => 1, |
454c3a2c | 61 | 'priv/tfa.cfg' => 1, |
7dac5e0e | 62 | 'priv/token.cfg' => 1, |
8b84a18a | 63 | 'priv/acme/plugins.cfg' => 1, |
fc28c2f8 | 64 | 'priv/ipam.db' => 1, |
2b2ecfe8 | 65 | 'priv/macs.db' => 1, |
fe000966 | 66 | '/qemu-server/' => 1, |
f71eee41 | 67 | '/openvz/' => 1, |
7f66b436 | 68 | '/lxc/' => 1, |
5a5417e6 DM |
69 | 'ha/crm_commands' => 1, |
70 | 'ha/manager_status' => 1, | |
71 | 'ha/resources.cfg' => 1, | |
72 | 'ha/groups.cfg' => 1, | |
e9af3eb7 | 73 | 'ha/fence.cfg' => 1, |
9d4f69ff | 74 | 'status.cfg' => 1, |
22e2ed76 | 75 | 'ceph.conf' => 1, |
1bb7107a | 76 | 'sdn/vnets.cfg' => 1, |
1bb7107a | 77 | 'sdn/zones.cfg' => 1, |
1bb7107a | 78 | 'sdn/controllers.cfg' => 1, |
c20823f8 | 79 | 'sdn/subnets.cfg' => 1, |
be1aa34b | 80 | 'sdn/ipams.cfg' => 1, |
9d3ea5ef | 81 | 'sdn/dns.cfg' => 1, |
a3d44df8 | 82 | 'sdn/.running-config' => 1, |
9bb0fbe9 | 83 | 'virtual-guest/cpu-models.conf' => 1, |
815e6ac1 DC |
84 | 'mapping/pci.cfg' => 1, |
85 | 'mapping/usb.cfg' => 1, | |
fe000966 DM |
86 | }; |
87 | ||
462c16b7 TL |
88 | sub prepare_observed_file_basedirs { |
89 | ||
393742f6 | 90 | if (!check_cfs_is_mounted(1)) { |
462c16b7 TL |
91 | warn "pmxcfs isn't mounted (/etc/pve), chickening out..\n"; |
92 | return; | |
93 | } | |
94 | ||
95 | for my $f (sort keys %$observed) { | |
96 | next if $f !~ m!^(.*)/[^/]+$!; | |
97 | my $dir = "$basedir/$1"; | |
98 | next if -e $dir; # can also be a link, so just use -e xist check | |
98cfaf44 | 99 | print "creating directory '$dir' for observed files\n"; |
462c16b7 TL |
100 | make_path($dir); |
101 | } | |
102 | } | |
103 | ||
c5204e14 FG |
104 | sub base_dir { |
105 | return $basedir; | |
106 | } | |
fe000966 | 107 | |
c5204e14 FG |
108 | sub auth_dir { |
109 | return $authdir; | |
fe000966 DM |
110 | } |
111 | ||
112 | sub check_cfs_quorum { | |
01dddfb9 DM |
113 | my ($noerr) = @_; |
114 | ||
fe000966 DM |
115 | # note: -w filename always return 1 for root, so wee need |
116 | # to use File::lstat here | |
117 | my $st = File::stat::lstat("$basedir/local"); | |
01dddfb9 DM |
118 | my $quorate = ($st && (($st->mode & 0200) != 0)); |
119 | ||
120 | die "cluster not ready - no quorum?\n" if !$quorate && !$noerr; | |
121 | ||
122 | return $quorate; | |
fe000966 DM |
123 | } |
124 | ||
125 | sub check_cfs_is_mounted { | |
126 | my ($noerr) = @_; | |
127 | ||
128 | my $res = -l "$basedir/local"; | |
129 | ||
4f26cc86 | 130 | die "pve configuration filesystem (pmxcfs) not mounted\n" if !$res && !$noerr; |
fe000966 DM |
131 | |
132 | return $res; | |
133 | } | |
134 | ||
fe000966 DM |
135 | my $versions = {}; |
136 | my $vmlist = {}; | |
137 | my $clinfo = {}; | |
138 | ||
139 | my $ipcc_send_rec = sub { | |
140 | my ($msgid, $data) = @_; | |
141 | ||
142 | my $res = PVE::IPCC::ipcc_send_rec($msgid, $data); | |
143 | ||
dd856b4d | 144 | die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0); |
fe000966 DM |
145 | |
146 | return $res; | |
147 | }; | |
148 | ||
149 | my $ipcc_send_rec_json = sub { | |
150 | my ($msgid, $data) = @_; | |
151 | ||
152 | my $res = PVE::IPCC::ipcc_send_rec($msgid, $data); | |
153 | ||
dd856b4d | 154 | die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0); |
fe000966 DM |
155 | |
156 | return decode_json($res); | |
157 | }; | |
158 | ||
159 | my $ipcc_get_config = sub { | |
160 | my ($path) = @_; | |
161 | ||
162 | my $bindata = pack "Z*", $path; | |
95c44445 | 163 | my $res = PVE::IPCC::ipcc_send_rec(CFS_IPC_GET_CONFIG, $bindata); |
2db32d95 | 164 | if (!defined($res)) { |
7bac9ca5 WB |
165 | if ($! != 0) { |
166 | return undef if $! == ENOENT; | |
167 | die "$!\n"; | |
168 | } | |
2db32d95 DM |
169 | return ''; |
170 | } | |
171 | ||
172 | return $res; | |
fe000966 DM |
173 | }; |
174 | ||
175 | my $ipcc_get_status = sub { | |
176 | my ($name, $nodename) = @_; | |
177 | ||
178 | my $bindata = pack "Z[256]Z[256]", $name, ($nodename || ""); | |
95c44445 | 179 | return PVE::IPCC::ipcc_send_rec(CFS_IPC_GET_STATUS, $bindata); |
fe000966 DM |
180 | }; |
181 | ||
71cc17bc DC |
182 | my $ipcc_remove_status = sub { |
183 | my ($name) = @_; | |
0adf0c5a TL |
184 | # we just omit the data payload, pmxcfs takes this as hint and removes this |
185 | # key from the status hashtable | |
71cc17bc DC |
186 | my $bindata = pack "Z[256]", $name; |
187 | return &$ipcc_send_rec(CFS_IPC_SET_STATUS, $bindata); | |
188 | }; | |
189 | ||
fe000966 DM |
190 | my $ipcc_update_status = sub { |
191 | my ($name, $data) = @_; | |
192 | ||
193 | my $raw = ref($data) ? encode_json($data) : $data; | |
194 | # update status | |
195 | my $bindata = pack "Z[256]Z*", $name, $raw; | |
196 | ||
95c44445 | 197 | return &$ipcc_send_rec(CFS_IPC_SET_STATUS, $bindata); |
fe000966 DM |
198 | }; |
199 | ||
200 | my $ipcc_log = sub { | |
201 | my ($priority, $ident, $tag, $msg) = @_; | |
202 | ||
203 | my $bindata = pack "CCCZ*Z*Z*", $priority, bytes::length($ident) + 1, | |
204 | bytes::length($tag) + 1, $ident, $tag, $msg; | |
205 | ||
95c44445 | 206 | return &$ipcc_send_rec(CFS_IPC_LOG_CLUSTER_MSG, $bindata); |
fe000966 DM |
207 | }; |
208 | ||
209 | my $ipcc_get_cluster_log = sub { | |
210 | my ($user, $max) = @_; | |
211 | ||
212 | $max = 0 if !defined($max); | |
213 | ||
214 | my $bindata = pack "VVVVZ*", $max, 0, 0, 0, ($user || ""); | |
95c44445 | 215 | return &$ipcc_send_rec(CFS_IPC_GET_CLUSTER_LOG, $bindata); |
fe000966 DM |
216 | }; |
217 | ||
7dac5e0e FG |
218 | my $ipcc_verify_token = sub { |
219 | my ($full_token) = @_; | |
220 | ||
221 | my $bindata = pack "Z*", $full_token; | |
222 | my $res = PVE::IPCC::ipcc_send_rec(CFS_IPC_VERIFY_TOKEN, $bindata); | |
223 | ||
224 | return 1 if $! == 0; | |
225 | return 0 if $! == ENOENT; | |
226 | ||
227 | die "$!\n"; | |
228 | }; | |
229 | ||
fe000966 DM |
230 | my $ccache = {}; |
231 | ||
232 | sub cfs_update { | |
686801e4 | 233 | my ($fail) = @_; |
fe000966 | 234 | eval { |
95c44445 | 235 | my $res = &$ipcc_send_rec_json(CFS_IPC_GET_FS_VERSION); |
fe000966 DM |
236 | die "no starttime\n" if !$res->{starttime}; |
237 | ||
238 | if (!$res->{starttime} || !$versions->{starttime} || | |
239 | $res->{starttime} != $versions->{starttime}) { | |
240 | #print "detected changed starttime\n"; | |
241 | $vmlist = {}; | |
242 | $clinfo = {}; | |
243 | $ccache = {}; | |
244 | } | |
245 | ||
246 | $versions = $res; | |
247 | }; | |
248 | my $err = $@; | |
249 | if ($err) { | |
250 | $versions = {}; | |
251 | $vmlist = {}; | |
252 | $clinfo = {}; | |
253 | $ccache = {}; | |
686801e4 | 254 | die $err if $fail; |
fe000966 DM |
255 | warn $err; |
256 | } | |
257 | ||
258 | eval { | |
259 | if (!$clinfo->{version} || $clinfo->{version} != $versions->{clinfo}) { | |
260 | #warn "detected new clinfo\n"; | |
95c44445 | 261 | $clinfo = &$ipcc_send_rec_json(CFS_IPC_GET_CLUSTER_INFO); |
fe000966 DM |
262 | } |
263 | }; | |
264 | $err = $@; | |
265 | if ($err) { | |
266 | $clinfo = {}; | |
686801e4 | 267 | die $err if $fail; |
fe000966 DM |
268 | warn $err; |
269 | } | |
270 | ||
271 | eval { | |
272 | if (!$vmlist->{version} || $vmlist->{version} != $versions->{vmlist}) { | |
273 | #warn "detected new vmlist1\n"; | |
95c44445 | 274 | $vmlist = &$ipcc_send_rec_json(CFS_IPC_GET_GUEST_LIST); |
fe000966 DM |
275 | } |
276 | }; | |
277 | $err = $@; | |
278 | if ($err) { | |
279 | $vmlist = {}; | |
686801e4 | 280 | die $err if $fail; |
fe000966 DM |
281 | warn $err; |
282 | } | |
283 | } | |
284 | ||
285 | sub get_vmlist { | |
286 | return $vmlist; | |
287 | } | |
288 | ||
289 | sub get_clinfo { | |
290 | return $clinfo; | |
291 | } | |
292 | ||
9ddd4ae9 DM |
293 | sub get_members { |
294 | return $clinfo->{nodelist}; | |
295 | } | |
296 | ||
fe000966 | 297 | sub get_nodelist { |
fe000966 DM |
298 | my $nodelist = $clinfo->{nodelist}; |
299 | ||
fe000966 DM |
300 | my $nodename = PVE::INotify::nodename(); |
301 | ||
302 | if (!$nodelist || !$nodelist->{$nodename}) { | |
303 | return [ $nodename ]; | |
304 | } | |
305 | ||
306 | return [ keys %$nodelist ]; | |
307 | } | |
308 | ||
0adf0c5a TL |
309 | # only stored in a in-memory hashtable inside pmxcfs, local data is gone after |
310 | # a restart (of pmxcfs or the node), peer data is still available then | |
311 | # best used for status data, like running (ceph) services, package versions, ... | |
a26b18e1 DC |
312 | sub broadcast_node_kv { |
313 | my ($key, $data) = @_; | |
314 | ||
315 | if (!defined($data)) { | |
4d04cad0 | 316 | eval { $ipcc_remove_status->("kv/$key") }; |
a26b18e1 DC |
317 | } else { |
318 | die "cannot send a reference\n" if ref($data); | |
319 | my $size = length($data); | |
0adf0c5a | 320 | die "data for '$key' too big\n" if $size >= (32 * 1024); # limit from pmxfs |
a26b18e1 | 321 | |
4d04cad0 | 322 | eval { $ipcc_update_status->("kv/$key", $data) }; |
a26b18e1 | 323 | } |
a26b18e1 DC |
324 | warn $@ if $@; |
325 | } | |
326 | ||
0adf0c5a | 327 | # nodename is optional |
a26b18e1 DC |
328 | sub get_node_kv { |
329 | my ($key, $nodename) = @_; | |
330 | ||
331 | my $res = {}; | |
332 | my $get_node_data = sub { | |
333 | my ($node) = @_; | |
334 | my $raw = $ipcc_get_status->("kv/$key", $node); | |
14000cd7 | 335 | $res->{$node} = unpack("Z*", $raw) if $raw; |
a26b18e1 DC |
336 | }; |
337 | ||
338 | if ($nodename) { | |
339 | $get_node_data->($nodename); | |
340 | } else { | |
4d04cad0 | 341 | for my $node (get_nodelist()->@*) { |
a26b18e1 DC |
342 | $get_node_data->($node); |
343 | } | |
344 | } | |
345 | ||
346 | return $res; | |
347 | } | |
348 | ||
3e9a2ecb DC |
349 | # properties: an array-ref of config properties you want to get, e.g., this |
350 | # is perfect to get multiple properties of a guest _fast_ | |
351 | # (>100 faster than manual parsing here) | |
352 | # vmid: optional, if a valid is passed we only check that one, else return all | |
353 | # NOTE: does *not* searches snapshot and PENDING entries sections! | |
354 | # NOTE: returns the guest config lines (excluding trailing whitespace) as is, | |
355 | # so for non-trivial properties, checking the validity must be done | |
356 | # NOTE: no permission check is done, that is the responsibilty of the caller | |
357 | sub get_guest_config_properties { | |
358 | my ($properties, $vmid) = @_; | |
359 | ||
360 | die "properties required" if !defined($properties); | |
361 | ||
362 | my $num_props = scalar(@$properties); | |
363 | die "only up to 255 properties supported" if $num_props > 255; | |
364 | my $bindata = pack "VC", $vmid // 0, $num_props; | |
365 | for my $property (@$properties) { | |
366 | $bindata .= pack "Z*", $property; | |
367 | } | |
368 | my $res = $ipcc_send_rec_json->(CFS_IPC_GET_GUEST_CONFIG_PROPERTIES, $bindata); | |
369 | ||
370 | return $res; | |
371 | } | |
372 | ||
cf1b19d9 TL |
373 | # property: a config property you want to get, e.g., this is perfect to get |
374 | # the 'lock' entry of a guest _fast_ (>100 faster than manual parsing here) | |
8a93eeae | 375 | # vmid: optional, if a valid is passed we only check that one, else return all |
cf1b19d9 | 376 | # NOTE: does *not* searches snapshot and PENDING entries sections! |
3e9a2ecb DC |
377 | # NOTE: returns the guest config lines (excluding trailing whitespace) as is, |
378 | # so for non-trivial properties, checking the validity must be done | |
379 | # NOTE: no permission check is done, that is the responsibilty of the caller | |
cf1b19d9 TL |
380 | sub get_guest_config_property { |
381 | my ($property, $vmid) = @_; | |
382 | ||
383 | die "property is required" if !defined($property); | |
384 | ||
385 | my $bindata = pack "VZ*", $vmid // 0, $property; | |
386 | my $res = $ipcc_send_rec_json->(CFS_IPC_GET_GUEST_CONFIG_PROPERTY, $bindata); | |
387 | ||
388 | return $res; | |
389 | } | |
390 | ||
1b36b6b1 | 391 | # $data must be a chronological descending ordered array of tasks |
fe000966 DM |
392 | sub broadcast_tasklist { |
393 | my ($data) = @_; | |
394 | ||
4f26cc86 TL |
395 | # the serialized list may not get bigger than 128 KiB (CFS_MAX_STATUS_SIZE from pmxcfs) |
396 | # drop older items until we satisfy this constraint | |
1b36b6b1 | 397 | my $size = length(encode_json($data)); |
4f26cc86 | 398 | while ($size >= (32 * 1024)) { # TODO: update to 128 KiB in PVE 8.x |
1b36b6b1 TL |
399 | pop @$data; |
400 | $size = length(encode_json($data)); | |
401 | } | |
402 | ||
4f26cc86 | 403 | eval { $ipcc_update_status->("tasklist", $data) }; |
fe000966 DM |
404 | warn $@ if $@; |
405 | } | |
406 | ||
407 | my $tasklistcache = {}; | |
408 | ||
409 | sub get_tasklist { | |
410 | my ($nodename) = @_; | |
411 | ||
412 | my $kvstore = $versions->{kvstore} || {}; | |
413 | ||
414 | my $nodelist = get_nodelist(); | |
415 | ||
416 | my $res = []; | |
417 | foreach my $node (@$nodelist) { | |
418 | next if $nodename && ($nodename ne $node); | |
419 | eval { | |
deeedc09 | 420 | my $ver = exists $kvstore->{$node} ? $kvstore->{$node}->{tasklist} : undef; |
1f1c5c43 TL |
421 | my $cache = $tasklistcache->{$node}; |
422 | if (!$cache || !$ver || !$cache->{version} || ($cache->{version} != $ver)) { | |
423 | my $tasks = []; | |
424 | if (my $raw = $ipcc_get_status->("tasklist", $node)) { | |
aea0f5a6 TL |
425 | my $json_str = unpack("Z*", $raw); |
426 | $tasks = decode_json($json_str); | |
1f1c5c43 TL |
427 | } |
428 | push @$res, @$tasks; | |
429 | $tasklistcache->{$node} = { | |
430 | data => $tasks, | |
fe000966 DM |
431 | version => $ver, |
432 | }; | |
1f1c5c43 TL |
433 | } elsif ($cache && $cache->{data}) { |
434 | push @$res, $cache->{data}->@*; | |
fe000966 DM |
435 | } |
436 | }; | |
437 | my $err = $@; | |
438 | syslog('err', $err) if $err; | |
439 | } | |
440 | ||
441 | return $res; | |
442 | } | |
443 | ||
444 | sub broadcast_rrd { | |
445 | my ($rrdid, $data) = @_; | |
446 | ||
447 | eval { | |
448 | &$ipcc_update_status("rrd/$rrdid", $data); | |
449 | }; | |
450 | my $err = $@; | |
451 | ||
452 | warn $err if $err; | |
453 | } | |
454 | ||
455 | my $last_rrd_dump = 0; | |
456 | my $last_rrd_data = ""; | |
457 | ||
458 | sub rrd_dump { | |
459 | ||
460 | my $ctime = time(); | |
461 | ||
462 | my $diff = $ctime - $last_rrd_dump; | |
463 | if ($diff < 2) { | |
464 | return $last_rrd_data; | |
465 | } | |
466 | ||
467 | my $raw; | |
468 | eval { | |
95c44445 | 469 | $raw = &$ipcc_send_rec(CFS_IPC_GET_RRD_DUMP); |
fe000966 DM |
470 | }; |
471 | my $err = $@; | |
472 | ||
473 | if ($err) { | |
474 | warn $err; | |
475 | return {}; | |
476 | } | |
477 | ||
478 | my $res = {}; | |
479 | ||
c3fabca7 DM |
480 | if ($raw) { |
481 | while ($raw =~ s/^(.*)\n//) { | |
482 | my ($key, @ela) = split(/:/, $1); | |
483 | next if !$key; | |
484 | next if !(scalar(@ela) > 1); | |
d2aae33e | 485 | $res->{$key} = [ map { $_ eq 'U' ? undef : $_ } @ela ]; |
c3fabca7 | 486 | } |
fe000966 DM |
487 | } |
488 | ||
489 | $last_rrd_dump = $ctime; | |
490 | $last_rrd_data = $res; | |
491 | ||
492 | return $res; | |
493 | } | |
494 | ||
fe000966 DM |
495 | |
496 | # a fast way to read files (avoid fuse overhead) | |
497 | sub get_config { | |
498 | my ($path) = @_; | |
499 | ||
d3a92ba7 | 500 | return &$ipcc_get_config($path); |
fe000966 DM |
501 | } |
502 | ||
503 | sub get_cluster_log { | |
504 | my ($user, $max) = @_; | |
505 | ||
506 | return &$ipcc_get_cluster_log($user, $max); | |
507 | } | |
508 | ||
7dac5e0e FG |
509 | sub verify_token { |
510 | my ($userid, $token) = @_; | |
511 | ||
512 | return &$ipcc_verify_token("$userid $token"); | |
513 | } | |
514 | ||
fe000966 DM |
515 | my $file_info = {}; |
516 | ||
517 | sub cfs_register_file { | |
518 | my ($filename, $parser, $writer) = @_; | |
519 | ||
520 | $observed->{$filename} || die "unknown file '$filename'"; | |
521 | ||
522 | die "file '$filename' already registered" if $file_info->{$filename}; | |
523 | ||
524 | $file_info->{$filename} = { | |
525 | parser => $parser, | |
526 | writer => $writer, | |
527 | }; | |
528 | } | |
529 | ||
530 | my $ccache_read = sub { | |
531 | my ($filename, $parser, $version) = @_; | |
532 | ||
533 | $ccache->{$filename} = {} if !$ccache->{$filename}; | |
534 | ||
535 | my $ci = $ccache->{$filename}; | |
536 | ||
d3a92ba7 | 537 | if (!$ci->{version} || !$version || $ci->{version} != $version) { |
a79c7743 | 538 | # we always call the parser, even when the file does not exist |
d3a92ba7 | 539 | # (in that case $data is undef) |
fe000966 | 540 | my $data = get_config($filename); |
fe000966 DM |
541 | $ci->{data} = &$parser("/etc/pve/$filename", $data); |
542 | $ci->{version} = $version; | |
543 | } | |
544 | ||
545 | my $res = ref($ci->{data}) ? dclone($ci->{data}) : $ci->{data}; | |
546 | ||
547 | return $res; | |
548 | }; | |
549 | ||
550 | sub cfs_file_version { | |
551 | my ($filename) = @_; | |
552 | ||
553 | my $version; | |
554 | my $infotag; | |
6e73d5c2 | 555 | if ($filename =~ m!^nodes/[^/]+/(openvz|lxc|qemu-server)/(\d+)\.conf$!) { |
f71eee41 | 556 | my ($type, $vmid) = ($1, $2); |
fe000966 DM |
557 | if ($vmlist && $vmlist->{ids} && $vmlist->{ids}->{$vmid}) { |
558 | $version = $vmlist->{ids}->{$vmid}->{version}; | |
559 | } | |
f71eee41 | 560 | $infotag = "/$type/"; |
fe000966 DM |
561 | } else { |
562 | $infotag = $filename; | |
563 | $version = $versions->{$filename}; | |
564 | } | |
565 | ||
566 | my $info = $file_info->{$infotag} || | |
567 | die "unknown file type '$filename'\n"; | |
568 | ||
569 | return wantarray ? ($version, $info) : $version; | |
570 | } | |
571 | ||
572 | sub cfs_read_file { | |
573 | my ($filename) = @_; | |
574 | ||
c53b111f | 575 | my ($version, $info) = cfs_file_version($filename); |
fe000966 DM |
576 | my $parser = $info->{parser}; |
577 | ||
578 | return &$ccache_read($filename, $parser, $version); | |
579 | } | |
580 | ||
581 | sub cfs_write_file { | |
90c824ba | 582 | my ($filename, $data, $force_utf8) = @_; |
fe000966 | 583 | |
c53b111f | 584 | my ($version, $info) = cfs_file_version($filename); |
fe000966 DM |
585 | |
586 | my $writer = $info->{writer} || die "no writer defined"; | |
587 | ||
588 | my $fsname = "/etc/pve/$filename"; | |
589 | ||
590 | my $raw = &$writer($fsname, $data); | |
591 | ||
592 | if (my $ci = $ccache->{$filename}) { | |
593 | $ci->{version} = undef; | |
594 | } | |
595 | ||
90c824ba | 596 | PVE::Tools::file_set_contents($fsname, $raw, undef, 1); |
fe000966 DM |
597 | } |
598 | ||
599 | my $cfs_lock = sub { | |
600 | my ($lockid, $timeout, $code, @param) = @_; | |
601 | ||
00ea8428 TL |
602 | my $prev_alarm = alarm(0); # suspend outer alarm early |
603 | ||
fe000966 | 604 | my $res; |
bcdb1b3a | 605 | my $got_lock = 0; |
fe000966 | 606 | |
e8c6be91 | 607 | # this timeout is for acquire the lock |
fe000966 DM |
608 | $timeout = 10 if !$timeout; |
609 | ||
610 | my $filename = "$lockdir/$lockid"; | |
611 | ||
e085fe6f | 612 | my $is_code_err = 0; |
fe000966 DM |
613 | eval { |
614 | ||
615 | mkdir $lockdir; | |
616 | ||
617 | if (! -d $lockdir) { | |
6e13e20a | 618 | die "pve cluster filesystem not online.\n"; |
fe000966 DM |
619 | } |
620 | ||
6e13e20a | 621 | my $timeout_err = sub { die "got lock request timeout\n"; }; |
bcdb1b3a | 622 | local $SIG{ALRM} = $timeout_err; |
fe000966 | 623 | |
bcdb1b3a TL |
624 | while (1) { |
625 | alarm ($timeout); | |
626 | $got_lock = mkdir($filename); | |
3fb23b5b | 627 | $timeout = alarm(0) - 1; # we'll sleep for 1s, see down below |
bcdb1b3a TL |
628 | |
629 | last if $got_lock; | |
630 | ||
3fb23b5b | 631 | $timeout_err->() if $timeout <= 0; |
fe000966 | 632 | |
e8c6be91 | 633 | print STDERR "trying to acquire cfs lock '$lockid' ...\n"; |
bcdb1b3a TL |
634 | utime (0, 0, $filename); # cfs unlock request |
635 | sleep(1); | |
fe000966 DM |
636 | } |
637 | ||
638 | # fixed command timeout: cfs locks have a timeout of 120 | |
639 | # using 60 gives us another 60 seconds to abort the task | |
e085fe6f | 640 | local $SIG{ALRM} = sub { die "'$lockid'-locked command timed out - aborting\n"; }; |
00ea8428 | 641 | alarm(60); |
fe000966 | 642 | |
9c206b2b DM |
643 | cfs_update(); # make sure we read latest versions inside code() |
644 | ||
e085fe6f TL |
645 | $is_code_err = 1; # allows to differ between locking and actual-work errors |
646 | ||
fe000966 DM |
647 | $res = &$code(@param); |
648 | ||
649 | alarm(0); | |
650 | }; | |
651 | ||
652 | my $err = $@; | |
653 | ||
6e13e20a | 654 | $err = "no quorum!\n" if !$got_lock && !check_cfs_quorum(1); |
fe000966 | 655 | |
96857ee4 | 656 | rmdir $filename if $got_lock; # if we held the lock always unlock again |
fe000966 | 657 | |
00ea8428 TL |
658 | alarm($prev_alarm); |
659 | ||
fe000966 | 660 | if ($err) { |
e085fe6f | 661 | if (ref($err) eq 'PVE::Exception' || $is_code_err) { |
c09b0af5 FG |
662 | # re-raise defined exceptions |
663 | $@ = $err; | |
664 | } else { | |
e085fe6f TL |
665 | # add lock info for plain errors comming from the locking itself |
666 | $@ = "cfs-lock '$lockid' error: $err"; | |
c09b0af5 | 667 | } |
fe000966 DM |
668 | return undef; |
669 | } | |
670 | ||
671 | $@ = undef; | |
672 | ||
673 | return $res; | |
674 | }; | |
675 | ||
676 | sub cfs_lock_file { | |
677 | my ($filename, $timeout, $code, @param) = @_; | |
678 | ||
679 | my $info = $observed->{$filename} || die "unknown file '$filename'"; | |
680 | ||
681 | my $lockid = "file-$filename"; | |
682 | $lockid =~ s/[.\/]/_/g; | |
683 | ||
684 | &$cfs_lock($lockid, $timeout, $code, @param); | |
685 | } | |
686 | ||
687 | sub cfs_lock_storage { | |
688 | my ($storeid, $timeout, $code, @param) = @_; | |
689 | ||
690 | my $lockid = "storage-$storeid"; | |
691 | ||
692 | &$cfs_lock($lockid, $timeout, $code, @param); | |
693 | } | |
694 | ||
78897707 TL |
695 | sub cfs_lock_domain { |
696 | my ($domainname, $timeout, $code, @param) = @_; | |
697 | ||
698 | my $lockid = "domain-$domainname"; | |
699 | ||
700 | &$cfs_lock($lockid, $timeout, $code, @param); | |
701 | } | |
702 | ||
4790f9f4 FG |
703 | sub cfs_lock_acme { |
704 | my ($account, $timeout, $code, @param) = @_; | |
705 | ||
706 | my $lockid = "acme-$account"; | |
707 | ||
708 | &$cfs_lock($lockid, $timeout, $code, @param); | |
709 | } | |
710 | ||
900b50d9 FG |
711 | sub cfs_lock_authkey { |
712 | my ($timeout, $code, @param) = @_; | |
713 | ||
714 | $cfs_lock->('authkey', $timeout, $code, @param); | |
31c78985 FG |
715 | } |
716 | ||
717 | sub cfs_lock_firewall { | |
718 | my ($scope, $timeout, $code, @param) = @_; | |
719 | ||
720 | my $lockid = "firewall-$scope"; | |
721 | ||
722 | $cfs_lock->($lockid, $timeout, $code, @param); | |
900b50d9 FG |
723 | } |
724 | ||
fe000966 DM |
725 | my $log_levels = { |
726 | "emerg" => 0, | |
727 | "alert" => 1, | |
728 | "crit" => 2, | |
729 | "critical" => 2, | |
730 | "err" => 3, | |
731 | "error" => 3, | |
732 | "warn" => 4, | |
733 | "warning" => 4, | |
734 | "notice" => 5, | |
735 | "info" => 6, | |
736 | "debug" => 7, | |
737 | }; | |
738 | ||
739 | sub log_msg { | |
740 | my ($priority, $ident, $msg) = @_; | |
741 | ||
742 | if (my $tmp = $log_levels->{$priority}) { | |
743 | $priority = $tmp; | |
744 | } | |
745 | ||
746 | die "need numeric log priority" if $priority !~ /^\d+$/; | |
747 | ||
748 | my $tag = PVE::SafeSyslog::tag(); | |
749 | ||
750 | $msg = "empty message" if !$msg; | |
751 | ||
752 | $ident = "" if !$ident; | |
8f2d54ff | 753 | $ident = encode("ascii", $ident, |
fe000966 DM |
754 | sub { sprintf "\\u%04x", shift }); |
755 | ||
8f2d54ff | 756 | my $ascii = encode("ascii", $msg, sub { sprintf "\\u%04x", shift }); |
fe000966 DM |
757 | |
758 | if ($ident) { | |
759 | syslog($priority, "<%s> %s", $ident, $ascii); | |
760 | } else { | |
761 | syslog($priority, "%s", $ascii); | |
762 | } | |
763 | ||
764 | eval { &$ipcc_log($priority, $ident, $tag, $ascii); }; | |
765 | ||
766 | syslog("err", "writing cluster log failed: $@") if $@; | |
767 | } | |
768 | ||
9d76a1bb DM |
769 | sub check_vmid_unused { |
770 | my ($vmid, $noerr) = @_; | |
c53b111f | 771 | |
9d76a1bb DM |
772 | my $vmlist = get_vmlist(); |
773 | ||
774 | my $d = $vmlist->{ids}->{$vmid}; | |
775 | return 1 if !defined($d); | |
c53b111f | 776 | |
9d76a1bb DM |
777 | return undef if $noerr; |
778 | ||
4f66b109 | 779 | my $vmtypestr = $d->{type} eq 'qemu' ? 'VM' : 'CT'; |
e75ccbee | 780 | die "$vmtypestr $vmid already exists on node '$d->{node}'\n"; |
9d76a1bb DM |
781 | } |
782 | ||
65ff467f DM |
783 | sub check_node_exists { |
784 | my ($nodename, $noerr) = @_; | |
785 | ||
786 | my $nodelist = $clinfo->{nodelist}; | |
787 | return 1 if $nodelist && $nodelist->{$nodename}; | |
788 | ||
789 | return undef if $noerr; | |
790 | ||
791 | die "no such cluster node '$nodename'\n"; | |
792 | } | |
793 | ||
fe000966 DM |
794 | # this is also used to get the IP of the local node |
795 | sub remote_node_ip { | |
796 | my ($nodename, $noerr) = @_; | |
797 | ||
798 | my $nodelist = $clinfo->{nodelist}; | |
799 | if ($nodelist && $nodelist->{$nodename}) { | |
800 | if (my $ip = $nodelist->{$nodename}->{ip}) { | |
fc31f517 WB |
801 | return $ip if !wantarray; |
802 | my $family = $nodelist->{$nodename}->{address_family}; | |
803 | if (!$family) { | |
804 | $nodelist->{$nodename}->{address_family} = | |
805 | $family = | |
806 | PVE::Tools::get_host_address_family($ip); | |
807 | } | |
14830160 | 808 | return wantarray ? ($ip, $family) : $ip; |
fe000966 DM |
809 | } |
810 | } | |
811 | ||
812 | # fallback: try to get IP by other means | |
e064c9b0 | 813 | return PVE::Network::get_ip_from_hostname($nodename, $noerr); |
fe000966 DM |
814 | } |
815 | ||
1904862a TL |
816 | sub get_node_fingerprint { |
817 | my ($node) = @_; | |
818 | ||
819 | my $cert_path = "/etc/pve/nodes/$node/pve-ssl.pem"; | |
820 | my $custom_cert_path = "/etc/pve/nodes/$node/pveproxy-ssl.pem"; | |
821 | ||
822 | $cert_path = $custom_cert_path if -f $custom_cert_path; | |
823 | ||
c92b7716 | 824 | return PVE::Certificate::get_certificate_fingerprint($cert_path); |
1904862a TL |
825 | } |
826 | ||
15df58e6 DM |
827 | # bash completion helpers |
828 | ||
829 | sub complete_next_vmid { | |
830 | ||
831 | my $vmlist = get_vmlist() || {}; | |
832 | my $idlist = $vmlist->{ids} || {}; | |
833 | ||
834 | for (my $i = 100; $i < 10000; $i++) { | |
835 | return [$i] if !defined($idlist->{$i}); | |
836 | } | |
837 | ||
838 | return []; | |
839 | } | |
840 | ||
87515b25 DM |
841 | sub complete_vmid { |
842 | ||
843 | my $vmlist = get_vmlist(); | |
844 | my $ids = $vmlist->{ids} || {}; | |
845 | ||
846 | return [ keys %$ids ]; | |
847 | } | |
848 | ||
15df58e6 DM |
849 | sub complete_local_vmid { |
850 | ||
851 | my $vmlist = get_vmlist(); | |
852 | my $ids = $vmlist->{ids} || {}; | |
853 | ||
854 | my $nodename = PVE::INotify::nodename(); | |
855 | ||
856 | my $res = []; | |
857 | foreach my $vmid (keys %$ids) { | |
858 | my $d = $ids->{$vmid}; | |
859 | next if !$d->{node} || $d->{node} ne $nodename; | |
860 | push @$res, $vmid; | |
861 | } | |
862 | ||
863 | return $res; | |
864 | } | |
865 | ||
4dd189df DM |
866 | sub complete_migration_target { |
867 | ||
868 | my $res = []; | |
869 | ||
870 | my $nodename = PVE::INotify::nodename(); | |
871 | ||
872 | my $nodelist = get_nodelist(); | |
873 | foreach my $node (@$nodelist) { | |
874 | next if $node eq $nodename; | |
875 | push @$res, $node; | |
876 | } | |
877 | ||
878 | return $res; | |
879 | } | |
880 | ||
1f1aef8b | 881 | |
a9965358 | 882 | # NOTE: filesystem must be offline here, no DB changes allowed |
c5204e14 | 883 | sub cfs_backup_database { |
e02bdaae TL |
884 | mkdir $dbbackupdir; |
885 | ||
e02bdaae | 886 | my $ctime = time(); |
a9965358 | 887 | my $backup_fn = "$dbbackupdir/config-$ctime.sql.gz"; |
e02bdaae | 888 | |
a9965358 | 889 | print "backup old database to '$backup_fn'\n"; |
e02bdaae | 890 | |
a9965358 TL |
891 | my $cmd = [ ['sqlite3', $dbfile, '.dump'], ['gzip', '-', \ ">${backup_fn}"] ]; |
892 | run_command($cmd, 'errmsg' => "cannot backup old database\n"); | |
e02bdaae | 893 | |
a9965358 TL |
894 | my $maxfiles = 10; # purge older backup |
895 | my $backups = [ sort { $b cmp $a } <$dbbackupdir/config-*.sql.gz> ]; | |
896 | ||
897 | if ((my $count = scalar(@$backups)) > $maxfiles) { | |
898 | foreach my $f (@$backups[$maxfiles..$count-1]) { | |
ee0daa88 | 899 | next if $f !~ m/^(\S+)$/; # untaint |
a9965358 TL |
900 | print "delete old backup '$1'\n"; |
901 | unlink $1; | |
902 | } | |
e02bdaae | 903 | } |
8f64504c | 904 | |
c5204e14 | 905 | return $dbfile; |
8f64504c | 906 | } |
e02bdaae | 907 | |
ac68281b | 908 | 1; |