]> git.proxmox.com Git - pve-guest-common.git/blame - src/PVE/GuestHelpers.pm
guest helpers: avoid checking user/token if one can abort all tasks
[pve-guest-common.git] / src / PVE / GuestHelpers.pm
CommitLineData
94d526d7
DM
1package PVE::GuestHelpers;
2
3use strict;
4use warnings;
5
2b00f623 6use PVE::Exception qw(raise_perm_exc);
2bf45e38 7use PVE::Tools qw(split_list);
3acb4e74 8use PVE::Storage;
94d526d7 9
725dcadb 10use POSIX qw(strftime);
727080ed 11use Scalar::Util qw(weaken);
725dcadb 12
aabf879e
AD
13my $have_sdn;
14eval {
15 require PVE::Network::SDN;
16 $have_sdn = 1;
17};
18
b2b16cee
DC
19use base qw(Exporter);
20
2b00f623
DC
21our @EXPORT_OK = qw(
22assert_tag_permissions
aabf879e 23check_vnet_access
2b00f623
DC
24get_allowed_tags
25safe_boolean_ne
26safe_num_ne
27safe_string_ne
28typesafe_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
34our $lockdir = '/var/lock/pve-manager';
35
24d90d8d
OB
36# safe variable comparison functions
37
38sub 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
48sub 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
58sub 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
68sub 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
87sub 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
100sub 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
118sub 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
138sub 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
193sub 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 224sub 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
270sub 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
311sub 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
345sub 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
376my 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
393sub 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
419sub 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 4531;