]>
Commit | Line | Data |
---|---|---|
94d526d7 DM |
1 | package PVE::GuestHelpers; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
2b00f623 | 6 | use PVE::Exception qw(raise_perm_exc); |
2bf45e38 | 7 | use PVE::Tools qw(split_list); |
3acb4e74 | 8 | use PVE::Storage; |
94d526d7 | 9 | |
725dcadb | 10 | use POSIX qw(strftime); |
727080ed | 11 | use Scalar::Util qw(weaken); |
725dcadb | 12 | |
aabf879e AD |
13 | my $have_sdn; |
14 | eval { | |
15 | require PVE::Network::SDN; | |
16 | $have_sdn = 1; | |
17 | }; | |
18 | ||
b2b16cee DC |
19 | use base qw(Exporter); |
20 | ||
2b00f623 DC |
21 | our @EXPORT_OK = qw( |
22 | assert_tag_permissions | |
aabf879e | 23 | check_vnet_access |
2b00f623 DC |
24 | get_allowed_tags |
25 | safe_boolean_ne | |
26 | safe_num_ne | |
27 | safe_string_ne | |
28 | typesafe_ne | |
29 | ); | |
24d90d8d | 30 | |
94d526d7 DM |
31 | # We use a separate lock to block migration while a replication job |
32 | # is running. | |
33 | ||
7919c482 WB |
34 | our $lockdir = '/var/lock/pve-manager'; |
35 | ||
24d90d8d OB |
36 | # safe variable comparison functions |
37 | ||
38 | sub safe_num_ne { | |
39 | my ($a, $b) = @_; | |
40 | ||
41 | return 0 if !defined($a) && !defined($b); | |
42 | return 1 if !defined($a); | |
43 | return 1 if !defined($b); | |
44 | ||
45 | return $a != $b; | |
46 | } | |
47 | ||
48 | sub safe_string_ne { | |
49 | my ($a, $b) = @_; | |
50 | ||
51 | return 0 if !defined($a) && !defined($b); | |
52 | return 1 if !defined($a); | |
53 | return 1 if !defined($b); | |
54 | ||
55 | return $a ne $b; | |
56 | } | |
57 | ||
58 | sub safe_boolean_ne { | |
59 | my ($a, $b) = @_; | |
60 | ||
61 | # we don't check if value is defined, since undefined | |
62 | # is false (so it's a valid boolean) | |
63 | ||
64 | # negate both values to normalize and compare | |
65 | return !$a != !$b; | |
66 | } | |
67 | ||
68 | sub typesafe_ne { | |
69 | my ($a, $b, $type) = @_; | |
70 | ||
71 | return 0 if !defined($a) && !defined($b); | |
72 | return 1 if !defined($a); | |
73 | return 1 if !defined($b); | |
74 | ||
75 | if ($type eq 'string') { | |
76 | return safe_string_ne($a, $b); | |
77 | } elsif ($type eq 'number' || $type eq 'integer') { | |
78 | return safe_num_ne($a, $b); | |
79 | } elsif ($type eq 'boolean') { | |
80 | return safe_boolean_ne($a, $b); | |
81 | } | |
82 | ||
83 | die "internal error: can't compare $a and $b with type $type"; | |
84 | } | |
85 | ||
86 | ||
94d526d7 DM |
87 | sub guest_migration_lock { |
88 | my ($vmid, $timeout, $func, @param) = @_; | |
89 | ||
90 | my $lockid = "pve-migrate-$vmid"; | |
94d526d7 DM |
91 | |
92 | mkdir $lockdir; | |
93 | ||
94 | my $res = PVE::Tools::lock_file("$lockdir/$lockid", $timeout, $func, @param); | |
95 | die $@ if $@; | |
96 | ||
97 | return $res; | |
98 | } | |
99 | ||
3acb4e74 DC |
100 | sub check_hookscript { |
101 | my ($volid, $storecfg) = @_; | |
102 | ||
103 | $storecfg = PVE::Storage::config() if !defined($storecfg); | |
104 | my ($path, undef, $type) = PVE::Storage::path($storecfg, $volid); | |
105 | ||
106 | die "'$volid' is not in the snippets directory\n" | |
107 | if $type ne 'snippets'; | |
108 | ||
fb73d112 | 109 | die "script '$volid' does not exist\n" |
3acb4e74 DC |
110 | if ! -f $path; |
111 | ||
112 | die "script '$volid' is not executable\n" | |
113 | if ! -x $path; | |
114 | ||
115 | return $path; | |
116 | } | |
117 | ||
118 | sub exec_hookscript { | |
119 | my ($conf, $vmid, $phase, $stop_on_error) = @_; | |
120 | ||
121 | return if !$conf->{hookscript}; | |
3acb4e74 DC |
122 | |
123 | eval { | |
1c9da8ac TL |
124 | my $hookscript = check_hookscript($conf->{hookscript}); |
125 | die $@ if $@; | |
126 | ||
3acb4e74 DC |
127 | PVE::Tools::run_command([$hookscript, $vmid, $phase]); |
128 | }; | |
3acb4e74 DC |
129 | if (my $err = $@) { |
130 | my $errmsg = "hookscript error for $vmid on $phase: $err\n"; | |
1c9da8ac TL |
131 | die $errmsg if ($stop_on_error); |
132 | warn $errmsg; | |
3acb4e74 DC |
133 | } |
134 | } | |
135 | ||
5ee5f887 TL |
136 | # takes a snapshot list (e.g., qm/pct snapshot_list API call result) and |
137 | # prints it out in a nice tree sorted by age. Can cope with multiple roots | |
138 | sub print_snapshot_tree { | |
139 | my ($snapshot_list) = @_; | |
725dcadb | 140 | |
5ee5f887 | 141 | my $snapshots = { map { $_->{name} => $_ } @$snapshot_list }; |
725dcadb OB |
142 | |
143 | my @roots; | |
5ee5f887 | 144 | foreach my $e (@$snapshot_list) { |
725dcadb OB |
145 | my $parent; |
146 | if (($parent = $e->{parent}) && defined $snapshots->{$parent}) { | |
147 | push @{$snapshots->{$parent}->{children}}, $e->{name}; | |
148 | } else { | |
149 | push @roots, $e->{name}; | |
150 | } | |
151 | } | |
152 | ||
153 | # sort the elements by snaptime - with "current" (no snaptime) highest | |
154 | my $snaptimesort = sub { | |
155 | return +1 if !defined $snapshots->{$a}->{snaptime}; | |
156 | return -1 if !defined $snapshots->{$b}->{snaptime}; | |
157 | return $snapshots->{$a}->{snaptime} <=> $snapshots->{$b}->{snaptime}; | |
158 | }; | |
159 | ||
160 | # recursion function for displaying the tree | |
727080ed WB |
161 | my $snapshottree_weak; |
162 | $snapshottree_weak = sub { | |
725dcadb OB |
163 | my ($prefix, $root, $snapshots) = @_; |
164 | my $e = $snapshots->{$root}; | |
165 | ||
166 | my $description = $e->{description} || 'no-description'; | |
167 | ($description) = $description =~ m/(.*)$/m; | |
168 | ||
169 | my $timestring = ""; | |
170 | if (defined $e->{snaptime}) { | |
171 | $timestring = strftime("%F %H:%M:%S", localtime($e->{snaptime})); | |
172 | } | |
173 | ||
174 | my $len = 30 - length($prefix); # for aligning the description | |
9fca8f9d | 175 | $len = 0 if $len < 0; |
725dcadb OB |
176 | printf("%s %-${len}s %-23s %s\n", $prefix, $root, $timestring, $description); |
177 | ||
178 | if ($e->{children}) { | |
e6e15500 | 179 | $prefix = " $prefix"; |
725dcadb | 180 | foreach my $child (sort $snaptimesort @{$e->{children}}) { |
727080ed | 181 | $snapshottree_weak->($prefix, $child, $snapshots); |
725dcadb OB |
182 | } |
183 | } | |
184 | }; | |
727080ed WB |
185 | my $snapshottree = $snapshottree_weak; |
186 | weaken($snapshottree_weak); | |
725dcadb OB |
187 | |
188 | foreach my $root (sort $snaptimesort @roots) { | |
189 | $snapshottree->('`->', $root, $snapshots); | |
190 | } | |
191 | } | |
192 | ||
bd240228 OB |
193 | sub format_pending { |
194 | my ($data) = @_; | |
195 | foreach my $item (sort { $a->{key} cmp $b->{key}} @$data) { | |
196 | my $k = $item->{key}; | |
197 | next if $k eq 'digest'; | |
198 | my $v = $item->{value}; | |
199 | my $p = $item->{pending}; | |
200 | if ($k eq 'description') { | |
201 | $v = PVE::Tools::encode_text($v) if defined($v); | |
202 | $p = PVE::Tools::encode_text($p) if defined($p); | |
203 | } | |
204 | if (defined($v)) { | |
205 | if ($item->{delete}) { | |
206 | print "del $k: $v\n"; | |
207 | } elsif (defined($p)) { | |
208 | print "cur $k: $v\n"; | |
209 | print "new $k: $p\n"; | |
210 | } else { | |
211 | print "cur $k: $v\n"; | |
212 | } | |
213 | } elsif (defined($p)) { | |
214 | print "new $k: $p\n"; | |
215 | } | |
216 | } | |
217 | } | |
218 | ||
55001030 | 219 | # returns the config as an array of hashes, each hash can have the following keys: |
daf8fca5 TL |
220 | # key: the config property name, non-optional |
221 | # value: the current value in effect - if any | |
222 | # pending: a new, still pending, value - if any | |
223 | # delete: when deletions are pending, this is set to either 2 (force) or 1 (graceful) | |
55001030 | 224 | sub config_with_pending_array { |
15144481 OB |
225 | my ($conf, $pending_delete_hash) = @_; |
226 | ||
daf8fca5 TL |
227 | my $pending = delete $conf->{pending}; |
228 | # we don't care for snapshots in pending and it makes our loops throw up | |
229 | delete $conf->{snapshots}; | |
15144481 | 230 | |
daf8fca5 | 231 | my $res = []; |
15144481 | 232 | foreach my $opt (keys %$conf) { |
07c587d8 | 233 | next if ref($conf->{$opt}); # e.g., "raw" lxc.* keys are added as array ref |
dd59a7ca | 234 | |
daf8fca5 TL |
235 | my $item = { |
236 | key => $opt, | |
237 | value => $conf->{$opt}, | |
238 | }; | |
301b375b | 239 | $item->{pending} = delete $pending->{$opt} if defined($pending->{$opt}); |
daf8fca5 TL |
240 | my $delete = delete $pending_delete_hash->{$opt}; |
241 | $item->{delete} = $delete->{force} ? 2 : 1 if defined($delete); | |
15144481 OB |
242 | |
243 | push @$res, $item; | |
244 | } | |
245 | ||
daf8fca5 | 246 | foreach my $opt (keys %$pending) { |
15144481 | 247 | next if $opt eq 'delete'; |
daf8fca5 TL |
248 | push @$res, { |
249 | key => $opt, | |
250 | pending => $pending->{$opt}, | |
251 | }; | |
15144481 OB |
252 | } |
253 | ||
254 | while (my ($opt, $force) = each %$pending_delete_hash) { | |
daf8fca5 TL |
255 | push @$res, { |
256 | key => $opt, | |
257 | delete => $force ? 2 : 1, | |
258 | }; | |
15144481 OB |
259 | } |
260 | ||
261 | return $res; | |
262 | } | |
263 | ||
2b00f623 DC |
264 | # returns the allowed tags for the given user |
265 | # in scalar context, returns the list of allowed tags that exist | |
266 | # in list context, returns a tuple of allowed tags, privileged tags, and if freeform is enabled | |
267 | # | |
268 | # first parameter is a bool if the user is 'privileged' (normally Sys.Modify on /) | |
269 | # second parameter is a closure which takes the vmid. should check if the user can see the vm tags | |
270 | sub get_allowed_tags { | |
271 | my ($rpcenv, $user, $privileged_user) = @_; | |
272 | ||
273 | $privileged_user //= ($rpcenv->check($user, '/', ['Sys.Modify'], 1) // 0); | |
274 | ||
2bf45e38 | 275 | my $datacenter_config = PVE::Cluster::cfs_read_file('datacenter.cfg'); |
2b00f623 DC |
276 | |
277 | my $allowed_tags = {}; | |
278 | my $privileged_tags = {}; | |
2bf45e38 | 279 | if (my $tags = $datacenter_config->{'registered-tags'}) { |
2b00f623 DC |
280 | $privileged_tags->{$_} = 1 for $tags->@*; |
281 | } | |
2bf45e38 | 282 | my $user_tag_privs = $datacenter_config->{'user-tag-access'} // {}; |
2b00f623 DC |
283 | my $user_allow = $user_tag_privs->{'user-allow'} // 'free'; |
284 | my $freeform = $user_allow eq 'free'; | |
285 | ||
286 | if ($user_allow ne 'none' || $privileged_user) { | |
287 | $allowed_tags->{$_} = 1 for ($user_tag_privs->{'user-allow-list'} // [])->@*; | |
288 | } | |
289 | ||
290 | if ($user_allow eq 'free' || $user_allow eq 'existing' || $privileged_user) { | |
291 | my $props = PVE::Cluster::get_guest_config_properties(['tags']); | |
292 | for my $vmid (keys $props->%*) { | |
293 | next if !$privileged_user && !$rpcenv->check_vm_perm($user, $vmid, undef, ['VM.Audit'], 0, 1); | |
2bf45e38 | 294 | $allowed_tags->{$_} = 1 for split_list($props->{$vmid}->{tags}); |
2b00f623 DC |
295 | } |
296 | } | |
297 | ||
298 | if ($privileged_user) { | |
299 | $allowed_tags->{$_} = 1 for keys $privileged_tags->%*; | |
300 | } else { | |
301 | delete $allowed_tags->{$_} for keys $privileged_tags->%*; | |
302 | } | |
303 | ||
304 | return wantarray ? ($allowed_tags, $privileged_tags, $freeform) : $allowed_tags; | |
305 | } | |
306 | ||
307 | # checks the permissions for setting/updating/removing tags for guests | |
308 | # tagopt_old and tagopt_new expect the tags as they are in the config | |
309 | # | |
310 | # either returns gracefully or raises a permission exception | |
311 | sub assert_tag_permissions { | |
312 | my ($vmid, $tagopt_old, $tagopt_new, $rpcenv, $authuser) = @_; | |
313 | ||
2b00f623 DC |
314 | $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options']); |
315 | ||
2bf45e38 TL |
316 | my $privileged_user = $rpcenv->check($authuser, '/', ['Sys.Modify'], 1) // 0; |
317 | ||
318 | my ($allowed_tags, $privileged_tags, $freeform); | |
2b00f623 DC |
319 | my $check_single_tag = sub { |
320 | my ($tag) = @_; | |
321 | return if $privileged_user; | |
322 | ||
2bf45e38 TL |
323 | if (!defined($allowed_tags // $privileged_tags // $freeform)) { # cache |
324 | ($allowed_tags, $privileged_tags, $freeform) = get_allowed_tags($rpcenv, $authuser, $privileged_user); | |
2b00f623 DC |
325 | } |
326 | ||
327 | if ((!$allowed_tags->{$tag} && !$freeform) || $privileged_tags->{$tag}) { | |
328 | raise_perm_exc("/, Sys.Modify for modifying tag '$tag'"); | |
329 | } | |
330 | ||
331 | return; | |
332 | }; | |
333 | ||
2bf45e38 | 334 | my ($old_tags, $new_tags, $all_tags) = ({}, {}, {}); |
2b00f623 | 335 | |
2bf45e38 TL |
336 | $all_tags->{$_} = $old_tags->{$_} += 1 for split_list($tagopt_old // ''); |
337 | $all_tags->{$_} = $new_tags->{$_} += 1 for split_list($tagopt_new // ''); | |
2b00f623 DC |
338 | |
339 | for my $tag (keys $all_tags->%*) { | |
340 | next if ($new_tags->{$tag} // 0) == ($old_tags->{$tag} // 0); | |
341 | $check_single_tag->($tag); | |
342 | } | |
343 | } | |
344 | ||
09159800 TL |
345 | sub get_unique_tags { |
346 | my ($tags, $no_join_result) = @_; | |
347 | ||
348 | $tags = [ split_list($tags // '') ] if ref($tags) ne 'ARRAY'; | |
349 | return !$no_join_result ? '': [] if !scalar($tags->@*); | |
350 | ||
351 | my $datacenter_config = PVE::Cluster::cfs_read_file('datacenter.cfg'); | |
352 | my $tag_style_config = $datacenter_config->{'tag-style'} // {}; | |
353 | my $case_sensitive = !!$tag_style_config->{'case-sensitive'}; | |
354 | ||
355 | my $seen_tags = {}; | |
356 | my $res = []; | |
357 | if (!defined($tag_style_config->{ordering}) || $tag_style_config->{ordering} ne 'config') { | |
358 | for my $tag ( sort { $case_sensitive ? $a cmp $b : lc($a) cmp lc($b) } $tags->@*) { | |
359 | $tag = lc($tag) if !$case_sensitive; | |
360 | next if $seen_tags->{$tag}; | |
361 | $seen_tags->{$tag} = 1; | |
362 | push @$res, $tag; | |
363 | } | |
364 | } else { | |
365 | for my $tag ($tags->@*) { | |
366 | $tag = lc($tag) if !$case_sensitive; | |
367 | next if $seen_tags->{$tag}; | |
368 | $seen_tags->{$tag} = 1; | |
369 | push @$res, $tag; | |
370 | } | |
371 | } | |
372 | ||
373 | return !$no_join_result ? join(';', $res->@*) : $res; | |
374 | } | |
375 | ||
aabf879e AD |
376 | my sub get_tags_from_trunk { |
377 | my ($trunks) = @_; | |
378 | ||
379 | my $res = {}; | |
380 | my @trunks_array = split /;/, $trunks; | |
381 | for my $trunk (@trunks_array) { | |
382 | my ($tag, $tag_end) = split /-/, $trunk; | |
383 | if($tag_end && $tag_end > $tag) { | |
384 | my @tags = ($tag..$tag_end); | |
385 | $res->{$_} = 1 for @tags; | |
386 | } else { | |
387 | $res->{$tag} = 1; | |
388 | } | |
389 | } | |
390 | return $res; | |
391 | } | |
392 | ||
393 | sub check_vnet_access { | |
394 | my ($rpcenv, $authuser, $vnet, $tag, $trunks) = @_; | |
395 | ||
396 | my $zone = 'localnetwork'; | |
397 | ||
398 | if ($have_sdn) { | |
399 | my $vnet_cfg = PVE::Network::SDN::Vnets::config(); | |
400 | if (defined(my $vnet = PVE::Network::SDN::Vnets::sdn_vnets_config($vnet_cfg, $vnet, 1))) { | |
401 | $zone = $vnet->{zone}; | |
402 | } | |
403 | } | |
404 | ||
405 | # if a tag is defined, test if user have a specific access to the vlan (or propagated from full bridge acl) | |
406 | $rpcenv->check($authuser, "/sdn/zones/$zone/$vnet/$tag", ['SDN.Use']) if $tag; | |
407 | # check each vlan access from trunk | |
408 | if ($trunks) { | |
409 | my $tags = get_tags_from_trunk($trunks); | |
410 | for my $tag (sort keys %$tags) { | |
411 | $rpcenv->check($authuser, "/sdn/zones/$zone/$vnet/$tag", ['SDN.Use']); | |
412 | } | |
413 | } | |
414 | # if no tag, test if user have access to full bridge. | |
415 | $rpcenv->check($authuser, "/sdn/zones/$zone/$vnet", ['SDN.Use']) | |
416 | if !($tag || $trunks); | |
417 | } | |
418 | ||
253a2ea9 FW |
419 | sub abort_guest_tasks { |
420 | my ($rpcenv, $type, $vmid) = @_; | |
421 | ||
422 | my $authuser = $rpcenv->get_user(); | |
423 | my $node = PVE::INotify::nodename(); | |
424 | my $can_abort_all = $rpcenv->check($authuser, "/nodes/$node", [ 'Sys.Modify' ], 1); | |
425 | ||
426 | my $active_tasks = PVE::INotify::read_file('active'); | |
427 | my $aborted_tasks = []; | |
428 | for my $task (@$active_tasks) { | |
1d06c1bf TL |
429 | next if $task->{saved} || $task->{type} ne $type || $task->{id} ne $vmid; # filter |
430 | ||
431 | my $can_abort_task = $can_abort_all; | |
432 | if (!$can_abort_task) { | |
253a2ea9 FW |
433 | # tasks started by a token can be aborted by the token or token owner, |
434 | # tasks started by a user can be aborted by the user | |
435 | if (PVE::AccessControl::pve_verify_tokenid($task->{user}, 1)) { | |
436 | my $full_tokenid = $task->{user}; | |
437 | my ($task_username, undef) = PVE::AccessControl::split_tokenid($full_tokenid); | |
438 | $can_abort_task = $authuser eq $task_username || $authuser eq $full_tokenid; | |
439 | } else { | |
440 | $can_abort_task = $authuser eq $task->{user}; | |
441 | } | |
1d06c1bf | 442 | } |
253a2ea9 | 443 | |
1d06c1bf TL |
444 | if ($can_abort_task) { |
445 | # passing `1` for parameter $killit aborts the task | |
446 | PVE::RPCEnvironment->check_worker($task->{upid}, 1); | |
447 | push @$aborted_tasks, $task->{upid}; | |
253a2ea9 FW |
448 | } |
449 | } | |
450 | return $aborted_tasks; | |
451 | } | |
452 | ||
94d526d7 | 453 | 1; |