File Coverage

blib/lib/Net/OpenStack/Client/Identity/v3.pm
Criterion Covered Total %
statement 237 277 85.5
branch 98 146 67.1
condition 18 22 81.8
subroutine 26 27 96.3
pod 12 12 100.0
total 391 484 80.7


line stmt bran cond sub pod time code
1             package Net::OpenStack::Client::Identity::v3;
2             $Net::OpenStack::Client::Identity::v3::VERSION = '0.1.4';
3 2     2   7999 use strict;
  2         4  
  2         51  
4 2     2   8 use warnings;
  2         4  
  2         52  
5              
6 2     2   777 use Set::Scalar;
  2         18166  
  2         79  
7 2     2   13 use Readonly;
  2         3  
  2         68  
8              
9 2     2   11 use Net::OpenStack::Client::API::Convert qw(convert);
  2         4  
  2         63  
10 2     2   380 use Net::OpenStack::Client::Identity::Tagstore;
  2         4  
  2         51  
11 2     2   12 use Net::OpenStack::Client::Request qw(mkrequest);
  2         3  
  2         74  
12              
13 2     2   429 use MIME::Base64 qw(encode_base64url decode_base64url);
  2         533  
  2         6016  
14              
15             Readonly my $IDREG => qr{[0-9a-z]{33}};
16              
17             # This list is ordered:
18             # Configuration of n-th item does not require
19             # configuration of any items after that, but
20             # might require configuration of previous ones
21             Readonly our @SUPPORTED_OPERATIONS => qw(
22             region
23             domain
24             project
25             user
26             group
27             role
28             rolemap
29             service
30             endpoint
31             );
32              
33             Readonly my %PARENT_ATTR => {
34             region => 'parent_region_id',
35             project => 'parent_id',
36             };
37              
38             # tagstore cache
39             # key is project id; value is instance
40             my $_tagstores = {};
41              
42             =head1 Functions
43              
44             =over
45              
46             =item sort_parent
47              
48             Sort according to parent attribute.
49              
50             =cut
51              
52             # Use toposort?
53             # see https://rosettacode.org/wiki/Topological_sort#Perl
54              
55             sub sort_parent
56             {
57             # We assume that an empty string or number 0 is not a valid/used region name
58             # force strings, so we can do eq tests
59 14     14 1 17 my $ra = $a->{name};
60 14         17 my $rb = $b->{name};
61 14   100     28 my $pra = $a->{parent} || '';
62 14   100     29 my $prb = $b->{parent} || '';
63              
64 14         18 my $res;
65 14 100 100     51 if ($pra eq $rb) {
    100 100        
    100          
    100          
66             # b is parent of a: order b a
67 2         4 $res = 1;
68             } elsif ($prb eq $ra) {
69             # a is parent of b: order a b
70 1         1 $res = -1;
71             } elsif ($pra && !$prb) {
72             # a has parent, b does not: order b a
73 1         1 $res = 1;
74             } elsif ($prb && !$pra) {
75             # b has parent, a does not: order a b
76 3         5 $res = -1;
77             } else {
78             # does not matter, use alphabetical sort
79 7         9 $res = $ra cmp $rb;
80             }
81              
82 14         23 return $res;
83             }
84              
85             =item sort_parents
86              
87             Sort arrayref of C with data from C using parent C.
88              
89             =cut
90              
91             sub sort_parents
92             {
93 2     2 1 780 my ($names, $items, $attr) = @_;
94              
95             # Assume the id is equal to the name of the region
96 2         6 my @snames = sort sort_parent (map {{name => $_, parent => $items->{$_}->{$attr}}} @$names);
  9         27  
97 2         4 return map {$_->{name}} @snames;
  9         21  
98             }
99              
100             =item rest
101              
102             Convenience wrapper for direct REST calls
103             for C, C and options C.
104              
105             =cut
106              
107             sub rest
108             {
109 25     25 1 72 my ($self, $method, $operation, %ropts) = @_;
110 25         74 my $defropts = {
111             method => $method,
112             version => 'v3',
113             service => 'identity',
114             };
115              
116 25         104 %ropts = (%$defropts, %ropts);
117              
118             # generate raw data
119 25 100       81 $ropts{raw} = {$operation => delete $ropts{data}} if ($ropts{data});
120              
121 25   100     100 my $endpoint = "${operation}s/" . (delete $ropts{what} || '') . "?name=name";
122              
123 25         95 return $self->rest(mkrequest($endpoint, $method, %ropts));
124             };
125              
126             =item get_id
127              
128             Return the ID of an C.
129             If the name is an ID, return the ID without a lookup.
130             If the operation is 'region', return the name.
131              
132             Options
133              
134             =over
135              
136             =item error: report an error when no id is found
137              
138             =item msg: use the value as (part of) the reported message
139              
140             =back
141              
142             =cut
143              
144             sub get_id
145             {
146 16     16 1 74 my ($self, $operation, $name, %opts) = @_;
147              
148             # region has no id (or no name, whatever you like)
149 16 100 66     49 return $name if ($name =~ m/$IDREG/ || $operation eq 'region');
150              
151             # GET the list for name
152 12         200 my $resp = $self->api_identity_rest('GET', $operation, result => "/${operation}s", params => {name => $name});
153              
154 12         41 my $msg = "found for $operation with name $name";
155 12 100       36 $msg .= " $opts{msg}" if $opts{msg};
156              
157 12         14 my $id;
158 12 50       28 if ($resp) {
159 12 50       17 my @ids = (map {$_->{id}} @{$resp->result || []});
  12         31  
  12         26  
160 12 50       36 if (scalar @ids > 1) {
    50          
161             # what? do not return anything
162 0         0 $self->error("More than one ID $msg: @ids");
163             } elsif (@ids) {
164 12         17 $id = $ids[0];
165 12         44 $self->verbose("ID $id $msg");
166             } else {
167 0 0       0 my $method = $opts{error} ? 'error' : 'verbose';
168 0         0 $self->$method("No ID $msg");
169             }
170             } else {
171 0         0 $self->error("get_id invalid request $msg: $resp->{error}");
172             };
173              
174 12         3178 return $id;
175             }
176              
177             # Function to retrun the name attribute based on the the operation
178             sub _name_attribute
179             {
180 11     11   18 my ($operation) = @_;
181 11 100       24 return $operation eq 'region' ? 'id' : 'name';
182             }
183              
184             # Function to return the name based on the operation and data
185             sub _make_name
186             {
187 8     8   13 my ($operation, $data) = @_;
188 8 100       16 if ($operation eq 'endpoint') {
189             # for endpoint, we construct an internal unique name based on
190             # interface and url, seperated by a underscore
191 3         10 return "$data->{interface}_$data->{url}";
192             } else {
193 5         10 my $attr = _name_attribute($operation);
194 5         17 return $data->{$attr};
195             }
196             }
197              
198             =item tagstore_init
199              
200             Function to initialise tagstore or return cached version based on tagstore project name.
201              
202             =cut
203              
204             sub tagstore_init
205             {
206 3     3 1 7 my ($client, $tagstore_proj) = @_;
207              
208 3 100       9 if (!$_tagstores->{$tagstore_proj}) {
209              
210             # Does the project exist?
211 1         10 my $resp = $client->api_identity_projects(name => $tagstore_proj);
212 1 50       3 if ($resp) {
213 1         2 my @proj = @{$resp->result};
  1         3  
214 1 50       4 if (scalar @proj > 1) {
    50          
215             $client->error("More than one tagstore project $tagstore_proj found: ids ",
216 0         0 join(",", map {$_->{id}} @proj), ". Unsupported for now");
  0         0  
217 0         0 return;
218             } elsif (scalar @proj == 1) {
219 1         6 $client->verbose("Found one tagstore project $tagstore_proj id ", $proj[0]->{id});
220             } else {
221 0         0 $resp = $client->api_identity_add_project(name => $tagstore_proj,
222             description => "Main tagstore project $tagstore_proj");
223 0 0       0 if ($resp) {
224 0         0 $client->verbose("Created main tagstore project $tagstore_proj id ", $resp->result->{id});
225             } else {
226 0         0 $client->error("Failed to add main tagstore project $tagstore_proj: $resp->{error}");
227 0         0 return;
228             }
229             }
230             } else {
231 0         0 $client->error("Failed to list possible tagstore project $tagstore_proj: $resp->{error}");
232 0         0 return;
233             }
234              
235             # Get instance
236 1         265 my $tgst = Net::OpenStack::Client::Identity::Tagstore->new(
237             $client,
238             $tagstore_proj,
239             );
240              
241 1 50       4 if ($tgst) {
242 1         6 $_tagstores->{$tagstore_proj} = $tgst;
243             } else {
244 0         0 $client->error("sync: failed to create new tagstore for project $tagstore_proj");
245 0         0 return;
246             }
247             }
248              
249 3         7 return $_tagstores->{$tagstore_proj};
250             }
251              
252             =item tagstore_postprocess
253              
254             Function to postprocess sync operations when a tagstore is used.
255              
256             =cut
257              
258             sub tagstore_postprocess
259             {
260 7     7 1 17 my ($tagstore, $phase, $operation, $name, $result) = @_;
261              
262 7         23 my $msg = "sync postprocess $operation $name stopped after failure to $phase";
263 7 50       18 if (exists($result->{id})) {
264 7         14 my $id = $result->{id};
265 7         10 my $ok = 1;
266              
267 7 100 100     28 if ($phase eq 'create' || $phase eq 'delete') {
268 6 100       14 my $method = $phase eq 'create' ? 'add' : $phase;
269 6         31 $ok = $tagstore->$method("ID_${operation}_${id}");
270             } else {
271 1         5 $tagstore->verbose("sync: nothing to do for tagstore postprocessing during $phase for $name id $id");
272             }
273              
274 7 50       281 if ($ok) {
275 7         25 return 1;
276             } else {
277 0         0 $tagstore->error("$msg tag $id to tagstore. See previous error where to add the tag to continue");
278 0         0 return;
279             }
280             } else {
281 0         0 $tagstore->error("$msg no id in response");
282 0         0 return;
283             }
284             }
285              
286             =pod
287              
288             =back
289              
290             =head1 Methods
291              
292             =over
293              
294             =item sync
295              
296             For an C (like C, C, C, ...),
297             given an hashref of C (key is the name),
298             compare it with all existing items:
299              
300             =over
301              
302             =item Non-existing ones are added/created
303              
304             =item Existing ones are possibly updated
305              
306             =item Existing ones that are not requested are disbaled
307              
308             =back
309              
310             Returns a hasref with responses for the created items. The keys are
311             C, C and C and the values an arrayref of responses.
312              
313             For C operations, as they have no name, use the C<<_>>
314             as the name for the C hashref.
315              
316             Following options are supported:
317              
318             =over
319              
320             =item filter: a function to filter the existing items.
321             Return a true value to keep the existing item (false will ignore it).
322             By default, all existing items are considered.
323              
324             =item delete: when the delete option is true, existing items that are
325             not in the C hashref, will be deleted (instead of disabled).
326              
327             =item keep: when the keep option is true, existing items that are
328             not in the C hashref are ignored.
329             This precedes any value of C option.
330              
331             =item tagstore: use project tagstore to track synced ids.
332             If no filter is set, the tagstore is used to filter known ids
333             as existing tags in the tagstore.
334              
335             =back
336              
337             =cut
338              
339             sub sync
340             {
341 3     3 1 11 my ($self, $operation, $items, %opts) = @_;
342              
343 3 50       11 if (! grep {$_ eq $operation} @SUPPORTED_OPERATIONS) {
  27         138  
344 0         0 $self->error("Unsupported operation $operation");
345 0         0 return;
346             }
347              
348 3         9 my $tagstore;
349 3 100       13 $tagstore = tagstore_init($self, $opts{tagstore}) if $opts{tagstore};
350              
351 3         5 my $filter;
352 3 100       10 if ($opts{filter}) {
    50          
353 1         2 $filter = $opts{filter};
354 1 50       3 if (ref($filter) ne 'CODE') {
355 0         0 $self->error("sync filter is not CODE");
356 0         0 return;
357             }
358             } elsif ($tagstore) {
359 2     6   10 $filter = sub {return $tagstore->get("ID_${operation}_".$_[0]->{id})};
  6         27  
360             } else {
361 0     0   0 $filter = sub {return 1};
  0         0  
362             };
363              
364             # GET the list
365 3         20 my $resp_list = $self->api_identity_rest('GET', $operation, result => "/${operation}s");
366              
367             my $found = {
368 8         20 map {_make_name($operation, $_) => $_}
369 11         35 grep {$filter->($_)}
370 3 50       9 @{$resp_list->result || []}
  3         7  
371             };
372              
373 3         27 my $existing = Set::Scalar->new(keys %$found);
374 3         364 my $wanted = Set::Scalar->new(keys %$items);
375              
376             # Add default enabled=1 to all wanted operation
377 3         178 foreach my $want (@$wanted) {
378 9 50       120 $items->{$want}->{enabled} = convert(1, 'boolean') if ! exists($items->{$want}->{enabled});
379             };
380              
381             # compare
382              
383 3         14 my @tocreate = sort @{$wanted - $existing};
  3         19  
384              
385             # regions and projects can have parent relations, so they need to be sorted accordingly
386             # we only expect the order to be important with creation, not for updates or deletes
387             # the parent attr might also be the names, not the actual ids
388             # e.g. to support ordering not yet created parent
389 3         750 my $parentattr = $PARENT_ATTR{$operation};
390 3 100       139 @tocreate = sort_parents(\@tocreate, $items, $parentattr) if $parentattr;
391              
392 3         11 my $res = {
393             create => [],
394             update => [],
395             delete => [],
396             };
397              
398 3         5 my $postprocess;
399 3 100   7   11 $postprocess = sub { return tagstore_postprocess($tagstore, @_) } if ($tagstore);
  7         21  
400              
401 3 50       38 my $created = $self->api_identity_create($operation, \@tocreate, $items, $res, $postprocess) or return;
402              
403 3         5 my @checkupdate = sort @{$wanted * $existing};
  3         15  
404 3 50       870 $self->api_identity_update($operation, \@checkupdate, $found, $items, $res, $postprocess) or return;
405             # no tagstore operations?
406              
407 3         7 my @toremove = sort @{$existing - $wanted};
  3         12  
408 3 50       664 $self->api_identity_delete($operation, \@toremove, $found, \%opts, $res, $postprocess) or return;
409              
410 3         19 return $res;
411             }
412              
413             =item get_item
414              
415             Retrieve and augment an item with C from hashref C.
416              
417             Modification to the data
418              
419             =over
420              
421             =item name is inserted (unless this is an endpoint)
422              
423             =item any named ids (either from (other) operation(s) or parenting) are resolved
424             to their actual id.
425              
426             =back
427              
428             =cut
429              
430             sub get_item
431             {
432 9     9 1 22 my ($self, $operation, $name, $items) = @_;
433              
434 9         15 my $new = $items->{$name};
435              
436 9 100       19 if ($operation ne 'endpoint') {
437 6         14 my $nameattr = _name_attribute($operation);
438             # add name
439 6         14 $new->{$nameattr} = $name;
440             }
441              
442             # resolve ids
443 9         28 my %toresolve = (map {$_."_id" => $_} @SUPPORTED_OPERATIONS);
  81         497  
444             # resolve parent ids
445 9 100       47 $toresolve{$PARENT_ATTR{$operation}} = $operation if $PARENT_ATTR{$operation};
446              
447 9         119 foreach my $attr (sort keys %toresolve) {
448             # no autovivification
449 84 100       137 next if ! exists($new->{$attr});
450              
451 5         39 my $resolved = $self->api_identity_get_id($toresolve{$attr}, $new->{$attr}, error => 1);
452 5 50       48 if (defined($resolved)) {
453 5         13 $new->{$attr} = $resolved;
454             } else {
455 0         0 $self->error("Failed to resolve id for $operation name $name attr $attr with value $new->{$attr}");
456 0         0 return;
457             }
458             }
459              
460 9         46 return $new;
461             }
462              
463             =item _process_response
464              
465             Helper function for all 3 sync phases
466              
467             C is updated in place.
468              
469             Returns 1 on success, undef otherwise (and reports an error).
470              
471             =cut
472              
473             sub _process_response
474             {
475 10     10   29 my ($client, $phase, $resp, $res, $operation, $name, $postprocess) = @_;
476              
477 10 50       21 if ($resp) {
478 10         29 my $result = $resp->result("/$operation");
479 10         19 push(@{$res->{$phase}}, [$name, $result]);
  10         31  
480 10         40 $client->verbose("sync: ${phase}d $operation $name");
481 10 100       2532 if ($postprocess) {
482 7 50       18 $postprocess->($phase, $operation, $name, $result) or return;
483             }
484 10         53 return 1;
485             } else {
486 0         0 $client->error("sync: failed to $phase $operation $name: $resp->{error}");
487 0         0 return;
488             }
489             }
490              
491              
492             =item create
493              
494             Create C items in arrayref C from configured C
495             (using name attriute C),
496             with result hashref C. C is updated in place.
497              
498             C is a anonymous function called after a succesful REST call,
499             and is passed following arguments:
500              
501             =over
502              
503             =item phase: one of C, C or C, depending on what pahse of the sync
504             the REST call is made.
505              
506             =item operation: type of operation
507              
508             =item name: name of the operation
509              
510             =item result: result of the REST call
511              
512             =back
513              
514             =cut
515              
516             sub create
517             {
518 3     3 1 11 my ($self, $operation, $tocreate, $items, $res, $postprocess) = @_;
519              
520 3         7 my @tocreate = @$tocreate;
521              
522 3 50       8 if (@tocreate) {
523 3         25 $self->info("Creating ${operation}s: @tocreate");
524 3         781 foreach my $name (@tocreate) {
525             # POST to create
526 5 50       40 my $new = $self->api_identity_get_item($operation, $name, $items) or return;
527 5         29 my $resp = $self->api_identity_rest('POST', $operation, data => $new);
528 5 50       17 _process_response($self, 'create', $resp, $res, $operation, $name, $postprocess) or return;
529             }
530             } else {
531 0         0 $self->verbose("No ${operation}s to create");
532             }
533              
534 3         14 return 1;
535             }
536              
537             =item update
538              
539             Update C items in arrayref C from C items
540             with configured C, with result hashref C.
541             C is updated in place.
542              
543             =cut
544              
545             sub update
546             {
547 3     3 1 9 my ($self, $operation, $checkupdate, $found, $items, $res, $postprocess) = @_;
548              
549 3         7 my @checkupdate = @$checkupdate;
550              
551 3 100       8 if (@checkupdate) {
552 2         12 $self->info("Possibly updating existing ${operation}s: @checkupdate");
553 2         507 my @toupdate;
554 2         6 foreach my $name (@checkupdate) {
555             # anything to update?
556 4         7 my $update;
557 4 50       23 my $update_data = $self->api_identity_get_item($operation, $name, $items) or return;
558 4         14 foreach my $attr (sort keys %$update_data) {
559 16         22 my $wa = $update_data ->{$attr};
560 16         21 my $fo = $found->{$name}->{$attr};
561 16 100 25     69 my $action = $attr eq 'enabled' ? ($wa xor $fo): ($wa ne $fo);
562             # hmmm, how to keep this JSON safe?
563 16 100       49 $update->{$attr} = $wa if $action;
564             }
565 4 100       22 if (scalar keys %$update) {
566 2         4 push(@toupdate, $name);
567 2         11 my $resp = $self->api_identity_rest('PATCH', $operation, what => $found->{$name}->{id}, data => $update);
568 2 50       5 _process_response($self, 'update', $resp, $res, $operation, $name, $postprocess) or return;
569             }
570             }
571 2 50       14 $self->info(@toupdate ? "Updated existing ${operation}s: @toupdate" : "No existing ${operation}s updated");
572             } else {
573 1         6 $self->verbose("No existing ${operation}s to update");
574             }
575              
576 3         759 return 1;
577             }
578              
579             =item delete
580              
581             Delete (or disable) C items in arrayref C from C
582             existing items, with options C (for C and C)
583             and result hashref C. C is updated in place.
584              
585             When C option is true, nothing will happen.
586             When C is true, items will be delete; when items will be disabled.
587              
588             =cut
589              
590             sub delete
591             {
592 3     3 1 8 my ($self, $operation, $toremove, $found, $opts, $res, $postprocess) = @_;
593              
594 3         7 my @toremove = @$toremove;
595              
596 3 50       14 my $dowhat = $opts->{delete} ? 'delet' : 'disabl';
597              
598 3 50       8 if (@toremove) {
599 3 50       6 if ($opts->{ignore}) {
600 0         0 $self->info("Ignoring existing ${operation}s (instead of ${dowhat}ing): @toremove");
601             } else {
602 3         21 $self->info(ucfirst($dowhat)."ing existing ${operation}s: @toremove");
603 3         763 foreach my $name (@toremove) {
604 4         8 my $resp;
605 4 50       10 if ($opts->{delete}) {
606             # DELETE to delete
607 0         0 $resp = $self->api_identity_rest('DELETE', $operation, what => $found->{$name}->{id});
608             } else {
609             # PATCH to disable
610             # do not disable if already disabled
611 4 100       12 if ($found->{$name}->{enabled}) {
612             $resp = $self->api_identity_rest('PATCH', $operation,
613             what => $found->{$name}->{id},
614 3         14 data => {enabled => convert(0, 'boolean')});
615             } else {
616             $self->verbose("Not disabling already disabled ".
617 1         7 "$operation $name (id ".$found->{$name}->{id}.")");
618             }
619             }
620              
621 4 100       261 if (defined($resp)) {
622 3 50       11 _process_response($self, 'delete', $resp, $res, $operation, $name, $postprocess) or return;
623             }
624             }
625             }
626             } else {
627 0         0 $self->verbose("No existing ${operation}s to ${dowhat}e");
628             }
629              
630 3         13 return 1;
631             }
632              
633              
634             =item sync_rolemap
635              
636             Add missing roles for project/domain and group/user,
637             and delete any when tagstore is used.
638              
639             The roles are defined with a nested hashref, like
640             the url is structured (with an arrayref of roles as value).
641             E.g.
642             $roles = {
643             domain => {
644             dom1 => {
645             user => {
646             user1 => [role1 role2],
647             ...
648             },
649             group => {
650             ...
651             },
652             },
653             ...
654             project => {
655             ...
656             },
657             }
658              
659             Options
660              
661             =over
662              
663             =item tagstore: use project tagstore to track synced roles.
664              
665             =back
666              
667             =cut
668              
669              
670             sub sync_rolemap
671             {
672 1     1 1 4 my ($self, $roles, %opts) = @_;
673              
674             # Get all roles from tagstore (if defined)
675             # The role tag is ROLE_url
676             # url is
677             # projects/{project_id} OR domains/{domain_id} +
678             # groups/{group_id} OR users/{user_id} +
679             # roles/{role_id}
680              
681             # Will use url as identifier
682              
683 1         2 my ($tagstore, @found);
684              
685 1 50       4 if ($opts{tagstore}) {
686 1 50       4 $tagstore = tagstore_init($self, $opts{tagstore}) if $opts{tagstore};
687             # Strip ROLE_, decode/unescape the url
688 1         3 @found = map {my $url = $_; $url =~ s/^ROLE_//; decode_base64url($url)} grep {m/^ROLE_/} sort keys %{$tagstore->fetch};
  2         15  
  2         7  
  2         6  
  8         17  
  1         3  
689             };
690 1         11 my $existing = Set::Scalar->new(@found);
691              
692             # create hash: key is url, value is 1
693 1         66 my $items;
694 1         3 foreach my $base (qw(project domain)) {
695 2 50       4 foreach my $bval (sort keys %{$roles->{$base} || {}}) {
  2         10  
696 2 50       14 my $bid = $self->api_identity_get_id($base, $bval, error => 1, msg => 'for role sync')
697             or return;
698 2         85 foreach my $who (qw(user group)) {
699 4 100       8 foreach my $wval (sort keys %{$roles->{$base}->{$bval}->{$who} || {}}) {
  4         25  
700 2 50       14 my $wid = $self->api_identity_get_id($who, $wval, error => 1, msg => 'for role sync')
701             or return;
702 2         5 foreach my $role (@{$roles->{$base}->{$bval}->{$who}->{$wval}}) {
  2         73  
703 3 50       22 my $rid = $self->api_identity_get_id('role', $role, error => 1, msg => 'for role sync')
704             or return;
705 3         19 $items->{"${base}s/$bid/${who}s/$wid/roles/$rid"} = 1;
706             }
707             }
708             };
709             };
710             };
711              
712 1         7 my $wanted = Set::Scalar->new(keys %$items);
713              
714             my $rest = sub {
715 2     2   74 my ($urls, $method, $tagmethod) = @_;
716              
717 2 50       5 if (@$urls) {
718 2         10 $self->verbose("roles sync: going to $tagmethod @$urls");
719             } else {
720 0         0 $self->verbose("roles sync: nothing to $tagmethod");
721 0         0 return 1;
722             };
723              
724 2         498 foreach my $url (@$urls) {
725 3         11 my $resp = $self->rest(mkrequest($url, $method, version => 'v3', service => 'identity'));
726 3 50       12 if ($resp) {
727 3 50       8 if ($tagstore) {
728 3         9 my $tag = "ROLE_" . encode_base64url($url);
729 3 50       41 if (!$tagstore->$tagmethod($tag)) {
730 0         0 $tagstore->error("Failed to $tagmethod tag $tag to tagstore. ".
731             "See previous error where to add the tag to continue");
732 0         0 return;
733             }
734             }
735             } else {
736 0         0 $self->error("Failed to sync role $method $url");
737 0         0 return;
738             }
739             }
740 2         7 return 1
741 1         93 };
742              
743             # Add new ones
744 1         2 my @tocreate = sort @{$wanted - $existing};
  1         4  
745 1 50       243 $rest->(\@tocreate, 'PUT', 'add') or return;
746              
747             # Delete unknown
748 1         2 my @toremove = sort @{$existing - $wanted};
  1         4  
749 1 50       219 $rest->(\@toremove, 'DELETE', 'delete') or return;
750              
751 1         11 return 1;
752             }
753              
754             =pod
755              
756             =back
757              
758             =cut
759              
760             1;