| 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; |