File Coverage

blib/lib/Net/OpenStack/Client/Identity/Tagstore.pm
Criterion Covered Total %
statement 111 122 90.9
branch 33 48 68.7
condition 1 3 33.3
subroutine 12 12 100.0
pod 5 5 100.0
total 162 190 85.2


line stmt bran cond sub pod time code
1             package Net::OpenStack::Client::Identity::Tagstore;
2             $Net::OpenStack::Client::Identity::Tagstore::VERSION = '0.1.4';
3 2     2   508 use strict;
  2         4  
  2         52  
4 2     2   9 use warnings;
  2         5  
  2         46  
5              
6 2     2   8 use parent qw(Net::OpenStack::Client::Base);
  2         4  
  2         10  
7              
8 2     2   105 use Readonly;
  2         4  
  2         2376  
9              
10             # Maximum tags per project
11             # identity v3.10 has 80
12             # leave a few empty for possible future extensions
13             Readonly our $MAXTAGS => 70;
14              
15             =head1 DESCRIPTION
16              
17             Make a simple storage-like object that uses the Identity v3 interface
18             based on project tags as a backend.
19              
20             It allows you to store a list of strings, that can be encoded/decoded.
21              
22             =head1 Methods
23              
24             =over
25              
26             =item _initialize
27              
28             =over
29              
30             =item client
31              
32             The C instance to use.
33             (It will also be used as the reporter instance.)
34              
35             =item project
36              
37             The base project (name or id) that will store the tags.
38             The project has to exist.
39              
40             All tags are stored in child projects with name C<>>.
41              
42             =back
43              
44             =cut
45              
46             sub _initialize
47             {
48 3     3   7 my ($self, $client, $project) = @_;
49              
50             # Use the client as reporter
51 3         13 $self->{log} = $client;
52              
53 3         6 $self->{cache} = undef;
54 3         7 $self->{empty} = []; # list of empty child projects (they are not in the tag cache)
55 3         6 $self->{counter} = undef; # counter of last child project
56 3         23 $self->{client} = $client;
57 3         8 $self->{project} = $project;
58 3         28 $self->{id} = $self->{client}->api_identity_get_id('project', $project);
59 3 50       10 if (defined($self->{id})) {
60 3         41 $self->verbose("Tagstore for $project intialised (id $self->{id})");
61             } else {
62 0         0 $self->error("No tagstore project id found for project $project");
63 0         0 return;
64             };
65              
66 3         768 return 1;
67             }
68              
69             =item _tag
70              
71             Interact with the API.
72              
73             =cut
74              
75             sub _tag
76             {
77 14     14   44 my ($self, $oper, $project_id, $tag) = @_;
78              
79 14         27 my $method = "api_identity_";
80 14         33 my %opts = (
81             project_id => $project_id,
82             );
83 14 50       37 $opts{tag} = $tag if defined($tag);
84 14 50       37 if ($oper eq 'get') {
85 0 0       0 $method .= 'tag' . (defined($tag) ? 's' : '');
86             } else {
87 14         30 $method .= "${oper}_tag";
88             }
89              
90 14         115 return $self->{client}->$method(%opts);
91             }
92              
93             =item fetch
94              
95             Fetch data and populate the cache (and counter).
96             If the cache already exists, it doesn't do anything.
97             If you want to renew the cache, flush it first.
98              
99             =cut
100              
101             sub fetch
102             {
103 28     28 1 3426 my $self = shift;
104              
105 28 100       73 if ($self->{cache}) {
106 24         80 $self->debug("fetch: tagstore cache exists, not doing anything");
107             } else {
108 4         13 $self->verbose("fetching tagstore data cache");
109             # gather all projects with parent_id $self->{project}
110 4         1018 my $resp = $self->{client}->api_identity_projects(parent_id => $self->{id});
111 4 50       12 if ($resp) {
112             # get all tags for each project and add them to the cache
113 4         10 $self->{cache} = {};
114 4         9 $self->{counter} = 0; # init with 0, no child project will ever have this counter
115 4 50       8 foreach my $proj (@{$resp->result || []}) {
  4         12  
116 7 50       38 if ($proj->{name} =~ m/_(\d+)$/) {
117 7 100       28 $self->{counter} = $1 if $1 > $self->{counter};
118             } else {
119             # this is not really a problem, as the the counter is only used
120             # to garantee uniqueness in the naming of the child projects
121 0         0 $self->warn("Child tagstore project of $self->{project} with name $proj->{name} ".
122             "(id $proj->{id}) does not match counter regex");
123             }
124              
125 7 50       11 my @tags = @{$proj->{tags} || []};
  7         27  
126 7 100       17 if (@tags) {
127 6         11 foreach my $tag (@tags) {
128 285         510 $self->{cache}->{$tag} = $proj->{id};
129             }
130             } else {
131             # handle empty projects
132 1         3 push(@{$self->{empty}}, $proj->{id});
  1         4  
133             }
134             }
135             } else {
136 0         0 $self->error("Can't get all tagstore projects with parent $self->{project} (id $self->{id})");
137             }
138             }
139 28         6176 return $self->{cache};
140             }
141              
142             =item flush
143              
144             Flushes the cache.
145              
146             =cut
147              
148             sub flush
149             {
150 2     2 1 2385 my ($self) = @_;
151              
152 2         10 $self->{cache} = undef;
153 2         3 $self->{counter} = undef;
154 2         10 $self->info('flushed tagstore cache and counter');
155             }
156              
157             =item get
158              
159             Return (cached) data for C.
160             If C is not defined, return all (cached) data as a hashref
161             (key is tag, value is projectid that holds the tag).
162             Data is fetched if cache is undefined.
163              
164             =cut
165              
166             sub get
167             {
168 9     9 1 18 my ($self, $tag) = @_;
169              
170 9         21 $self->fetch();
171              
172 9 100       24 if (defined($tag)) {
173             # no autovivification
174 8 100       39 return exists($self->{cache}->{$tag}) ? $self->{cache}->{$tag} : undef;
175             } else {
176 1         63 return $self->{cache};
177             };
178             }
179              
180             =item _sane_data
181              
182             Sanity check on tag data to add/delete.
183              
184             Returns 1 on success, undef on failure (and reports an error).
185              
186             =cut
187              
188             sub _sane_data
189             {
190 17     17   1494 my ($self, $method, $data) = @_;
191              
192 17         37 my $txt = "No sane tag data to $method:";
193 17 100       62 if (!defined($data)) {
194 1         17 $self->error("$txt undefined value");
195 1         240 return;
196             }
197              
198 16         29 my $ref = ref($data);
199 16 100       36 if ($ref ne '') {
200 1         6 $self->error("$txt only scalar allowed, got $ref.");
201 1         251 return;
202             }
203              
204 15         43 return 1;
205             }
206              
207             =item add
208              
209             Add element (to store and cache).
210              
211             Returns 1 on success; undef on failure (and reports an error).
212              
213             =cut
214              
215             sub add
216             {
217 9     9 1 24 my ($self, $data) = @_;
218              
219             # reports an error
220 9 50       24 $self->_sane_data('add', $data) or return;
221              
222 9         28 $self->fetch();
223              
224             # look for projectid that has tagspace left
225 9         16 my $pid = shift(@{$self->{empty}});
  9         24  
226 9 100       25 if (defined($pid)) {
227 1         6 $self->verbose("Using first empty tagstore project id $pid");
228             } else {
229 8         15 my %count;
230 8         11 foreach my $v (values %{$self->{cache}}) {
  8         30  
231 325         429 $count{$v}++;
232             };
233 8         29 my @avail = (grep {$count{$_} < $MAXTAGS} sort keys %count);
  10         41  
234              
235 8 100       55 if (@avail) {
236 7         14 $pid = $avail[0];
237 7         27 $self->verbose("using existing tagstore project $pid for $data");
238             } else {
239             # make new subproject
240 1         3 my $counter = $self->{counter};
241 1         13 $counter++; # used counter is never 0
242              
243 1         14 my $resp = $self->{client}->api_identity_add_project(name => "$self->{project}_$counter", parent_id => $self->{id});
244 1 50 33     3 if ($resp && $resp->result) {
245 1         3 $pid = $resp->result->{id};
246 1         5 $self->{counter} = $counter;
247             } else {
248 0         0 $self->error("Failed to create child tagstore project for counter $counter");
249 0         0 return;
250             }
251             }
252             }
253              
254             # add tag to project
255 9         2021 my $resp = $self->_tag('add', $pid, $data);
256 9 50       27 if ($resp) {
257             # add tag to cache
258 9         30 $self->{cache}->{$data} = $pid;
259 9         33 $self->verbose("Added $data to tagstore");
260             } else {
261 0         0 $self->error("Failed to add $data to tagstore (project child id $pid)");
262 0         0 return;
263             }
264              
265 9         2336 return 1;
266             }
267              
268             =item delete
269              
270             Delete item (from store and cache) if it exists in the cache.
271              
272             Returns 1 on success (incl. when the data was not available in the first place);
273             undef on failure (and reports an error).
274              
275             =cut
276              
277             sub delete
278             {
279 5     5 1 511 my ($self, $data) = @_;
280              
281             # reports an error
282 5 50       16 $self->_sane_data('delete', $data) or return;
283              
284 5         16 $self->fetch();
285              
286 5         16 my $pid = $self->{cache}->{$data};
287 5 50       16 if (defined($pid)) {
288             # delete tag from project
289 5         29 my $resp = $self->_tag('delete', $pid, $data);
290 5 50       13 if ($resp) {
291             # delete tag from cache
292 5         22 delete $self->{cache}->{$data};
293 5         23 $self->verbose("deleted $data from tagstore");
294 5 100       1262 if (! grep {$_ eq $pid} values %{$self->{cache}}) {
  165         250  
  5         25  
295 1         2 push(@{$self->{empty}}, $pid);
  1         4  
296             }
297             } else {
298 0         0 $self->error("Failed to delete $data from tagstore (project child id $pid)");
299 0         0 return;
300             }
301             }
302              
303 5         20 return 1;
304             }
305              
306              
307             =back
308              
309             =cut
310              
311              
312             1;